@wizzlethorpe/vaults 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asset-refs.js +274 -0
- package/dist/asset-refs.js.map +1 -0
- package/dist/auth.js.map +1 -1
- package/dist/build.js +252 -492
- package/dist/build.js.map +1 -1
- package/dist/commands/build.js +54 -5
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/preview.js +0 -4
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/push.js +4 -9
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/role.js +55 -14
- package/dist/commands/role.js.map +1 -1
- package/dist/config.js +25 -5
- package/dist/config.js.map +1 -1
- package/dist/foundry-importer.bundle.js +1228 -327
- package/dist/foundry-importer.js +2 -7
- package/dist/foundry-importer.js.map +1 -1
- package/dist/foundry-meta.js +284 -0
- package/dist/foundry-meta.js.map +1 -0
- package/dist/foundry-module-journal.js +112 -0
- package/dist/foundry-module-journal.js.map +1 -0
- package/dist/foundry-module-render.js +75 -0
- package/dist/foundry-module-render.js.map +1 -0
- package/dist/foundry-module.js +1075 -0
- package/dist/foundry-module.js.map +1 -0
- package/dist/frontmatter-defaults.js +68 -0
- package/dist/frontmatter-defaults.js.map +1 -0
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/dist/manifest.js +115 -0
- package/dist/manifest.js.map +1 -0
- package/dist/render/auth-template.js +323 -35
- package/dist/render/auth-template.js.map +1 -1
- package/dist/render/bases.js +22 -38
- package/dist/render/bases.js.map +1 -1
- package/dist/render/cover.js +23 -1
- package/dist/render/cover.js.map +1 -1
- package/dist/render/handlers/builtin/download.js +90 -0
- package/dist/render/handlers/builtin/download.js.map +1 -0
- package/dist/render/handlers/builtin/foundry-manifest.js +158 -0
- package/dist/render/handlers/builtin/foundry-manifest.js.map +1 -0
- package/dist/render/handlers/builtin/index.js +3 -1
- package/dist/render/handlers/builtin/index.js.map +1 -1
- package/dist/render/pipeline.js +4 -1
- package/dist/render/pipeline.js.map +1 -1
- package/dist/render/slug.js +0 -5
- package/dist/render/slug.js.map +1 -1
- package/dist/scan.js +8 -0
- package/dist/scan.js.map +1 -1
- package/dist/settings.js +112 -6
- package/dist/settings.js.map +1 -1
- package/package.json +2 -1
|
@@ -16,10 +16,15 @@ async function fetchManifest(vault) {
|
|
|
16
16
|
var BATCH_SIZE = 100;
|
|
17
17
|
var BATCH_CONCURRENCY = 4;
|
|
18
18
|
var DIRECT_CONCURRENCY = 8;
|
|
19
|
-
|
|
19
|
+
function batchEndpoint(vault, role) {
|
|
20
|
+
const endpoint = new URL(url(vault, "/_batch"));
|
|
21
|
+
if (role) endpoint.searchParams.set("role", role);
|
|
22
|
+
return endpoint;
|
|
23
|
+
}
|
|
24
|
+
async function fetchSourceBatch(vault, paths, role) {
|
|
20
25
|
if (paths.length === 0) return /* @__PURE__ */ new Map();
|
|
21
26
|
if (vault.public) return fetchSourceDirect(vault, paths);
|
|
22
|
-
const endpoint =
|
|
27
|
+
const endpoint = batchEndpoint(vault, role);
|
|
23
28
|
const chunks = [];
|
|
24
29
|
for (let i = 0; i < paths.length; i += BATCH_SIZE) chunks.push(paths.slice(i, i + BATCH_SIZE));
|
|
25
30
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -27,7 +32,7 @@ async function fetchSourceBatch(vault, paths) {
|
|
|
27
32
|
const workers = Array.from({ length: Math.min(BATCH_CONCURRENCY, chunks.length) }, async () => {
|
|
28
33
|
while (next < chunks.length) {
|
|
29
34
|
const idx = next++;
|
|
30
|
-
const res = await fetch(endpoint, {
|
|
35
|
+
const res = await fetch(endpoint.toString(), {
|
|
31
36
|
method: "POST",
|
|
32
37
|
headers: { "Content-Type": "text/plain" },
|
|
33
38
|
body: chunks[idx].join("\n")
|
|
@@ -69,6 +74,185 @@ async function fetchSourceDirect(vault, paths) {
|
|
|
69
74
|
// ../foundry/scripts/settings.mjs
|
|
70
75
|
var MODULE_ID = "vaults";
|
|
71
76
|
|
|
77
|
+
// ../foundry/scripts/foundry-base.mjs
|
|
78
|
+
var PACK_KEY = {
|
|
79
|
+
Actor: "actors",
|
|
80
|
+
Item: "items",
|
|
81
|
+
Scene: "scenes",
|
|
82
|
+
JournalEntry: "journal",
|
|
83
|
+
RollTable: "tables",
|
|
84
|
+
Macro: "macros",
|
|
85
|
+
Cards: "cards",
|
|
86
|
+
Playlist: "playlists"
|
|
87
|
+
};
|
|
88
|
+
var BLANK_DOC_TYPES = [
|
|
89
|
+
"Actor",
|
|
90
|
+
"Item",
|
|
91
|
+
"Scene",
|
|
92
|
+
"JournalEntry",
|
|
93
|
+
"RollTable",
|
|
94
|
+
"Macro",
|
|
95
|
+
"Cards",
|
|
96
|
+
"Playlist"
|
|
97
|
+
];
|
|
98
|
+
function canonicalType(raw) {
|
|
99
|
+
if (!raw) return null;
|
|
100
|
+
return BLANK_DOC_TYPES.find((t) => t.toLowerCase() === raw.toLowerCase()) ?? null;
|
|
101
|
+
}
|
|
102
|
+
function parseFoundryBase(spec) {
|
|
103
|
+
if (typeof spec !== "string" || !spec) return null;
|
|
104
|
+
if (spec.startsWith("@moulinette/")) {
|
|
105
|
+
const ref = spec.slice("@moulinette/".length);
|
|
106
|
+
return ref ? { kind: "moulinette", ref } : null;
|
|
107
|
+
}
|
|
108
|
+
if (spec.includes(".")) return { kind: "uuid", uuid: spec };
|
|
109
|
+
const [typeRaw, subtype] = spec.split(":");
|
|
110
|
+
const docName = canonicalType(typeRaw);
|
|
111
|
+
if (!docName) return null;
|
|
112
|
+
return { kind: "blank", docName, subtype: subtype || void 0 };
|
|
113
|
+
}
|
|
114
|
+
function docNameOf(parsed) {
|
|
115
|
+
if (!parsed) return null;
|
|
116
|
+
if (parsed.kind === "blank") return parsed.docName;
|
|
117
|
+
if (parsed.kind === "moulinette") return null;
|
|
118
|
+
const parts = parsed.uuid.split(".");
|
|
119
|
+
if (parts.length < 2) return null;
|
|
120
|
+
const raw = parts[parts.length - 2];
|
|
121
|
+
return canonicalType(raw) ?? raw ?? null;
|
|
122
|
+
}
|
|
123
|
+
function docNameFromBase(base) {
|
|
124
|
+
const specs = Array.isArray(base) ? base : [base];
|
|
125
|
+
for (const spec of specs) {
|
|
126
|
+
const docName = docNameOf(parseFoundryBase(spec));
|
|
127
|
+
if (docName) return docName;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ../foundry/scripts/packs.mjs
|
|
133
|
+
function packName(vault, docName) {
|
|
134
|
+
const key = docName === "Adventure" ? "adventure" : PACK_KEY[docName];
|
|
135
|
+
if (!key) return null;
|
|
136
|
+
return `${vault.id}-${key}`;
|
|
137
|
+
}
|
|
138
|
+
function packCollection(vault, docName) {
|
|
139
|
+
const name = packName(vault, docName);
|
|
140
|
+
return name ? `world.${name}` : null;
|
|
141
|
+
}
|
|
142
|
+
function isAdventure(vault) {
|
|
143
|
+
return vault.foundryPackage === "adventure";
|
|
144
|
+
}
|
|
145
|
+
function uuidPrefix(vault, docName) {
|
|
146
|
+
return isAdventure(vault) ? "" : `Compendium.${packCollection(vault, docName)}.`;
|
|
147
|
+
}
|
|
148
|
+
async function pruneStalePacks(vault) {
|
|
149
|
+
const wanted = isAdventure(vault) ? /* @__PURE__ */ new Set(["Adventure"]) : new Set(Object.keys(PACK_KEY));
|
|
150
|
+
for (const docName of [...Object.keys(PACK_KEY), "Adventure"]) {
|
|
151
|
+
if (wanted.has(docName)) continue;
|
|
152
|
+
const pack = getPack(vault, docName);
|
|
153
|
+
if (!pack) continue;
|
|
154
|
+
try {
|
|
155
|
+
await pack.deleteCompendium();
|
|
156
|
+
console.info(
|
|
157
|
+
`Vaults | ${vault.label}: removed ${pack.collection}, left over from the previous foundry_package setting.`
|
|
158
|
+
);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.warn(`Vaults | could not remove the stale pack ${pack.collection}:`, err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function journalPageUuid(vault, entryId2, pageId2) {
|
|
165
|
+
return uuidPrefix(vault, "JournalEntry") + `JournalEntry.${entryId2}.JournalEntryPage.${pageId2}`;
|
|
166
|
+
}
|
|
167
|
+
function instanceUuid(vault, docName, id) {
|
|
168
|
+
return uuidPrefix(vault, docName) + `${docName}.${id}`;
|
|
169
|
+
}
|
|
170
|
+
function getPack(vault, docName) {
|
|
171
|
+
const collection = packCollection(vault, docName);
|
|
172
|
+
return collection ? game.packs.get(collection) ?? null : null;
|
|
173
|
+
}
|
|
174
|
+
function vaultPacks(vault) {
|
|
175
|
+
return [...Object.keys(PACK_KEY), "Adventure"].map((docName) => getPack(vault, docName)).filter((pack) => pack !== null);
|
|
176
|
+
}
|
|
177
|
+
async function deleteVaultPacks(vaultId) {
|
|
178
|
+
for (const pack of vaultPacks({ id: vaultId })) {
|
|
179
|
+
try {
|
|
180
|
+
await pack.deleteCompendium();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.warn(`Vaults | failed to delete pack ${pack.collection}:`, err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
187
|
+
async function ensurePack(vault, docName) {
|
|
188
|
+
const collection = packCollection(vault, docName);
|
|
189
|
+
if (!collection) throw new Error(`No pack is defined for ${docName} documents`);
|
|
190
|
+
const existing = game.packs.get(collection);
|
|
191
|
+
if (existing) {
|
|
192
|
+
assertWritable(existing, vault);
|
|
193
|
+
await enforceOwnership(existing, vault);
|
|
194
|
+
return existing;
|
|
195
|
+
}
|
|
196
|
+
const pending = inFlight.get(collection);
|
|
197
|
+
if (pending) return pending;
|
|
198
|
+
const promise = createPack(vault, docName, collection).finally(() => inFlight.delete(collection));
|
|
199
|
+
inFlight.set(collection, promise);
|
|
200
|
+
return promise;
|
|
201
|
+
}
|
|
202
|
+
function ownershipFor(vault) {
|
|
203
|
+
const base = { GAMEMASTER: "OWNER", ASSISTANT: "OWNER" };
|
|
204
|
+
return vault.public ? { ...base, TRUSTED: "OBSERVER", PLAYER: "OBSERVER" } : { ...base, TRUSTED: "NONE", PLAYER: "NONE" };
|
|
205
|
+
}
|
|
206
|
+
async function enforceOwnership(pack, vault) {
|
|
207
|
+
const want = ownershipFor(vault);
|
|
208
|
+
const have = pack.config.ownership;
|
|
209
|
+
if (have && Object.entries(want).every(([k, v]) => have[k] === v)) return;
|
|
210
|
+
await pack.configure({ ownership: want });
|
|
211
|
+
if (!vault.public) {
|
|
212
|
+
console.info(
|
|
213
|
+
`Vaults | ${pack.collection}: restricted to GM. It was readable by ${have?.PLAYER ?? "PLAYER: OBSERVER (Foundry's default)"}, which exposes every name and image in the pack index.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function createPack(vault, docName, collection) {
|
|
218
|
+
const label = `${vault.label || "Vault"}: ${LABEL[docName] ?? docName}`;
|
|
219
|
+
await foundry.documents.collections.CompendiumCollection.createCompendium({
|
|
220
|
+
type: docName,
|
|
221
|
+
label,
|
|
222
|
+
name: packName(vault, docName),
|
|
223
|
+
packageType: "world"
|
|
224
|
+
});
|
|
225
|
+
const pack = game.packs.get(collection);
|
|
226
|
+
if (!pack) throw new Error(`Compendium pack ${collection} was not created`);
|
|
227
|
+
await pack.setFolder(await ensurePackFolder(vault));
|
|
228
|
+
assertWritable(pack, vault);
|
|
229
|
+
await enforceOwnership(pack, vault);
|
|
230
|
+
return pack;
|
|
231
|
+
}
|
|
232
|
+
function assertWritable(pack, vault) {
|
|
233
|
+
if (!pack.locked) return;
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Compendium pack ${pack.collection} is locked. Unlock it in the sidebar to let ${vault.label || "this vault"} sync into it.`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
async function ensurePackFolder(vault) {
|
|
239
|
+
const name = vault.rootFolder || vault.label || "Vault";
|
|
240
|
+
const existing = game.folders.find((f) => f.type === "Compendium" && f.name === name);
|
|
241
|
+
if (existing) return existing;
|
|
242
|
+
return Folder.create({ name, type: "Compendium" });
|
|
243
|
+
}
|
|
244
|
+
var LABEL = {
|
|
245
|
+
Adventure: "Adventure",
|
|
246
|
+
Actor: "Actors",
|
|
247
|
+
Item: "Items",
|
|
248
|
+
Scene: "Scenes",
|
|
249
|
+
JournalEntry: "Journals",
|
|
250
|
+
RollTable: "Roll Tables",
|
|
251
|
+
Macro: "Macros",
|
|
252
|
+
Cards: "Cards",
|
|
253
|
+
Playlist: "Playlists"
|
|
254
|
+
};
|
|
255
|
+
|
|
72
256
|
// ../foundry/scripts/util.mjs
|
|
73
257
|
function escapeAttr(s) {
|
|
74
258
|
return String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
@@ -83,6 +267,11 @@ async function hexDigest(algorithm, text) {
|
|
|
83
267
|
const buf = await crypto.subtle.digest(algorithm, new TextEncoder().encode(text));
|
|
84
268
|
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
85
269
|
}
|
|
270
|
+
function localizeOr(host, key, fallback, args) {
|
|
271
|
+
const text = host.localize(key, args);
|
|
272
|
+
if (text && text !== key) return text;
|
|
273
|
+
return String(fallback).replace(/\{(\w+)\}/g, (whole, name) => args && name in args ? String(args[name]) : whole);
|
|
274
|
+
}
|
|
86
275
|
|
|
87
276
|
// ../foundry/scripts/ids.mjs
|
|
88
277
|
async function det(kind, key) {
|
|
@@ -98,10 +287,65 @@ var pageId = (vaultId, path) => det("page", `${vaultId}:${path}`);
|
|
|
98
287
|
var folderId = (vaultId, path) => det("folder", `${vaultId}:${path}`);
|
|
99
288
|
var instanceId = (vaultId, path) => det("instance", `${vaultId}:${path}`);
|
|
100
289
|
var subdocId = (vaultId, path, pointer) => det("subdoc", `${vaultId}:${path}:${pointer}`);
|
|
290
|
+
var adventureId = (vaultId) => det("adventure", vaultId);
|
|
101
291
|
|
|
102
292
|
// ../foundry/scripts/parser.mjs
|
|
103
293
|
var CACHED_EXT_RE = /\.(png|jpe?g|webp|gif|svg|avif|tiff?|bmp|heic|apng|ogg|mp3|m4a|wav|flac|opus|aac|mp4|webm|mov|ogv|pdf|epub|json)$/i;
|
|
104
294
|
|
|
295
|
+
// ../foundry/scripts/progress.mjs
|
|
296
|
+
var bar = null;
|
|
297
|
+
var vaultLabel = "";
|
|
298
|
+
var phaseLabel = "";
|
|
299
|
+
var done = 0;
|
|
300
|
+
var total = 0;
|
|
301
|
+
function paint(message) {
|
|
302
|
+
if (!bar) return;
|
|
303
|
+
const counter = total > 0 ? ` ${Math.min(done, total)}/${total}` : "";
|
|
304
|
+
const head = phaseLabel ? `${vaultLabel}: ${phaseLabel}${counter}` : vaultLabel;
|
|
305
|
+
try {
|
|
306
|
+
bar.update({
|
|
307
|
+
pct: total > 0 ? Math.min(done / total, 1) : 0,
|
|
308
|
+
message: message ? `${head} \u2014 ${message}` : head
|
|
309
|
+
});
|
|
310
|
+
} catch {
|
|
311
|
+
bar = null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function begin(label) {
|
|
315
|
+
vaultLabel = label;
|
|
316
|
+
phaseLabel = "";
|
|
317
|
+
done = 0;
|
|
318
|
+
total = 0;
|
|
319
|
+
try {
|
|
320
|
+
const handle = ui?.notifications?.info(label, { progress: true, permanent: true });
|
|
321
|
+
bar = typeof handle?.update === "function" ? handle : null;
|
|
322
|
+
} catch {
|
|
323
|
+
bar = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function phase(name, count) {
|
|
327
|
+
phaseLabel = name;
|
|
328
|
+
done = 0;
|
|
329
|
+
total = count;
|
|
330
|
+
paint("");
|
|
331
|
+
}
|
|
332
|
+
function step(message) {
|
|
333
|
+
done++;
|
|
334
|
+
paint(message);
|
|
335
|
+
}
|
|
336
|
+
function note(message) {
|
|
337
|
+
paint(message);
|
|
338
|
+
}
|
|
339
|
+
function end() {
|
|
340
|
+
if (!bar) return;
|
|
341
|
+
try {
|
|
342
|
+
ui?.notifications?.remove?.(bar);
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
bar = null;
|
|
346
|
+
phaseLabel = "";
|
|
347
|
+
}
|
|
348
|
+
|
|
105
349
|
// ../foundry/scripts/media.mjs
|
|
106
350
|
var CACHE_DIR = "vaults-cache";
|
|
107
351
|
var BATCH_SIZE2 = 25;
|
|
@@ -111,6 +355,7 @@ function isCacheable(path) {
|
|
|
111
355
|
if (!CACHED_EXT_RE.test(path)) return false;
|
|
112
356
|
if (path.startsWith("_") || path.includes("/_")) return false;
|
|
113
357
|
if (/\.preview\.json$/i.test(path)) return false;
|
|
358
|
+
if (/(^|\/)module\.json$/i.test(path)) return false;
|
|
114
359
|
return true;
|
|
115
360
|
}
|
|
116
361
|
function localFileUrl(vaultId, vaultPath) {
|
|
@@ -131,13 +376,19 @@ async function syncImages(host, vault, manifestFiles) {
|
|
|
131
376
|
}
|
|
132
377
|
const lastImageManifest = host.getVaultState(vault.id).lastImageManifest;
|
|
133
378
|
const last = new Map(Object.entries(lastImageManifest || {}));
|
|
379
|
+
const baseDir = vaultCacheDir(vault.id);
|
|
380
|
+
const present = await listCached(baseDir, last.keys());
|
|
381
|
+
const vanished = [...last.keys()].filter((p) => !present.has(p));
|
|
382
|
+
for (const path of vanished) last.delete(path);
|
|
383
|
+
if (vanished.length > 0) {
|
|
384
|
+
console.info(`Vaults | ${vanished.length} cached file(s) are gone from ${baseDir}; re-fetching.`);
|
|
385
|
+
}
|
|
134
386
|
const toDownload = [];
|
|
135
387
|
for (const [path, hash] of remoteImages) {
|
|
136
388
|
if (last.get(path) !== hash) toDownload.push(path);
|
|
137
389
|
}
|
|
138
|
-
|
|
390
|
+
let toDelete = [...last.keys()].filter((p) => !remoteImages.has(p));
|
|
139
391
|
if (toDownload.length === 0 && toDelete.length === 0) return { downloaded: 0, removed: 0, errors: 0 };
|
|
140
|
-
const baseDir = vaultCacheDir(vault.id);
|
|
141
392
|
const worldRoot = `worlds/${game.world.id}`;
|
|
142
393
|
const dirsNeeded = /* @__PURE__ */ new Set();
|
|
143
394
|
const addChain = (fullPath) => {
|
|
@@ -155,6 +406,7 @@ async function syncImages(host, vault, manifestFiles) {
|
|
|
155
406
|
if (segs.length > 0) addChain(`${baseDir}/${segs.join("/")}`);
|
|
156
407
|
}
|
|
157
408
|
await ensureDirs([...dirsNeeded]);
|
|
409
|
+
await writeCacheMarker(baseDir, vault);
|
|
158
410
|
const sizeOf = /* @__PURE__ */ new Map();
|
|
159
411
|
for (const f of manifestFiles) sizeOf.set(f.path, f.size ?? 0);
|
|
160
412
|
const chunks = [];
|
|
@@ -171,6 +423,7 @@ async function syncImages(host, vault, manifestFiles) {
|
|
|
171
423
|
chunkBytes += bytes;
|
|
172
424
|
}
|
|
173
425
|
if (chunk.length > 0) chunks.push(chunk);
|
|
426
|
+
phase("Images", toDownload.length);
|
|
174
427
|
let next = 0;
|
|
175
428
|
const downloaded = [];
|
|
176
429
|
const errors = [];
|
|
@@ -187,6 +440,7 @@ async function syncImages(host, vault, manifestFiles) {
|
|
|
187
440
|
continue;
|
|
188
441
|
}
|
|
189
442
|
try {
|
|
443
|
+
step(path.split("/").pop());
|
|
190
444
|
await uploadToWorld(baseDir, path, blob);
|
|
191
445
|
downloaded.push(path);
|
|
192
446
|
} catch (err) {
|
|
@@ -203,6 +457,10 @@ async function syncImages(host, vault, manifestFiles) {
|
|
|
203
457
|
console.warn(`Vaults | ${errors.length} image(s) failed to download:`, errors);
|
|
204
458
|
}
|
|
205
459
|
let removed = 0;
|
|
460
|
+
if (toDelete.length > 0 && !canDelete()) {
|
|
461
|
+
console.info(`Vaults | ${toDelete.length} cached image(s) are no longer in the vault. Foundry provides no API to delete files, so they stay in ${baseDir} until removed by hand.`);
|
|
462
|
+
toDelete = [];
|
|
463
|
+
}
|
|
206
464
|
for (const path of toDelete) {
|
|
207
465
|
try {
|
|
208
466
|
await deleteFromWorld(baseDir, path);
|
|
@@ -302,6 +560,47 @@ async function ensureDirs(paths) {
|
|
|
302
560
|
}
|
|
303
561
|
}
|
|
304
562
|
}
|
|
563
|
+
async function writeCacheMarker(baseDir, vault) {
|
|
564
|
+
const body = JSON.stringify({
|
|
565
|
+
label: vault.label ?? null,
|
|
566
|
+
url: vault.url ?? null,
|
|
567
|
+
vaultId: vault.id,
|
|
568
|
+
note: "Cache for one vault. Safe to delete entirely; the next sync re-downloads it."
|
|
569
|
+
}, null, 2);
|
|
570
|
+
try {
|
|
571
|
+
await uploadToWorld(baseDir, "vault-info.json", new Blob([body], { type: "application/json" }));
|
|
572
|
+
} catch (err) {
|
|
573
|
+
console.debug(`Vaults | could not write cache marker in ${baseDir}:`, err?.message || err);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function listCached(baseDir, paths) {
|
|
577
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
578
|
+
for (const p of paths) {
|
|
579
|
+
const segs = p.split("/");
|
|
580
|
+
segs.pop();
|
|
581
|
+
const dir = segs.join("/");
|
|
582
|
+
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
583
|
+
byDir.get(dir).push(p);
|
|
584
|
+
}
|
|
585
|
+
const present = /* @__PURE__ */ new Set();
|
|
586
|
+
for (const dir of byDir.keys()) {
|
|
587
|
+
const full = dir ? `${baseDir}/${dir}` : baseDir;
|
|
588
|
+
let listing;
|
|
589
|
+
try {
|
|
590
|
+
listing = await fp().browse("data", full);
|
|
591
|
+
} catch {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
for (const file of listing?.files ?? []) {
|
|
595
|
+
const name = decodeURIComponent(String(file).split("/").pop());
|
|
596
|
+
present.add(dir ? `${dir}/${name}` : name);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return present;
|
|
600
|
+
}
|
|
601
|
+
function canDelete() {
|
|
602
|
+
return typeof fp().deleteFile === "function";
|
|
603
|
+
}
|
|
305
604
|
async function deleteFromWorld(baseDir, path) {
|
|
306
605
|
const full = `${baseDir}/${path}`;
|
|
307
606
|
const impl = fp();
|
|
@@ -364,25 +663,19 @@ function buildPathIndex(manifestFiles) {
|
|
|
364
663
|
docTargets.set(mdPath, docName);
|
|
365
664
|
}
|
|
366
665
|
}
|
|
367
|
-
|
|
666
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
667
|
+
for (const f of manifestFiles) if (f.hash) hashes.set(f.path, f.hash);
|
|
668
|
+
return { paths, idOverrides, docTargets, hashes };
|
|
368
669
|
}
|
|
369
|
-
function
|
|
370
|
-
if (typeof base !== "string" || !base) return null;
|
|
371
|
-
if (base.startsWith("Compendium.")) {
|
|
372
|
-
const parts = base.split(".");
|
|
373
|
-
return parts.length >= 5 ? parts[3] : null;
|
|
374
|
-
}
|
|
375
|
-
return base.split(":")[0] || null;
|
|
376
|
-
}
|
|
377
|
-
async function targetUuid(vaultId, path, index) {
|
|
670
|
+
async function targetUuid(vault, path, index) {
|
|
378
671
|
const docName = index.docTargets?.get(path);
|
|
379
672
|
if (docName) {
|
|
380
|
-
const id = index.idOverrides?.get(path) ?? await instanceId(
|
|
381
|
-
return
|
|
673
|
+
const id = index.idOverrides?.get(path) ?? await instanceId(vault.id, path);
|
|
674
|
+
return instanceUuid(vault, docName, id);
|
|
382
675
|
}
|
|
383
|
-
const eId = await entryId(
|
|
384
|
-
const pId = index.idOverrides?.get(path) ?? await pageId(
|
|
385
|
-
return
|
|
676
|
+
const eId = await entryId(vault.id, path);
|
|
677
|
+
const pId = index.idOverrides?.get(path) ?? await pageId(vault.id, path);
|
|
678
|
+
return journalPageUuid(vault, eId, pId);
|
|
386
679
|
}
|
|
387
680
|
function logicalPathFromHref(href) {
|
|
388
681
|
const decoded = decodeHtmlEntities(href);
|
|
@@ -398,9 +691,9 @@ function decodeHtmlEntities(s) {
|
|
|
398
691
|
ta.innerHTML = s;
|
|
399
692
|
return ta.value;
|
|
400
693
|
}
|
|
401
|
-
async function transformHtmlForFoundry(vault, html, index) {
|
|
402
|
-
html = await rewriteWikilinks(vault
|
|
403
|
-
html = rewriteMediaSrcs(vault.id, html);
|
|
694
|
+
async function transformHtmlForFoundry(vault, html, index, mediaRefs) {
|
|
695
|
+
html = await rewriteWikilinks(vault, html, index);
|
|
696
|
+
html = rewriteMediaSrcs(vault.id, html, index?.hashes, mediaRefs);
|
|
404
697
|
html = rewritePassthroughLinks(vault.id, html);
|
|
405
698
|
html = await applyDomTransforms(html, vault, index);
|
|
406
699
|
return html;
|
|
@@ -411,7 +704,7 @@ async function applyDomTransforms(html, vault, index) {
|
|
|
411
704
|
touched = stripWebOnlyWidgets(doc) || touched;
|
|
412
705
|
touched = flattenBasesTabs(doc) || touched;
|
|
413
706
|
touched = neutralizeEnrichersInCode(doc) || touched;
|
|
414
|
-
touched = await rewriteBasesCardLinks(doc, vault
|
|
707
|
+
touched = await rewriteBasesCardLinks(doc, vault, index) || touched;
|
|
415
708
|
touched = rewriteDiceButtons(doc) || touched;
|
|
416
709
|
touched = wrapRestrictedCalloutsAsSecret(doc, vault) || touched;
|
|
417
710
|
return touched ? doc.body.innerHTML : html;
|
|
@@ -465,7 +758,18 @@ function neutralizeEnrichersInCode(doc) {
|
|
|
465
758
|
}
|
|
466
759
|
return touched;
|
|
467
760
|
}
|
|
468
|
-
|
|
761
|
+
function contentLinkAttrs(uuid) {
|
|
762
|
+
return {
|
|
763
|
+
// Empty string, matching what TextEditor.createAnchor emits: the selector
|
|
764
|
+
// tests for presence, not value.
|
|
765
|
+
"data-link": "",
|
|
766
|
+
"data-uuid": uuid,
|
|
767
|
+
// Real content links are draggable, and the drag handler keys off the same
|
|
768
|
+
// attribute; without this a card cannot be dropped onto the canvas.
|
|
769
|
+
draggable: "true"
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
async function rewriteBasesCardLinks(doc, vault, index) {
|
|
469
773
|
const cards = doc.querySelectorAll("a.bases-card[href]");
|
|
470
774
|
if (cards.length === 0) return false;
|
|
471
775
|
let touched = false;
|
|
@@ -475,23 +779,25 @@ async function rewriteBasesCardLinks(doc, vaultId, index) {
|
|
|
475
779
|
const path = logicalPathFromHref(href);
|
|
476
780
|
if (!index.paths.has(path)) continue;
|
|
477
781
|
a.classList.add("content-link");
|
|
478
|
-
|
|
782
|
+
for (const [k, v] of Object.entries(contentLinkAttrs(await targetUuid(vault, path, index)))) {
|
|
783
|
+
a.setAttribute(k, v);
|
|
784
|
+
}
|
|
479
785
|
a.removeAttribute("href");
|
|
480
786
|
touched = true;
|
|
481
787
|
}
|
|
482
788
|
return touched;
|
|
483
789
|
}
|
|
484
790
|
function wrapRestrictedCalloutsAsSecret(doc, vault) {
|
|
485
|
-
if (!vault?.
|
|
791
|
+
if (!vault?.playerRole || !Array.isArray(vault.knownRoles) || vault.knownRoles.length === 0) {
|
|
486
792
|
return false;
|
|
487
793
|
}
|
|
488
|
-
const
|
|
489
|
-
if (
|
|
490
|
-
const restrictedRoles = vault.knownRoles.slice(
|
|
794
|
+
const playerIdx = vault.knownRoles.indexOf(vault.playerRole);
|
|
795
|
+
if (playerIdx < 0) return false;
|
|
796
|
+
const restrictedRoles = vault.knownRoles.slice(playerIdx + 1);
|
|
491
797
|
if (restrictedRoles.length === 0) return false;
|
|
492
798
|
let touched = false;
|
|
493
799
|
for (const role of restrictedRoles) {
|
|
494
|
-
for (const el of doc.querySelectorAll(
|
|
800
|
+
for (const el of doc.querySelectorAll(calloutSelectorFor(role))) {
|
|
495
801
|
const section = doc.createElement("section");
|
|
496
802
|
section.className = "secret";
|
|
497
803
|
el.parentNode.insertBefore(section, el);
|
|
@@ -501,10 +807,13 @@ function wrapRestrictedCalloutsAsSecret(doc, vault) {
|
|
|
501
807
|
}
|
|
502
808
|
return touched;
|
|
503
809
|
}
|
|
810
|
+
function calloutSelectorFor(role) {
|
|
811
|
+
return ".callout.callout-" + cssEscape(String(role).toLowerCase());
|
|
812
|
+
}
|
|
504
813
|
function cssEscape(s) {
|
|
505
814
|
return String(s).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
506
815
|
}
|
|
507
|
-
async function rewriteWikilinks(
|
|
816
|
+
async function rewriteWikilinks(vault, html, index) {
|
|
508
817
|
const matches = [];
|
|
509
818
|
let m;
|
|
510
819
|
ANCHOR_RE.lastIndex = 0;
|
|
@@ -525,7 +834,7 @@ async function rewriteWikilinks(vaultId, html, index) {
|
|
|
525
834
|
}
|
|
526
835
|
const uuidMatches = matches.filter((r) => r.kind === "uuid");
|
|
527
836
|
const resolved = await Promise.all(
|
|
528
|
-
uuidMatches.map((r) => targetUuid(
|
|
837
|
+
uuidMatches.map((r) => targetUuid(vault, r.path, index))
|
|
529
838
|
);
|
|
530
839
|
uuidMatches.forEach((r, i) => {
|
|
531
840
|
r.uuid = resolved[i];
|
|
@@ -537,14 +846,20 @@ async function rewriteWikilinks(vaultId, html, index) {
|
|
|
537
846
|
}
|
|
538
847
|
return html;
|
|
539
848
|
}
|
|
540
|
-
function rewriteMediaSrcs(vaultId, html) {
|
|
849
|
+
function rewriteMediaSrcs(vaultId, html, hashes, mediaRefs) {
|
|
541
850
|
return html.replace(MEDIA_SRC_RE, (full, tag, before, src, after) => {
|
|
542
851
|
if (!src.startsWith("/")) return full;
|
|
543
852
|
const path = decodeURIComponent(src.replace(/^\//, ""));
|
|
544
853
|
if (!CACHED_EXT_RE.test(path)) return full;
|
|
545
|
-
|
|
854
|
+
mediaRefs?.add(path);
|
|
855
|
+
const url2 = localFileUrl(vaultId, path) + mediaVersion(hashes, path);
|
|
856
|
+
return `<${tag}${before}src="${escapeAttr(url2)}"${after}>`;
|
|
546
857
|
});
|
|
547
858
|
}
|
|
859
|
+
function mediaVersion(hashes, path) {
|
|
860
|
+
const hash = hashes?.get(path);
|
|
861
|
+
return hash ? `?v=${encodeURIComponent(String(hash).slice(0, 12))}` : "";
|
|
862
|
+
}
|
|
548
863
|
function rewritePassthroughLinks(vaultId, html) {
|
|
549
864
|
return html.replace(ANCHOR_RE, (full, attrs, inner) => {
|
|
550
865
|
const cls = ATTR_CLASS_RE.exec(attrs)?.[1] || "";
|
|
@@ -564,46 +879,17 @@ function stripTags(s) {
|
|
|
564
879
|
// ../foundry/scripts/importer.mjs
|
|
565
880
|
var INDEX_BASENAME = "index";
|
|
566
881
|
var NON_INDEX_SORT_BASE = 1e5;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
"
|
|
570
|
-
"Scene",
|
|
571
|
-
"JournalEntry",
|
|
572
|
-
"RollTable",
|
|
573
|
-
"Macro",
|
|
574
|
-
"Cards",
|
|
575
|
-
"Playlist"
|
|
576
|
-
];
|
|
577
|
-
function inferInstanceDocName(base) {
|
|
578
|
-
if (typeof base !== "string" || !base) return null;
|
|
579
|
-
const raw = base.includes(".") ? base.split(".").at(-2) : base.split(":")[0];
|
|
580
|
-
return KNOWN_INSTANCE_DOC_TYPES.find((t) => t.toLowerCase() === raw?.toLowerCase()) ?? null;
|
|
581
|
-
}
|
|
582
|
-
async function ensureFolderChain(vault, segments) {
|
|
583
|
-
const rootName = vault.rootFolder || vault.label || "Vault";
|
|
584
|
-
const rootKey = `${vault.id}/__root__/${rootName}`;
|
|
585
|
-
const rootFId = await folderId(vault.id, rootKey);
|
|
586
|
-
await upsertFolder(rootFId, rootName, null);
|
|
587
|
-
let parentId = rootFId;
|
|
588
|
-
let acc = rootKey;
|
|
882
|
+
async function ensureFolderChain(target, vault, segments) {
|
|
883
|
+
let parentId = null;
|
|
884
|
+
let acc = `${vault.id}/__root__/${vault.rootFolder || vault.label || "Vault"}`;
|
|
589
885
|
for (const seg of segments) {
|
|
590
886
|
acc += "/" + seg;
|
|
591
887
|
const fId = await folderId(vault.id, acc);
|
|
592
|
-
await
|
|
888
|
+
await target.putFolder("JournalEntry", { _id: fId, name: seg, folder: parentId });
|
|
593
889
|
parentId = fId;
|
|
594
890
|
}
|
|
595
891
|
return parentId;
|
|
596
892
|
}
|
|
597
|
-
async function upsertFolder(id, name, parentId) {
|
|
598
|
-
const existing = game.folders.get(id);
|
|
599
|
-
if (existing) {
|
|
600
|
-
if (existing.name !== name || existing.folder?.id !== parentId) {
|
|
601
|
-
await existing.update({ name, folder: parentId });
|
|
602
|
-
}
|
|
603
|
-
return existing;
|
|
604
|
-
}
|
|
605
|
-
return Folder.create({ _id: id, name, type: "JournalEntry", folder: parentId }, { keepId: true });
|
|
606
|
-
}
|
|
607
893
|
function buildFolderInfo(mdPaths) {
|
|
608
894
|
const map = /* @__PURE__ */ new Map();
|
|
609
895
|
const ensure = (folderPath) => {
|
|
@@ -629,8 +915,8 @@ function buildFolderInfo(mdPaths) {
|
|
|
629
915
|
function isIndexFile(filename) {
|
|
630
916
|
return filename.replace(/\.md$/i, "") === INDEX_BASENAME;
|
|
631
917
|
}
|
|
632
|
-
async function upsertFile(vault, path, body, index, meta, folderInfo) {
|
|
633
|
-
let html = await transformHtmlForFoundry(vault, body, index);
|
|
918
|
+
async function upsertFile(target, vault, path, body, index, meta, folderInfo, mediaRefs) {
|
|
919
|
+
let html = await transformHtmlForFoundry(vault, body, index, mediaRefs);
|
|
634
920
|
html = await appendInstanceDocLink(html, vault, path, meta);
|
|
635
921
|
const segs = path.split("/");
|
|
636
922
|
const filename = segs.pop();
|
|
@@ -638,7 +924,7 @@ async function upsertFile(vault, path, body, index, meta, folderInfo) {
|
|
|
638
924
|
const fInfo = folderInfo?.get(folderPath);
|
|
639
925
|
const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
|
|
640
926
|
const hostSegs = leaf ? segs.slice(0, -1) : segs;
|
|
641
|
-
const folderFId = await ensureFolderChain(vault, hostSegs);
|
|
927
|
+
const folderFId = await ensureFolderChain(target, vault, hostSegs);
|
|
642
928
|
const entryName = folderPath === "" ? vault.rootFolder || vault.label || "Vault" : fInfo?.displayName || segs[segs.length - 1] || "";
|
|
643
929
|
const pageName = meta?.title || filename.replace(/\.md$/i, "");
|
|
644
930
|
const eId = await entryId(vault.id, path);
|
|
@@ -666,311 +952,555 @@ async function upsertFile(vault, path, body, index, meta, folderInfo) {
|
|
|
666
952
|
// page in it). See reconcileOwnership below.
|
|
667
953
|
...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
|
|
668
954
|
};
|
|
669
|
-
const existing =
|
|
955
|
+
const existing = await target.get("JournalEntry", eId);
|
|
670
956
|
if (existing) {
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
957
|
+
const pages = (existing.pages ?? []).filter((pg) => pg._id !== pId);
|
|
958
|
+
pages.push(pageData);
|
|
959
|
+
await target.put("JournalEntry", {
|
|
960
|
+
...existing,
|
|
961
|
+
name: entryName,
|
|
962
|
+
folder: folderFId,
|
|
963
|
+
pages,
|
|
964
|
+
flags
|
|
965
|
+
});
|
|
678
966
|
return "modified";
|
|
679
967
|
}
|
|
680
|
-
|
|
968
|
+
return target.put("JournalEntry", {
|
|
681
969
|
_id: eId,
|
|
682
970
|
name: entryName,
|
|
683
971
|
folder: folderFId,
|
|
684
972
|
pages: [pageData],
|
|
685
973
|
flags,
|
|
686
|
-
// Bootstrap the entry visible at the page's tier so player-visible
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
// via reconcile.
|
|
974
|
+
// Bootstrap the entry visible at the page's tier so player-visible pages
|
|
975
|
+
// aren't hidden in the gap before reconcileEntries runs. Mixed-tier
|
|
976
|
+
// folders converge to max(pages) there.
|
|
690
977
|
...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
|
|
691
|
-
}
|
|
692
|
-
return "added";
|
|
978
|
+
});
|
|
693
979
|
}
|
|
694
980
|
function pageOwnershipLevelFor(vault, pageRole) {
|
|
695
|
-
if (!vault.
|
|
696
|
-
const
|
|
697
|
-
if (
|
|
981
|
+
if (!vault.playerRole || !vault.knownRoles?.length) return null;
|
|
982
|
+
const playerIdx = vault.knownRoles.indexOf(vault.playerRole);
|
|
983
|
+
if (playerIdx < 0) return null;
|
|
698
984
|
const pageIdx = pageRole ? vault.knownRoles.indexOf(pageRole) : -1;
|
|
699
985
|
const effectiveIdx = pageIdx < 0 ? 0 : pageIdx;
|
|
700
|
-
return effectiveIdx
|
|
986
|
+
return effectiveIdx <= playerIdx ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER : CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
|
|
701
987
|
}
|
|
702
|
-
async function
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
(j) => j.getFlag(MODULE_ID, "vaultId") === vault.id
|
|
706
|
-
);
|
|
988
|
+
async function reconcileEntries(target, vault, folderInfo, bodyMetaIndex) {
|
|
989
|
+
const ours = (await target.contents("JournalEntry")).filter((j) => j.flags?.[MODULE_ID]?.vaultId === vault.id);
|
|
990
|
+
const gated = !!vault.playerRole && !!vault.knownRoles?.length;
|
|
707
991
|
for (const entry of ours) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const bodyPath = pPath.replace(/\.md$/i, ".body.html");
|
|
713
|
-
const meta = bodyMetaIndex.get(bodyPath);
|
|
714
|
-
if (!meta) continue;
|
|
715
|
-
const level = pageOwnershipLevelFor(vault, meta.role);
|
|
716
|
-
if (level === null) continue;
|
|
717
|
-
if (page.ownership?.default !== level) {
|
|
718
|
-
try {
|
|
719
|
-
await page.update({ ownership: { default: level } });
|
|
720
|
-
} catch (err) {
|
|
721
|
-
console.warn(`Vaults | reconcile ownership ${pPath} \u2192 ${level} failed:`, err);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
if (entryMax === null || level > entryMax) entryMax = level;
|
|
725
|
-
}
|
|
726
|
-
if (entryMax !== null && entry.ownership?.default !== entryMax) {
|
|
992
|
+
const patched = reconcileOwnershipData(gated ? vault : null, entry, bodyMetaIndex);
|
|
993
|
+
const folder = await expectedFolder(target, vault, entry, folderInfo);
|
|
994
|
+
if (folder !== void 0 && folder !== (patched.folder ?? null)) patched.folder = folder;
|
|
995
|
+
if (JSON.stringify(patched) !== JSON.stringify(entry)) {
|
|
727
996
|
try {
|
|
728
|
-
await
|
|
997
|
+
await target.put("JournalEntry", patched);
|
|
729
998
|
} catch (err) {
|
|
730
|
-
console.warn(`Vaults | reconcile
|
|
999
|
+
console.warn(`Vaults | reconcile failed for ${entry.name}:`, err);
|
|
731
1000
|
}
|
|
732
1001
|
}
|
|
733
1002
|
}
|
|
734
1003
|
}
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
for (const
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1004
|
+
function reconcileOwnershipData(vault, entry, bodyMetaIndex) {
|
|
1005
|
+
const patched = structuredClone(entry);
|
|
1006
|
+
if (!vault) return patched;
|
|
1007
|
+
let entryMax = null;
|
|
1008
|
+
for (const page of patched.pages ?? []) {
|
|
1009
|
+
const pPath = page.flags?.[MODULE_ID]?.path;
|
|
1010
|
+
if (!pPath) continue;
|
|
1011
|
+
const meta = bodyMetaIndex.get(pPath.replace(/\.md$/i, ".body.html"));
|
|
1012
|
+
if (!meta) continue;
|
|
1013
|
+
const level = pageOwnershipLevelFor(vault, meta.role);
|
|
1014
|
+
if (level === null) continue;
|
|
1015
|
+
page.ownership = { ...page.ownership, default: level };
|
|
1016
|
+
if (entryMax === null || level > entryMax) entryMax = level;
|
|
1017
|
+
}
|
|
1018
|
+
if (entryMax !== null) patched.ownership = { ...patched.ownership, default: entryMax };
|
|
1019
|
+
return patched;
|
|
1020
|
+
}
|
|
1021
|
+
async function expectedFolder(target, vault, entry, folderInfo) {
|
|
1022
|
+
const path = entry.pages?.[0]?.flags?.[MODULE_ID]?.path;
|
|
1023
|
+
if (!path) return void 0;
|
|
1024
|
+
const segs = path.split("/");
|
|
1025
|
+
segs.pop();
|
|
1026
|
+
const folderPath = segs.join("/");
|
|
1027
|
+
const fInfo = folderInfo?.get(folderPath);
|
|
1028
|
+
const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
|
|
1029
|
+
return ensureFolderChain(target, vault, leaf ? segs.slice(0, -1) : segs);
|
|
758
1030
|
}
|
|
759
|
-
async function deleteFile(vault, path) {
|
|
1031
|
+
async function deleteFile(target, vault, path) {
|
|
760
1032
|
const eId = await entryId(vault.id, path);
|
|
761
1033
|
const pId = await pageId(vault.id, path);
|
|
762
|
-
const entry =
|
|
1034
|
+
const entry = await target.get("JournalEntry", eId);
|
|
763
1035
|
if (!entry) return;
|
|
764
|
-
const
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
async function deleteVaultJournals(vaultId) {
|
|
769
|
-
const journals = game.journal.contents.filter((j) => j.getFlag(MODULE_ID, "vaultId") === vaultId);
|
|
770
|
-
for (const j of journals) await j.delete();
|
|
771
|
-
const folders = game.folders.contents.filter((f) => f.type === "JournalEntry");
|
|
772
|
-
for (const f of folders.reverse()) {
|
|
773
|
-
if (f.contents.length === 0 && f.children.length === 0) {
|
|
774
|
-
const root = await folderId(vaultId, `${vaultId}/__root__/${f.name}`);
|
|
775
|
-
if (f.id === root) await f.delete();
|
|
776
|
-
}
|
|
777
|
-
}
|
|
1036
|
+
const pages = (entry.pages ?? []).filter((pg) => pg._id !== pId);
|
|
1037
|
+
if (pages.length === 0) await target.remove("JournalEntry", eId);
|
|
1038
|
+
else await target.put("JournalEntry", { ...entry, pages });
|
|
778
1039
|
}
|
|
779
1040
|
async function appendInstanceDocLink(html, vault, path, meta) {
|
|
780
1041
|
const base = meta?.foundry?.base;
|
|
781
|
-
const docName =
|
|
1042
|
+
const docName = docNameFromBase(base);
|
|
782
1043
|
if (!docName) return html;
|
|
783
1044
|
const idOverride = meta?.foundry?.id;
|
|
784
1045
|
const docId = typeof idOverride === "string" && idOverride ? idOverride : await instanceId(vault.id, path);
|
|
785
1046
|
const label = meta?.title || path.split("/").pop().replace(/\.md$/i, "");
|
|
1047
|
+
const uuid = instanceUuid(vault, docName, docId);
|
|
786
1048
|
return html + `
|
|
787
|
-
<p class="vaults-instance-link"><em>Foundry document:</em> @UUID[${
|
|
1049
|
+
<p class="vaults-instance-link"><em>Foundry document:</em> @UUID[${uuid}]{${escapeBraces(label)}}</p>`;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// ../foundry/scripts/moulinette.mjs
|
|
1053
|
+
var MOULINETTE_PREFIX = "@moulinette/";
|
|
1054
|
+
var CACHED_COLLECTION = "mou-cloud-cached";
|
|
1055
|
+
function parseMoulinetteRef(s) {
|
|
1056
|
+
if (typeof s !== "string" || !s.startsWith(MOULINETTE_PREFIX)) return null;
|
|
1057
|
+
const [pack, ...fileParts] = s.slice(MOULINETTE_PREFIX.length).split("/");
|
|
1058
|
+
const file = fileParts.join("/");
|
|
1059
|
+
if (!pack || !file) return null;
|
|
1060
|
+
return { pack, file };
|
|
1061
|
+
}
|
|
1062
|
+
async function loadIndex(log) {
|
|
1063
|
+
const mod = game.modules?.get("moulinette");
|
|
1064
|
+
if (!mod) {
|
|
1065
|
+
log("references need the Moulinette module, which is not installed");
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
if (!mod.active) {
|
|
1069
|
+
log("references need the Moulinette module, which is installed but not enabled in this world");
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
const collection = mod.collections?.find((c) => c.getId?.() === CACHED_COLLECTION);
|
|
1073
|
+
if (!collection?.initialize || !collection.selectAsset || !collection.downloadAsset) {
|
|
1074
|
+
log("Moulinette is installed but its asset index is not where we expect; skipping");
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
await collection.initialize();
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
log(`could not load the Moulinette index: ${err?.message ?? err}`);
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
const assets = mod.cache?.allAssets;
|
|
1084
|
+
if (!Array.isArray(assets)) {
|
|
1085
|
+
log("Moulinette's asset index is not a list; skipping");
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
return { mod, collection, assets };
|
|
1089
|
+
}
|
|
1090
|
+
function findAsset(ref, index, log) {
|
|
1091
|
+
const match = index.assets.find(
|
|
1092
|
+
(a) => String(a?.pack_id) === ref.pack && a?.url === ref.file
|
|
1093
|
+
);
|
|
1094
|
+
if (!match) {
|
|
1095
|
+
log(`no asset ${ref.file} in pack ${ref.pack} \u2014 not subscribed, or it moved`);
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
return match;
|
|
1099
|
+
}
|
|
1100
|
+
async function resolveMoulinetteDocument(spec, warn) {
|
|
1101
|
+
const log = (msg) => warn(msg);
|
|
1102
|
+
const ref = parseMoulinetteRef(MOULINETTE_PREFIX + spec);
|
|
1103
|
+
if (!ref) {
|
|
1104
|
+
log(`malformed reference '${spec}' \u2014 expected <pack_ref>/<filepath>`);
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
const index = await loadIndex(log);
|
|
1108
|
+
if (!index) return null;
|
|
1109
|
+
const asset = findAsset(ref, index, log);
|
|
1110
|
+
if (!asset) return null;
|
|
1111
|
+
try {
|
|
1112
|
+
const descriptor = await index.mod.cloudclient.apiGET(`/asset/${asset.id}`, {
|
|
1113
|
+
session: index.mod.getSessionId()
|
|
1114
|
+
});
|
|
1115
|
+
const dl = await index.collection.downloadAsset(descriptor);
|
|
1116
|
+
if (!dl?.message) {
|
|
1117
|
+
log(`${ref.pack}/${ref.file} is not a document; foundry.base needs one`);
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
return JSON.parse(dl.message);
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
log(`could not read ${ref.pack}/${ref.file}: ${err?.message ?? err}`);
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async function resolveOne(ref, index, log) {
|
|
1127
|
+
const match = findAsset(ref, index, log);
|
|
1128
|
+
if (!match) return null;
|
|
1129
|
+
try {
|
|
1130
|
+
note(`Moulinette: ${ref.file.split("/").pop()}`);
|
|
1131
|
+
const path = await index.collection.selectAsset(match);
|
|
1132
|
+
if (!path) {
|
|
1133
|
+
log(`${ref.pack}/${ref.file} is not a media asset; only files can be referenced`);
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
return path;
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
log(`download failed for ${ref.pack}/${ref.file}: ${err?.message ?? err}`);
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async function resolveMoulinetteRefs(value, warn) {
|
|
1143
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1144
|
+
const stats = { resolved: 0, unresolved: 0 };
|
|
1145
|
+
const seenWarnings = /* @__PURE__ */ new Set();
|
|
1146
|
+
const log = (msg) => {
|
|
1147
|
+
if (seenWarnings.has(msg)) return;
|
|
1148
|
+
seenWarnings.add(msg);
|
|
1149
|
+
warn(msg);
|
|
1150
|
+
};
|
|
1151
|
+
let index;
|
|
1152
|
+
const lookup = async (s) => {
|
|
1153
|
+
if (cache.has(s)) return cache.get(s);
|
|
1154
|
+
const ref = parseMoulinetteRef(s);
|
|
1155
|
+
let path = null;
|
|
1156
|
+
if (!ref) {
|
|
1157
|
+
log(`malformed reference '${s}' \u2014 expected @moulinette/<pack_ref>/<filepath>`);
|
|
1158
|
+
} else {
|
|
1159
|
+
if (index === void 0) index = await loadIndex(log);
|
|
1160
|
+
if (index) path = await resolveOne(ref, index, log);
|
|
1161
|
+
}
|
|
1162
|
+
cache.set(s, path);
|
|
1163
|
+
if (path) stats.resolved++;
|
|
1164
|
+
else stats.unresolved++;
|
|
1165
|
+
return path;
|
|
1166
|
+
};
|
|
1167
|
+
const walk = async (node) => {
|
|
1168
|
+
if (Array.isArray(node)) {
|
|
1169
|
+
const kept = [];
|
|
1170
|
+
for (const item of node) {
|
|
1171
|
+
if (typeof item === "string" && item.startsWith(MOULINETTE_PREFIX)) {
|
|
1172
|
+
const path = await lookup(item);
|
|
1173
|
+
if (path) kept.push(path);
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (await walk(item)) kept.push(item);
|
|
1177
|
+
}
|
|
1178
|
+
node.length = 0;
|
|
1179
|
+
node.push(...kept);
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
if (node && typeof node === "object") {
|
|
1183
|
+
let viable = true;
|
|
1184
|
+
for (const key of Object.keys(node)) {
|
|
1185
|
+
const v = node[key];
|
|
1186
|
+
if (typeof v === "string" && v.startsWith(MOULINETTE_PREFIX)) {
|
|
1187
|
+
const path = await lookup(v);
|
|
1188
|
+
if (path) node[key] = path;
|
|
1189
|
+
else {
|
|
1190
|
+
delete node[key];
|
|
1191
|
+
viable = false;
|
|
1192
|
+
}
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (!await walk(v)) delete node[key];
|
|
1196
|
+
}
|
|
1197
|
+
return viable;
|
|
1198
|
+
}
|
|
1199
|
+
return true;
|
|
1200
|
+
};
|
|
1201
|
+
await walk(value);
|
|
1202
|
+
return stats;
|
|
788
1203
|
}
|
|
789
1204
|
|
|
790
1205
|
// ../foundry/scripts/instance.mjs
|
|
1206
|
+
var CORE_DESCRIPTION_FIELDS = {
|
|
1207
|
+
RollTable: "description",
|
|
1208
|
+
Playlist: "description"
|
|
1209
|
+
};
|
|
1210
|
+
function descriptionPathFor(docName, systemId) {
|
|
1211
|
+
return CORE_DESCRIPTION_FIELDS[docName] ?? DESCRIPTION_FIELDS[systemId]?.[docName];
|
|
1212
|
+
}
|
|
791
1213
|
var DESCRIPTION_FIELDS = {
|
|
792
1214
|
dnd5e: {
|
|
793
1215
|
Actor: "system.details.biography.value",
|
|
794
1216
|
Item: "system.description.value"
|
|
795
1217
|
}
|
|
796
1218
|
};
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
"
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1219
|
+
function sceneBackgroundSrc(data) {
|
|
1220
|
+
const levels = Array.isArray(data?.levels) ? data.levels : [];
|
|
1221
|
+
const initial = levels.find((l) => l._id === data.initialLevel) ?? levels[0];
|
|
1222
|
+
return initial?.background?.src || data?.background?.src || null;
|
|
1223
|
+
}
|
|
1224
|
+
async function attachSceneThumb(data, docName, vaultPath) {
|
|
1225
|
+
if (docName !== "Scene" || data.thumb) return;
|
|
1226
|
+
const src = sceneBackgroundSrc(data);
|
|
1227
|
+
if (!src) return;
|
|
1228
|
+
try {
|
|
1229
|
+
const { thumb } = await foundry.helpers.media.ImageHelper.createThumbnail(
|
|
1230
|
+
src,
|
|
1231
|
+
{ width: 300, height: 100 }
|
|
1232
|
+
);
|
|
1233
|
+
if (thumb) data.thumb = thumb;
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
console.warn(`Vaults | could not make a scene thumbnail for ${vaultPath}:`, err);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function wantsJournalNote(docName, fm) {
|
|
1239
|
+
return docName === "Scene" && fm?.journal !== false;
|
|
1240
|
+
}
|
|
1241
|
+
async function applyInstance(target, vault, vaultPath, meta, { forceFull = false } = {}) {
|
|
819
1242
|
const fm = meta?.foundry;
|
|
820
|
-
if (!fm || typeof fm !== "object") return;
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
} else {
|
|
846
|
-
docName = parsed.docName;
|
|
847
|
-
baseData = parsed.subtype ? { type: parsed.subtype } : {};
|
|
848
|
-
}
|
|
849
|
-
const collection = COLLECTION_FOR[docName]?.();
|
|
850
|
-
if (!collection) {
|
|
851
|
-
console.warn(`Vaults | foundry.base: no world collection for ${docName}; skipping ${vaultPath}.`);
|
|
852
|
-
return;
|
|
1243
|
+
if (!fm || typeof fm !== "object") return null;
|
|
1244
|
+
if (fm.base === void 0 || fm.base === null) return null;
|
|
1245
|
+
const specs = Array.isArray(fm.base) ? fm.base : [fm.base];
|
|
1246
|
+
const candidates = specs.map(parseFoundryBase);
|
|
1247
|
+
if (candidates.length === 0 || candidates.some((c) => !c)) {
|
|
1248
|
+
console.warn(
|
|
1249
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 unrecognised base ${JSON.stringify(fm.base)}; expected a UUID ("Compendium.<pkg>.<pack>.Actor.<id>") or a type ("Actor:npc"), or a list of those. Skipping.`
|
|
1250
|
+
);
|
|
1251
|
+
return { ok: false, reason: "unparseable" };
|
|
1252
|
+
}
|
|
1253
|
+
const docNames = new Set(candidates.map(docNameOf).filter(Boolean));
|
|
1254
|
+
const docName = [...docNames][0] ?? null;
|
|
1255
|
+
if (!docName) {
|
|
1256
|
+
console.warn(`Vaults | foundry.base: ${vaultPath} \u2192 could not read a document type from ${JSON.stringify(fm.base)}. Skipping.`);
|
|
1257
|
+
return { ok: false, reason: "unparseable" };
|
|
1258
|
+
}
|
|
1259
|
+
if (docNames.size > 1) {
|
|
1260
|
+
console.warn(
|
|
1261
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 every entry must name the same document type, got ${[...docNames].join(", ")}. Skipping.`
|
|
1262
|
+
);
|
|
1263
|
+
return { ok: false, reason: "mixed-types" };
|
|
1264
|
+
}
|
|
1265
|
+
if (!CONFIG[docName]) {
|
|
1266
|
+
console.warn(`Vaults | foundry.base: this world has no ${docName} documents; skipping ${vaultPath}.`);
|
|
1267
|
+
return { ok: false, reason: "no-collection" };
|
|
853
1268
|
}
|
|
854
|
-
const docClass = CONFIG[docName].documentClass;
|
|
855
1269
|
const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, vaultPath);
|
|
856
1270
|
const dataJson = fm.data_json && typeof fm.data_json === "object" && !Array.isArray(fm.data_json) ? rewriteVaultPaths(structuredClone(fm.data_json), vault.id) : null;
|
|
857
|
-
|
|
1271
|
+
const moulinetteWarn = (msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`);
|
|
1272
|
+
if (dataJson) await resolveMoulinetteRefs(dataJson, moulinetteWarn);
|
|
1273
|
+
if (dataJson) {
|
|
1274
|
+
await resolveItemUuids(dataJson, vaultPath);
|
|
1275
|
+
await ensureEmbeddedIds(dataJson, vault.id, vaultPath);
|
|
1276
|
+
}
|
|
858
1277
|
const derived = {};
|
|
859
|
-
const overlay = await buildOverlay(vault, vaultPath, meta, docName, derived);
|
|
1278
|
+
const overlay = await buildOverlay(target, vault, vaultPath, meta, docName, derived);
|
|
860
1279
|
const tokenFloor = derived.tokenTexture && !dataJson?.prototypeToken?.texture?.src && !fm?.data?.prototypeToken?.texture?.src ? { prototypeToken: { texture: { src: derived.tokenTexture } } } : null;
|
|
861
|
-
const existing =
|
|
1280
|
+
const existing = await target.get(docName, id);
|
|
862
1281
|
if (existing) {
|
|
863
1282
|
const base = tokenFloor ? deepMerge(structuredClone(tokenFloor), dataJson ?? {}) : dataJson;
|
|
864
1283
|
const updatePatch = base ? deepMerge(structuredClone(base), overlay) : overlay;
|
|
1284
|
+
if (wantsJournalNote(docName, fm)) {
|
|
1285
|
+
await attachJournalNote(updatePatch, {
|
|
1286
|
+
width: updatePatch.width ?? existing.width,
|
|
1287
|
+
height: updatePatch.height ?? existing.height,
|
|
1288
|
+
padding: updatePatch.padding ?? existing.padding,
|
|
1289
|
+
grid: updatePatch.grid ?? { size: existing.grid?.size }
|
|
1290
|
+
}, vault, vaultPath, meta);
|
|
1291
|
+
}
|
|
1292
|
+
if (!forceFull) delete updatePatch.folder;
|
|
865
1293
|
try {
|
|
866
|
-
|
|
1294
|
+
const merged = deepMerge(structuredClone(existing), updatePatch);
|
|
1295
|
+
if (sceneBackgroundSrc(merged) !== sceneBackgroundSrc(existing) || !existing.thumb) {
|
|
1296
|
+
await attachSceneThumb(merged, docName, vaultPath);
|
|
1297
|
+
}
|
|
1298
|
+
await target.put(docName, merged);
|
|
867
1299
|
} catch (err) {
|
|
868
1300
|
console.warn(`Vaults | foundry.base update failed for ${vaultPath}:`, err);
|
|
1301
|
+
return { ok: false, reason: "update-failed" };
|
|
869
1302
|
}
|
|
870
|
-
return;
|
|
1303
|
+
return { ok: true, action: "updated" };
|
|
871
1304
|
}
|
|
1305
|
+
const resolved = await resolveBase(candidates, vaultPath);
|
|
1306
|
+
if (!resolved) return { ok: false, reason: "unresolved" };
|
|
1307
|
+
const baseData = resolved.data;
|
|
1308
|
+
const baseItems = Array.isArray(baseData.items) ? baseData.items : null;
|
|
872
1309
|
if (tokenFloor) deepMerge(baseData, tokenFloor);
|
|
873
1310
|
if (dataJson) deepMerge(baseData, dataJson);
|
|
874
1311
|
baseData._id = id;
|
|
875
1312
|
deepMerge(baseData, overlay);
|
|
1313
|
+
if (wantsJournalNote(docName, fm)) {
|
|
1314
|
+
await attachJournalNote(baseData, baseData, vault, vaultPath, meta);
|
|
1315
|
+
}
|
|
1316
|
+
if (baseItems && baseData.items !== baseItems) {
|
|
1317
|
+
baseData.items = mergeItemsById(baseItems, baseData.items);
|
|
1318
|
+
}
|
|
876
1319
|
try {
|
|
877
|
-
await
|
|
1320
|
+
await attachSceneThumb(baseData, docName, vaultPath);
|
|
1321
|
+
await target.put(docName, baseData);
|
|
878
1322
|
} catch (err) {
|
|
879
1323
|
console.warn(`Vaults | foundry.base create failed for ${vaultPath}:`, err);
|
|
880
|
-
return;
|
|
1324
|
+
return { ok: false, reason: "create-failed" };
|
|
881
1325
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1326
|
+
const created = await target.get(docName, id);
|
|
1327
|
+
if (!created) {
|
|
1328
|
+
console.warn(
|
|
1329
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 ${docName} ${id} was rejected on create; no document exists. Foundry logged the validation error separately (look for "DataModelValidationError" above).`
|
|
1330
|
+
);
|
|
1331
|
+
return { ok: false, reason: "create-rejected" };
|
|
1332
|
+
}
|
|
1333
|
+
if (docName === "Scene" && (!created.thumb || resolved.from)) {
|
|
1334
|
+
try {
|
|
1335
|
+
const { thumb } = await created.createThumbnail();
|
|
1336
|
+
if (thumb) await created.update({ thumb });
|
|
1337
|
+
else console.warn(`Vaults | scene thumbnail came back empty for ${vaultPath}`);
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
console.warn(`Vaults | scene thumbnail generation failed for ${vaultPath}:`, err);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return { ok: true, action: "created", skew: resolved.skew ?? null };
|
|
1343
|
+
}
|
|
1344
|
+
function generationSkew(data, ref) {
|
|
1345
|
+
const exported = Number.parseInt(String(data?._stats?.coreVersion ?? ""), 10);
|
|
1346
|
+
const world = Number(game.release?.generation);
|
|
1347
|
+
if (!Number.isFinite(exported) || !Number.isFinite(world) || exported === world) return null;
|
|
1348
|
+
return { ref, exported: data._stats.coreVersion, world: game.release.generation };
|
|
1349
|
+
}
|
|
1350
|
+
async function resolveBase(candidates, vaultPath) {
|
|
1351
|
+
const tried = [];
|
|
1352
|
+
for (const parsed of candidates) {
|
|
1353
|
+
if (parsed.kind === "blank") {
|
|
1354
|
+
if (tried.length > 0) {
|
|
1355
|
+
console.info(
|
|
1356
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 fell back to blank ${parsed.docName}${parsed.subtype ? `:${parsed.subtype}` : ""} after ${tried.length} earlier candidate(s) did not resolve.`
|
|
1357
|
+
);
|
|
890
1358
|
}
|
|
1359
|
+
return { data: parsed.subtype ? { type: parsed.subtype } : {}, from: null };
|
|
891
1360
|
}
|
|
1361
|
+
if (parsed.kind === "moulinette") {
|
|
1362
|
+
const data2 = await resolveMoulinetteDocument(
|
|
1363
|
+
parsed.ref,
|
|
1364
|
+
(msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`)
|
|
1365
|
+
);
|
|
1366
|
+
if (!data2) {
|
|
1367
|
+
tried.push(`@moulinette/${parsed.ref} \u2014 did not resolve`);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
const skew = generationSkew(data2, parsed.ref);
|
|
1371
|
+
delete data2._id;
|
|
1372
|
+
if (tried.length > 0) {
|
|
1373
|
+
console.info(
|
|
1374
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 using @moulinette/${parsed.ref}; earlier candidate(s) skipped:
|
|
1375
|
+
` + tried.join("\n ")
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
return { data: data2, from: `@moulinette/${parsed.ref}`, skew };
|
|
1379
|
+
}
|
|
1380
|
+
const template = await safeFromUuid(parsed.uuid);
|
|
1381
|
+
if (!template) {
|
|
1382
|
+
tried.push(`${parsed.uuid} \u2014 did not resolve`);
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (!BLANK_DOC_TYPES.includes(template.documentName)) {
|
|
1386
|
+
tried.push(`${parsed.uuid} \u2014 is a ${template.documentName}; vaults can instantiate ${BLANK_DOC_TYPES.join(", ")}`);
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
let data;
|
|
1390
|
+
try {
|
|
1391
|
+
data = template.toObject();
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
tried.push(`${parsed.uuid} \u2014 unreadable: ${err.message}`);
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
delete data._id;
|
|
1397
|
+
const sourceUuid = template.uuid ?? parsed.uuid;
|
|
1398
|
+
if (sourceUuid.startsWith("Compendium.")) {
|
|
1399
|
+
data._stats = { ...data._stats, compendiumSource: sourceUuid };
|
|
1400
|
+
}
|
|
1401
|
+
const via = sourceUuid === parsed.uuid ? "" : ` (redirected to ${sourceUuid})`;
|
|
1402
|
+
if (tried.length > 0) {
|
|
1403
|
+
console.info(
|
|
1404
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 using ${parsed.uuid}${via}; earlier candidate(s) skipped:
|
|
1405
|
+
` + tried.join("\n ")
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
return { data, from: sourceUuid };
|
|
892
1409
|
}
|
|
1410
|
+
console.warn(
|
|
1411
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 no candidate resolved:
|
|
1412
|
+
` + tried.join("\n ") + `
|
|
1413
|
+
Add a blank-document entry (e.g. "Actor:npc") as the last item so this can't fail.`
|
|
1414
|
+
);
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
async function missingBasePackages(metas) {
|
|
1418
|
+
const pageSpecs = [];
|
|
1419
|
+
for (const meta of metas) {
|
|
1420
|
+
const base = meta?.foundry?.base;
|
|
1421
|
+
if (base === void 0 || base === null) continue;
|
|
1422
|
+
const specs = (Array.isArray(base) ? base : [base]).filter((s) => typeof s === "string");
|
|
1423
|
+
if (specs.length === 0) continue;
|
|
1424
|
+
if (specs.some((s) => !s.includes("."))) continue;
|
|
1425
|
+
if (specs.some((s) => !s.startsWith("Compendium."))) continue;
|
|
1426
|
+
if (specs.some((s) => s.split(".")[1] === "world")) continue;
|
|
1427
|
+
pageSpecs.push(specs);
|
|
1428
|
+
}
|
|
1429
|
+
if (pageSpecs.length === 0) return /* @__PURE__ */ new Map();
|
|
1430
|
+
const PROBES_PER_PACK = 3;
|
|
1431
|
+
const packOf = (spec) => spec.split(".").slice(1, 3).join(".");
|
|
1432
|
+
const probes = /* @__PURE__ */ new Map();
|
|
1433
|
+
for (const specs of pageSpecs) {
|
|
1434
|
+
for (const spec of specs) {
|
|
1435
|
+
const pack = packOf(spec);
|
|
1436
|
+
const chosen = probes.get(pack) ?? [];
|
|
1437
|
+
if (chosen.length < PROBES_PER_PACK && !chosen.includes(spec)) chosen.push(spec);
|
|
1438
|
+
probes.set(pack, chosen);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const reachable = /* @__PURE__ */ new Map();
|
|
1442
|
+
for (const [pack, specs] of probes) {
|
|
1443
|
+
let ok = false;
|
|
1444
|
+
for (const spec of specs) if (await safeFromUuid(spec)) {
|
|
1445
|
+
ok = true;
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
reachable.set(pack, ok);
|
|
1449
|
+
}
|
|
1450
|
+
const missing = /* @__PURE__ */ new Map();
|
|
1451
|
+
for (const specs of pageSpecs) {
|
|
1452
|
+
if (specs.some((s) => reachable.get(packOf(s)))) continue;
|
|
1453
|
+
for (const pkg of new Set(specs.map((s) => s.split(".")[1]).filter(Boolean))) {
|
|
1454
|
+
missing.set(pkg, (missing.get(pkg) ?? 0) + 1);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return missing;
|
|
893
1458
|
}
|
|
894
|
-
function
|
|
895
|
-
if (typeof spec !== "string" || !spec) return null;
|
|
896
|
-
if (spec.includes(".")) return { kind: "uuid", uuid: spec };
|
|
897
|
-
const [typeRaw, subtype] = spec.split(":");
|
|
898
|
-
const docName = [...BLANK_DOC_TYPES].find((t) => t.toLowerCase() === typeRaw.toLowerCase());
|
|
899
|
-
if (!docName) return null;
|
|
900
|
-
return { kind: "blank", docName, subtype: subtype || void 0 };
|
|
901
|
-
}
|
|
902
|
-
async function deleteInstance(vault, vaultPath) {
|
|
1459
|
+
async function deleteInstance(target, vault, vaultPath) {
|
|
903
1460
|
const id = await instanceId(vault.id, vaultPath);
|
|
904
|
-
for (const
|
|
905
|
-
const
|
|
906
|
-
const doc = collection?.get(id);
|
|
1461
|
+
for (const docName of BLANK_DOC_TYPES) {
|
|
1462
|
+
const doc = await target.get(docName, id);
|
|
907
1463
|
if (!doc) continue;
|
|
908
|
-
if (doc.
|
|
1464
|
+
if (doc.flags?.[MODULE_ID]?.vaultId !== vault.id) continue;
|
|
909
1465
|
try {
|
|
910
|
-
await
|
|
1466
|
+
await target.remove(docName, id);
|
|
911
1467
|
} catch (err) {
|
|
912
|
-
console.warn(`Vaults | failed to delete ${
|
|
1468
|
+
console.warn(`Vaults | failed to delete ${docName} for ${vaultPath}:`, err);
|
|
913
1469
|
}
|
|
914
1470
|
}
|
|
915
1471
|
}
|
|
916
|
-
async function
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
try {
|
|
923
|
-
await doc.delete();
|
|
924
|
-
} catch (err) {
|
|
925
|
-
console.warn(`Vaults | failed to delete ${docName} ${doc.id}:`, err);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
for (const docName of BLANK_DOC_TYPES) {
|
|
930
|
-
const fId = await instanceFolderId(vaultId, docName);
|
|
931
|
-
const folder = game.folders.get(fId);
|
|
932
|
-
if (!folder || folder.type !== docName) continue;
|
|
933
|
-
if (folder.contents.length > 0 || folder.children.length > 0) continue;
|
|
1472
|
+
async function ensureInstanceFolder(target, vault, docName, subPath = "") {
|
|
1473
|
+
let parentId = null;
|
|
1474
|
+
let key = `${vault.id}/__instance__/${docName}`;
|
|
1475
|
+
for (const segment of splitFolderPath(subPath)) {
|
|
1476
|
+
key += `/${segment}`;
|
|
1477
|
+
const fId = await folderId(vault.id, key);
|
|
934
1478
|
try {
|
|
935
|
-
await
|
|
1479
|
+
await target.putFolder(docName, { _id: fId, name: segment, folder: parentId });
|
|
936
1480
|
} catch (err) {
|
|
937
|
-
console.warn(`Vaults |
|
|
1481
|
+
console.warn(`Vaults | could not create ${docName} folder for ${vault.label}:`, err);
|
|
1482
|
+
return parentId;
|
|
938
1483
|
}
|
|
1484
|
+
parentId = fId;
|
|
939
1485
|
}
|
|
1486
|
+
return parentId;
|
|
940
1487
|
}
|
|
941
|
-
|
|
942
|
-
return
|
|
1488
|
+
function splitFolderPath(subPath) {
|
|
1489
|
+
return typeof subPath === "string" ? subPath.split("/").map((s) => s.trim()).filter(Boolean) : [];
|
|
943
1490
|
}
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if (existing) {
|
|
949
|
-
if (existing.name !== name) {
|
|
950
|
-
try {
|
|
951
|
-
await existing.update({ name });
|
|
952
|
-
} catch (err) {
|
|
953
|
-
console.warn(`Vaults | could not rename ${docName} folder for ${vault.label}:`, err);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
return fId;
|
|
957
|
-
}
|
|
958
|
-
try {
|
|
959
|
-
await Folder.create({ _id: fId, name, type: docName, folder: null }, { keepId: true });
|
|
960
|
-
return fId;
|
|
961
|
-
} catch (err) {
|
|
962
|
-
console.warn(`Vaults | could not create ${docName} folder for ${vault.label}:`, err);
|
|
963
|
-
return null;
|
|
964
|
-
}
|
|
1491
|
+
function instanceSubPath(vaultPath, meta) {
|
|
1492
|
+
const override = meta?.foundry?.folder;
|
|
1493
|
+
if (typeof override === "string" && override.trim()) return override;
|
|
1494
|
+
return folderOfPath(vaultPath);
|
|
965
1495
|
}
|
|
966
|
-
async function buildOverlay(vault, vaultPath, meta, docName, derived = {}) {
|
|
1496
|
+
async function buildOverlay(target, vault, vaultPath, meta, docName, derived = {}) {
|
|
967
1497
|
const overlay = {
|
|
968
1498
|
// Prefer the page's frontmatter `title:` over the filename — the wiki
|
|
969
1499
|
// already treats title as the page's display name, and a doc named
|
|
970
1500
|
// "Potion of Healing (Mossfoot Brew)" reads better in the Foundry
|
|
971
1501
|
// sidebar than "Healing Potion".
|
|
972
1502
|
name: meta.title || baseName(vaultPath),
|
|
973
|
-
folder: await ensureInstanceFolder(vault, docName),
|
|
1503
|
+
folder: await ensureInstanceFolder(target, vault, docName, instanceSubPath(vaultPath, meta)),
|
|
974
1504
|
flags: { [MODULE_ID]: { vaultId: vault.id, path: vaultPath } }
|
|
975
1505
|
};
|
|
976
1506
|
if (meta.image) {
|
|
@@ -981,42 +1511,45 @@ async function buildOverlay(vault, vaultPath, meta, docName, derived = {}) {
|
|
|
981
1511
|
}
|
|
982
1512
|
}
|
|
983
1513
|
const fm = meta?.foundry;
|
|
984
|
-
const descPath =
|
|
1514
|
+
const descPath = descriptionPathFor(docName, game.system.id);
|
|
985
1515
|
const embedAuto = fm?.embed !== false && fm?.journal !== false;
|
|
986
1516
|
if (descPath && embedAuto) {
|
|
987
1517
|
const eId = await entryId(vault.id, vaultPath);
|
|
988
1518
|
const pId = await pageId(vault.id, vaultPath);
|
|
989
|
-
setPath(overlay, descPath, `<p>@Embed[
|
|
1519
|
+
setPath(overlay, descPath, `<p>@Embed[${journalPageUuid(vault, eId, pId)} inline]</p>`);
|
|
990
1520
|
}
|
|
991
1521
|
if (fm?.data && typeof fm.data === "object") {
|
|
992
1522
|
const cloned = rewriteVaultPaths(structuredClone(fm.data), vault.id);
|
|
1523
|
+
await resolveMoulinetteRefs(cloned, (msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`));
|
|
1524
|
+
await resolveItemUuids(cloned, vaultPath);
|
|
993
1525
|
await ensureEmbeddedIds(cloned, vault.id, vaultPath);
|
|
994
1526
|
deepMerge(overlay, cloned);
|
|
995
1527
|
}
|
|
996
|
-
if (docName === "Scene") {
|
|
997
|
-
const note = await buildJournalNote(vault, vaultPath, meta);
|
|
998
|
-
if (note) overlay.notes = [...overlay.notes ?? [], note];
|
|
999
|
-
}
|
|
1000
1528
|
return overlay;
|
|
1001
1529
|
}
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1530
|
+
function notePosition(geom) {
|
|
1531
|
+
const width = Number(geom.width) || 4e3;
|
|
1532
|
+
const height = Number(geom.height) || 3e3;
|
|
1533
|
+
const padding = Number(geom.padding ?? 0.25);
|
|
1534
|
+
const gridSize = Number(geom.grid?.size) || 100;
|
|
1535
|
+
return {
|
|
1536
|
+
x: gridSize * (Math.ceil(width / gridSize * padding) - 0.5),
|
|
1537
|
+
y: gridSize * (Math.ceil(height / gridSize * padding) + 0.5)
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
async function attachJournalNote(sceneData, geom, vault, vaultPath, meta) {
|
|
1541
|
+
const gridSize = Number(geom.grid?.size) || 100;
|
|
1542
|
+
const { x, y } = notePosition(geom);
|
|
1010
1543
|
const eId = await entryId(vault.id, vaultPath);
|
|
1011
1544
|
const idOverride = meta?.foundry?.id;
|
|
1012
1545
|
const pId = typeof idOverride === "string" && idOverride ? idOverride : await pageId(vault.id, vaultPath);
|
|
1013
|
-
|
|
1546
|
+
const note2 = {
|
|
1014
1547
|
_id: await subdocId(vault.id, vaultPath, "/notes/__journalLink__"),
|
|
1015
1548
|
entryId: eId,
|
|
1016
1549
|
pageId: pId,
|
|
1017
|
-
x
|
|
1018
|
-
y
|
|
1019
|
-
iconSize,
|
|
1550
|
+
x,
|
|
1551
|
+
y,
|
|
1552
|
+
iconSize: gridSize,
|
|
1020
1553
|
texture: {
|
|
1021
1554
|
src: "icons/svg/book.svg",
|
|
1022
1555
|
anchorX: 0.5,
|
|
@@ -1026,8 +1559,48 @@ async function buildJournalNote(vault, vaultPath, meta) {
|
|
|
1026
1559
|
},
|
|
1027
1560
|
text: ""
|
|
1028
1561
|
};
|
|
1562
|
+
sceneData.notes = [...sceneData.notes ?? [], note2];
|
|
1029
1563
|
}
|
|
1030
1564
|
var VALID_SUBDOC_ID = /^[A-Za-z0-9]{16}$/;
|
|
1565
|
+
function mergeItemsById(baseItems, pageItems) {
|
|
1566
|
+
if (!Array.isArray(pageItems)) return baseItems;
|
|
1567
|
+
if (!baseItems.length) return pageItems;
|
|
1568
|
+
const merged = baseItems.map((item) => structuredClone(item));
|
|
1569
|
+
const positionOf = new Map(merged.map((item, i) => [item?._id, i]));
|
|
1570
|
+
for (const item of pageItems) {
|
|
1571
|
+
const at = item && item._id !== void 0 ? positionOf.get(item._id) : void 0;
|
|
1572
|
+
if (at === void 0) merged.push(item);
|
|
1573
|
+
else deepMerge(merged[at], item);
|
|
1574
|
+
}
|
|
1575
|
+
return merged;
|
|
1576
|
+
}
|
|
1577
|
+
async function resolveItemUuids(data, vaultPath) {
|
|
1578
|
+
if (!Array.isArray(data?.items)) return;
|
|
1579
|
+
const resolved = [];
|
|
1580
|
+
for (const entry of data.items) {
|
|
1581
|
+
if (!entry || typeof entry !== "object" || typeof entry.uuid !== "string") {
|
|
1582
|
+
resolved.push(entry);
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
const { uuid, ...overrides } = entry;
|
|
1586
|
+
const source = await safeFromUuid(uuid);
|
|
1587
|
+
if (!source) {
|
|
1588
|
+
console.warn(`Vaults | foundry item uuid: ${vaultPath} \u2192 ${uuid} did not resolve; skipping that item.`);
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
if (source.documentName !== "Item") {
|
|
1592
|
+
console.warn(`Vaults | foundry item uuid: ${vaultPath} \u2192 ${uuid} is a ${source.documentName}, not an Item; skipping that item.`);
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const itemData = source.toObject();
|
|
1596
|
+
delete itemData._id;
|
|
1597
|
+
if (uuid.startsWith("Compendium.")) {
|
|
1598
|
+
itemData._stats = { ...itemData._stats, compendiumSource: uuid };
|
|
1599
|
+
}
|
|
1600
|
+
resolved.push(deepMerge(itemData, overrides));
|
|
1601
|
+
}
|
|
1602
|
+
data.items = resolved;
|
|
1603
|
+
}
|
|
1031
1604
|
async function ensureEmbeddedIds(value, vaultId, pagePath, ptr = "") {
|
|
1032
1605
|
if (Array.isArray(value)) {
|
|
1033
1606
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -1101,6 +1674,37 @@ function deepMerge(target, source) {
|
|
|
1101
1674
|
}
|
|
1102
1675
|
return target;
|
|
1103
1676
|
}
|
|
1677
|
+
async function findMissingDocuments(target, vault, entries) {
|
|
1678
|
+
const out = [];
|
|
1679
|
+
const journalPages = /* @__PURE__ */ new Set();
|
|
1680
|
+
for (const entry of await target.contents("JournalEntry")) {
|
|
1681
|
+
for (const page of entry.pages ?? []) journalPages.add(`${entry._id}.${page._id}`);
|
|
1682
|
+
}
|
|
1683
|
+
const idsByType = /* @__PURE__ */ new Map();
|
|
1684
|
+
const packIds = async (docName) => {
|
|
1685
|
+
if (!idsByType.has(docName)) idsByType.set(docName, await target.ids(docName));
|
|
1686
|
+
return idsByType.get(docName);
|
|
1687
|
+
};
|
|
1688
|
+
for (const { logicalPath, meta } of entries) {
|
|
1689
|
+
const fm = meta?.foundry;
|
|
1690
|
+
const missing = [];
|
|
1691
|
+
if (fm?.journal !== false) {
|
|
1692
|
+
const eId = await entryId(vault.id, logicalPath);
|
|
1693
|
+
const pId = typeof fm?.id === "string" && fm.id ? fm.id : await pageId(vault.id, logicalPath);
|
|
1694
|
+
if (!journalPages.has(`${eId}.${pId}`)) missing.push("journal");
|
|
1695
|
+
}
|
|
1696
|
+
if (fm?.base !== void 0 && fm?.base !== null) {
|
|
1697
|
+
const specs = Array.isArray(fm.base) ? fm.base : [fm.base];
|
|
1698
|
+
const docName = docNameOf(parseFoundryBase(specs[0]));
|
|
1699
|
+
if (docName) {
|
|
1700
|
+
const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, logicalPath);
|
|
1701
|
+
if (!(await packIds(docName)).has(id)) missing.push("document");
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (missing.length > 0) out.push({ path: logicalPath, missing: missing.join(" + ") });
|
|
1705
|
+
}
|
|
1706
|
+
return out;
|
|
1707
|
+
}
|
|
1104
1708
|
|
|
1105
1709
|
// ../foundry/scripts/auth.mjs
|
|
1106
1710
|
function tokenInfo(token) {
|
|
@@ -1114,8 +1718,187 @@ function tokenInfo(token) {
|
|
|
1114
1718
|
};
|
|
1115
1719
|
}
|
|
1116
1720
|
|
|
1721
|
+
// ../foundry/scripts/target.mjs
|
|
1722
|
+
async function openTarget(vault) {
|
|
1723
|
+
return isAdventure(vault) ? openAdventure(vault) : openCompendium(vault);
|
|
1724
|
+
}
|
|
1725
|
+
async function openCompendium(vault) {
|
|
1726
|
+
return {
|
|
1727
|
+
vault,
|
|
1728
|
+
adventure: false,
|
|
1729
|
+
async get(docName, id) {
|
|
1730
|
+
const pack = await ensurePack(vault, docName);
|
|
1731
|
+
const doc = await pack.getDocument(id);
|
|
1732
|
+
return doc ? doc.toObject() : null;
|
|
1733
|
+
},
|
|
1734
|
+
async put(docName, data) {
|
|
1735
|
+
const pack = await ensurePack(vault, docName);
|
|
1736
|
+
const existing = await pack.getDocument(data._id);
|
|
1737
|
+
if (existing) {
|
|
1738
|
+
await existing.update(data);
|
|
1739
|
+
return "modified";
|
|
1740
|
+
}
|
|
1741
|
+
const cls = CONFIG[docName].documentClass;
|
|
1742
|
+
await cls.create(data, { pack: pack.collection, keepId: true, keepEmbeddedIds: true });
|
|
1743
|
+
if (!await pack.getDocument(data._id)) {
|
|
1744
|
+
throw new Error(`${docName} ${data._id} was rejected on create`);
|
|
1745
|
+
}
|
|
1746
|
+
return "added";
|
|
1747
|
+
},
|
|
1748
|
+
async remove(docName, id) {
|
|
1749
|
+
const pack = getPack(vault, docName);
|
|
1750
|
+
const doc = pack && await pack.getDocument(id);
|
|
1751
|
+
if (doc) await doc.delete();
|
|
1752
|
+
},
|
|
1753
|
+
async putFolder(docName, folder) {
|
|
1754
|
+
const pack = await ensurePack(vault, docName);
|
|
1755
|
+
const existing = pack.folders.get(folder._id);
|
|
1756
|
+
if (existing) {
|
|
1757
|
+
if (existing.name !== folder.name || (existing.folder?.id ?? null) !== (folder.folder ?? null)) {
|
|
1758
|
+
await existing.update({ name: folder.name, folder: folder.folder ?? null });
|
|
1759
|
+
}
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
await Folder.create(
|
|
1763
|
+
{ ...folder, type: docName },
|
|
1764
|
+
{ pack: pack.collection, keepId: true }
|
|
1765
|
+
);
|
|
1766
|
+
if (!pack.folders.get(folder._id)) {
|
|
1767
|
+
throw new Error(`Folder ${folder._id} ("${folder.name}") was rejected on create`);
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
/** Every document of a type this vault owns. One request, not one per id. */
|
|
1771
|
+
async contents(docName) {
|
|
1772
|
+
const pack = getPack(vault, docName);
|
|
1773
|
+
return pack ? (await pack.getDocuments()).map((d) => d.toObject()) : [];
|
|
1774
|
+
},
|
|
1775
|
+
async ids(docName) {
|
|
1776
|
+
const pack = getPack(vault, docName);
|
|
1777
|
+
return new Set(pack ? (await pack.getIndex()).map((e) => e._id) : []);
|
|
1778
|
+
},
|
|
1779
|
+
// Each write already landed.
|
|
1780
|
+
async commit() {
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
var FIELD = {
|
|
1785
|
+
Actor: "actors",
|
|
1786
|
+
Item: "items",
|
|
1787
|
+
Scene: "scenes",
|
|
1788
|
+
JournalEntry: "journal",
|
|
1789
|
+
RollTable: "tables",
|
|
1790
|
+
Macro: "macros",
|
|
1791
|
+
Cards: "cards",
|
|
1792
|
+
Playlist: "playlists"
|
|
1793
|
+
};
|
|
1794
|
+
async function openAdventure(vault) {
|
|
1795
|
+
const pack = await ensurePack(vault, "Adventure");
|
|
1796
|
+
const id = await adventureId(vault.id);
|
|
1797
|
+
const existing = await pack.getDocument(id);
|
|
1798
|
+
const source = existing ? existing.toObject() : null;
|
|
1799
|
+
const docs = /* @__PURE__ */ new Map();
|
|
1800
|
+
for (const [docName, field] of Object.entries(FIELD)) {
|
|
1801
|
+
docs.set(docName, new Map((source?.[field] ?? []).map((d) => [d._id, d])));
|
|
1802
|
+
}
|
|
1803
|
+
const folders = new Map((source?.folders ?? []).map((f) => [f._id, f]));
|
|
1804
|
+
let dirty = false;
|
|
1805
|
+
return {
|
|
1806
|
+
vault,
|
|
1807
|
+
adventure: true,
|
|
1808
|
+
async get(docName, docId) {
|
|
1809
|
+
return docs.get(docName)?.get(docId) ?? null;
|
|
1810
|
+
},
|
|
1811
|
+
async put(docName, data) {
|
|
1812
|
+
const byId = docs.get(docName);
|
|
1813
|
+
if (!byId) throw new Error(`An Adventure cannot hold a ${docName}`);
|
|
1814
|
+
const had = byId.has(data._id);
|
|
1815
|
+
byId.set(data._id, data);
|
|
1816
|
+
dirty = true;
|
|
1817
|
+
return had ? "modified" : "added";
|
|
1818
|
+
},
|
|
1819
|
+
async remove(docName, docId) {
|
|
1820
|
+
if (docs.get(docName)?.delete(docId)) dirty = true;
|
|
1821
|
+
},
|
|
1822
|
+
async putFolder(docName, folder) {
|
|
1823
|
+
const want = { ...folder, type: docName, folder: folder.folder ?? null };
|
|
1824
|
+
const have = folders.get(folder._id);
|
|
1825
|
+
if (have && have.name === want.name && have.folder === want.folder) return;
|
|
1826
|
+
folders.set(folder._id, want);
|
|
1827
|
+
dirty = true;
|
|
1828
|
+
},
|
|
1829
|
+
async contents(docName) {
|
|
1830
|
+
return [...docs.get(docName)?.values() ?? []];
|
|
1831
|
+
},
|
|
1832
|
+
async ids(docName) {
|
|
1833
|
+
return new Set(docs.get(docName)?.keys() ?? []);
|
|
1834
|
+
},
|
|
1835
|
+
async commit() {
|
|
1836
|
+
if (!dirty && existing) return;
|
|
1837
|
+
const data = {
|
|
1838
|
+
_id: id,
|
|
1839
|
+
name: vault.label || "Vault",
|
|
1840
|
+
// The sheet shows these before import, and they are the only
|
|
1841
|
+
// description a GM gets of what they are about to bring in.
|
|
1842
|
+
caption: `Synced from ${vault.url}`,
|
|
1843
|
+
description: `<p>Every page, document and folder from the <strong>${vault.label || "vault"}</strong> vault. Importing creates them in this world; importing again updates what is already here rather than duplicating it.</p>`,
|
|
1844
|
+
folders: [...folders.values()],
|
|
1845
|
+
flags: { [MODULE_ID]: { vaultId: vault.id } }
|
|
1846
|
+
};
|
|
1847
|
+
for (const [docName, field] of Object.entries(FIELD)) {
|
|
1848
|
+
data[field] = [...docs.get(docName).values()];
|
|
1849
|
+
}
|
|
1850
|
+
if (existing) await existing.update(data);
|
|
1851
|
+
else {
|
|
1852
|
+
await Adventure.create(data, { pack: pack.collection, keepId: true, keepEmbeddedIds: true });
|
|
1853
|
+
if (!await pack.getDocument(id)) {
|
|
1854
|
+
throw new Error(`Adventure ${id} was rejected on create`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1117
1861
|
// ../foundry/scripts/sync.mjs
|
|
1118
|
-
async function
|
|
1862
|
+
async function orderByBaseDeps(vault, paths, bodyMetaIndex) {
|
|
1863
|
+
const ownerOf = /* @__PURE__ */ new Map();
|
|
1864
|
+
for (const p of paths) {
|
|
1865
|
+
const fm = bodyMetaIndex.get(p)?.foundry;
|
|
1866
|
+
if (!fm?.base) continue;
|
|
1867
|
+
const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, p.replace(/\.body\.html$/i, ".md"));
|
|
1868
|
+
ownerOf.set(id, p);
|
|
1869
|
+
}
|
|
1870
|
+
if (!ownerOf.size) return paths;
|
|
1871
|
+
const dependsOn = /* @__PURE__ */ new Map();
|
|
1872
|
+
for (const p of paths) {
|
|
1873
|
+
const base = bodyMetaIndex.get(p)?.foundry?.base;
|
|
1874
|
+
const match = typeof base === "string" && base.match(/^(?:Actor|Item)\.([A-Za-z0-9]{16})$/);
|
|
1875
|
+
const from = match && ownerOf.get(match[1]);
|
|
1876
|
+
if (from && from !== p) dependsOn.set(p, from);
|
|
1877
|
+
}
|
|
1878
|
+
if (!dependsOn.size) return paths;
|
|
1879
|
+
const ordered = [];
|
|
1880
|
+
const placed = /* @__PURE__ */ new Set();
|
|
1881
|
+
const visit = (p, chain) => {
|
|
1882
|
+
if (placed.has(p) || chain.has(p)) return;
|
|
1883
|
+
chain.add(p);
|
|
1884
|
+
const from = dependsOn.get(p);
|
|
1885
|
+
if (from) visit(from, chain);
|
|
1886
|
+
if (!placed.has(p)) {
|
|
1887
|
+
placed.add(p);
|
|
1888
|
+
ordered.push(p);
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
for (const p of paths) visit(p, /* @__PURE__ */ new Set());
|
|
1892
|
+
return ordered;
|
|
1893
|
+
}
|
|
1894
|
+
async function sync(host, vault, opts = {}) {
|
|
1895
|
+
try {
|
|
1896
|
+
return await runSync(host, vault, opts);
|
|
1897
|
+
} finally {
|
|
1898
|
+
end();
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
async function runSync(host, vault, { forceFull = false } = {}) {
|
|
1119
1902
|
if (!vault?.url) {
|
|
1120
1903
|
host.notify("error", host.localize("VAULTS.Sync.NoUrl"));
|
|
1121
1904
|
return { ok: false, refreshHandlerAssets: false };
|
|
@@ -1131,6 +1914,7 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1131
1914
|
}
|
|
1132
1915
|
const start = Date.now();
|
|
1133
1916
|
host.notify("info", host.localize("VAULTS.Sync.StartingNamed", { name: vault.label }));
|
|
1917
|
+
begin(vault.label);
|
|
1134
1918
|
let manifest;
|
|
1135
1919
|
try {
|
|
1136
1920
|
manifest = await fetchManifest(vault);
|
|
@@ -1149,6 +1933,8 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1149
1933
|
const knownRoles = Array.isArray(manifest.auth?.roles) ? manifest.auth.roles : [];
|
|
1150
1934
|
const patch = {};
|
|
1151
1935
|
if (vault.public !== isPublic) patch.public = isPublic;
|
|
1936
|
+
const foundryPackage = manifest.foundry_package || "compendium";
|
|
1937
|
+
if (vault.foundryPackage !== foundryPackage) patch.foundryPackage = foundryPackage;
|
|
1152
1938
|
if (!arraysEqual(vault.knownRoles, knownRoles)) patch.knownRoles = knownRoles;
|
|
1153
1939
|
const remoteAssets = manifest.assets?.foundry || {};
|
|
1154
1940
|
const newAssetPaths = {
|
|
@@ -1158,7 +1944,8 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1158
1944
|
if (JSON.stringify(vault.handlerAssetPaths || {}) !== JSON.stringify(newAssetPaths)) {
|
|
1159
1945
|
patch.handlerAssetPaths = newAssetPaths;
|
|
1160
1946
|
}
|
|
1161
|
-
|
|
1947
|
+
const playerRole = typeof manifest.foundry_player_role === "string" ? manifest.foundry_player_role : "";
|
|
1948
|
+
if (vault.playerRole !== playerRole) patch.playerRole = playerRole;
|
|
1162
1949
|
if (Object.keys(patch).length > 0) {
|
|
1163
1950
|
await host.updateVaultEntry(vault.id, patch);
|
|
1164
1951
|
Object.assign(vault, patch);
|
|
@@ -1168,7 +1955,15 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1168
1955
|
);
|
|
1169
1956
|
const remote = new Map(files.map((f) => [f.path, f.hash]));
|
|
1170
1957
|
const lastSync = host.getVaultState(vault.id);
|
|
1171
|
-
const
|
|
1958
|
+
const remoteIdScheme = manifest.id_scheme || "v1";
|
|
1959
|
+
const schemeChanged = !!lastSync.lastIdScheme && lastSync.lastIdScheme !== remoteIdScheme;
|
|
1960
|
+
if (schemeChanged) {
|
|
1961
|
+
console.warn(
|
|
1962
|
+
`Vaults | ${vault.label}: document id scheme changed (${lastSync.lastIdScheme} \u2192 ${remoteIdScheme}); forcing a full re-sync so existing documents are re-derived rather than duplicated.`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
const fullPass = forceFull || schemeChanged;
|
|
1966
|
+
const local = fullPass ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastManifest || {}));
|
|
1172
1967
|
const bodyPaths = files.filter((f) => f.path.endsWith(".body.html")).map((f) => f.path);
|
|
1173
1968
|
const pathIndex = buildPathIndex(files);
|
|
1174
1969
|
const allMdPaths = bodyPaths.map((p) => p.replace(/\.body\.html$/i, ".md"));
|
|
@@ -1177,7 +1972,19 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1177
1972
|
for (const f of files) {
|
|
1178
1973
|
if (f.meta && f.path.endsWith(".body.html")) bodyMetaIndex.set(f.path, f.meta);
|
|
1179
1974
|
}
|
|
1180
|
-
const
|
|
1975
|
+
const missingPackages = await missingBasePackages(bodyMetaIndex.values());
|
|
1976
|
+
if (missingPackages.size > 0) {
|
|
1977
|
+
const summary = [...missingPackages].map(([pkg, n]) => `${pkg} (${n})`).join(", ");
|
|
1978
|
+
host.notify("warn", host.localize("VAULTS.Sync.MissingPackages", { packages: summary }));
|
|
1979
|
+
console.warn(
|
|
1980
|
+
`Vaults | ${vault.label}: foundry.base points into package(s) this world can't read: ${summary}. Those pages will sync as journals but instantiate no document.`
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
const prevImages = forceFull ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastImageManifest || {}));
|
|
1984
|
+
const lastMediaRefs = lastSync.lastMediaRefs || {};
|
|
1985
|
+
const mediaStale = (bodyPath) => (lastMediaRefs[bodyPath] || []).some((m) => remote.get(m) !== prevImages.get(m));
|
|
1986
|
+
const changed = bodyPaths.filter((p) => remote.get(p) !== local.get(p) || mediaStale(p));
|
|
1987
|
+
const toUpsert = await orderByBaseDeps(vault, changed, bodyMetaIndex);
|
|
1181
1988
|
const toDelete = [...local.keys()].filter((p) => p.endsWith(".body.html") && !remote.has(p));
|
|
1182
1989
|
if (forceFull) await host.setVaultState(vault.id, { lastImageManifest: {} });
|
|
1183
1990
|
let imageStats = { downloaded: 0, removed: 0, errors: 0 };
|
|
@@ -1186,8 +1993,25 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1186
1993
|
} catch (err) {
|
|
1187
1994
|
console.warn(`Vaults | image sync failed for ${vault.label}:`, err);
|
|
1188
1995
|
}
|
|
1996
|
+
const untouched = new Set(bodyPaths);
|
|
1997
|
+
for (const p of toUpsert) untouched.delete(p);
|
|
1998
|
+
for (const p of toDelete) untouched.delete(p);
|
|
1999
|
+
const target = await openTarget(vault);
|
|
2000
|
+
const missingDocs = await findMissingDocuments(target, vault, [...untouched].map((bodyPath) => ({
|
|
2001
|
+
logicalPath: bodyPath.replace(/\.body\.html$/i, ".md"),
|
|
2002
|
+
meta: bodyMetaIndex.get(bodyPath)
|
|
2003
|
+
})));
|
|
2004
|
+
const reportMissingDocs = () => {
|
|
2005
|
+
if (missingDocs.length === 0) return;
|
|
2006
|
+
host.notify("warn", host.localize("VAULTS.Sync.MissingDocuments", { count: missingDocs.length }));
|
|
2007
|
+
console.warn(
|
|
2008
|
+
`Vaults | ${vault.label}: ${missingDocs.length} page(s) have no document in this world. An incremental sync will not notice them again \u2014 use Force Sync to restore:`,
|
|
2009
|
+
missingDocs
|
|
2010
|
+
);
|
|
2011
|
+
};
|
|
1189
2012
|
if (toUpsert.length === 0 && toDelete.length === 0 && imageStats.downloaded === 0 && imageStats.removed === 0) {
|
|
1190
2013
|
host.notify("info", host.localize("VAULTS.Sync.NothingToDo"));
|
|
2014
|
+
reportMissingDocs();
|
|
1191
2015
|
return {
|
|
1192
2016
|
ok: true,
|
|
1193
2017
|
refreshHandlerAssets: false,
|
|
@@ -1195,12 +2019,18 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1195
2019
|
modified: 0,
|
|
1196
2020
|
removed: 0,
|
|
1197
2021
|
imageStats,
|
|
1198
|
-
instances: 0
|
|
2022
|
+
instances: 0,
|
|
2023
|
+
skipped: [],
|
|
2024
|
+
failed: [],
|
|
2025
|
+
// The drift list is computed above this return, so report it here too —
|
|
2026
|
+
// an up-to-date sync with missing documents is exactly the case a caller
|
|
2027
|
+
// most wants to see.
|
|
2028
|
+
missingDocuments: missingDocs
|
|
1199
2029
|
};
|
|
1200
2030
|
}
|
|
1201
2031
|
host.notify(
|
|
1202
2032
|
"info",
|
|
1203
|
-
|
|
2033
|
+
fullPass ? host.localize("VAULTS.Sync.Initial", { count: toUpsert.length }) : host.localize("VAULTS.Sync.Incremental", {
|
|
1204
2034
|
add: toUpsert.length,
|
|
1205
2035
|
mod: 0,
|
|
1206
2036
|
del: toDelete.length
|
|
@@ -1208,65 +2038,134 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1208
2038
|
);
|
|
1209
2039
|
let bodies;
|
|
1210
2040
|
try {
|
|
1211
|
-
|
|
2041
|
+
const byRole = /* @__PURE__ */ new Map();
|
|
2042
|
+
for (const bodyPath of toUpsert) {
|
|
2043
|
+
const pageRole = bodyMetaIndex.get(bodyPath)?.role;
|
|
2044
|
+
const key = pageRole || "";
|
|
2045
|
+
if (!byRole.has(key)) byRole.set(key, []);
|
|
2046
|
+
byRole.get(key).push(bodyPath);
|
|
2047
|
+
}
|
|
2048
|
+
bodies = /* @__PURE__ */ new Map();
|
|
2049
|
+
for (const [pageRole, group] of byRole) {
|
|
2050
|
+
const fetched = await fetchSourceBatch(vault, group, pageRole || void 0);
|
|
2051
|
+
for (const [k, v] of fetched) bodies.set(k, v);
|
|
2052
|
+
}
|
|
1212
2053
|
} catch (err) {
|
|
1213
2054
|
console.error(`Vaults | batch fetch failed for ${vault.label}:`, err);
|
|
1214
2055
|
host.notify("error", host.localize("VAULTS.Sync.Error", { message: err.message }));
|
|
1215
2056
|
return { ok: false, refreshHandlerAssets: false };
|
|
1216
2057
|
}
|
|
1217
2058
|
let added = 0, modified = 0, instances = 0;
|
|
2059
|
+
const skipped = [];
|
|
2060
|
+
const versionSkew = [];
|
|
2061
|
+
const failedPages = /* @__PURE__ */ new Map();
|
|
2062
|
+
const mediaRefs = {};
|
|
2063
|
+
for (const p of bodyPaths) if (lastMediaRefs[p]) mediaRefs[p] = lastMediaRefs[p];
|
|
2064
|
+
phase("Pages", toUpsert.length);
|
|
1218
2065
|
for (const bodyPath of toUpsert) {
|
|
2066
|
+
step(bodyPath.replace(/\.body\.html$/i, "").split("/").pop());
|
|
1219
2067
|
const html = bodies.get(bodyPath);
|
|
1220
2068
|
if (html == null) {
|
|
1221
2069
|
console.warn(`Vaults | server returned no content for ${bodyPath}`);
|
|
2070
|
+
failedPages.set(bodyPath, "no content returned");
|
|
1222
2071
|
continue;
|
|
1223
2072
|
}
|
|
1224
2073
|
const logicalPath = bodyPath.replace(/\.body\.html$/i, ".md");
|
|
1225
2074
|
const pageMeta = bodyMetaIndex.get(bodyPath);
|
|
1226
2075
|
try {
|
|
1227
2076
|
if (pageMeta?.foundry?.journal === false) {
|
|
1228
|
-
await deleteFile(vault, logicalPath);
|
|
2077
|
+
await deleteFile(target, vault, logicalPath);
|
|
1229
2078
|
} else {
|
|
1230
|
-
const
|
|
2079
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2080
|
+
const result = await upsertFile(target, vault, logicalPath, html, pathIndex, pageMeta, folderInfo, refs);
|
|
2081
|
+
mediaRefs[bodyPath] = [...refs];
|
|
1231
2082
|
if (result === "added") added++;
|
|
1232
2083
|
else modified++;
|
|
1233
2084
|
}
|
|
1234
2085
|
if (pageMeta?.foundry?.base) {
|
|
1235
2086
|
try {
|
|
1236
|
-
await applyInstance(vault, logicalPath, pageMeta);
|
|
1237
|
-
|
|
2087
|
+
const outcome = await applyInstance(target, vault, logicalPath, pageMeta, { forceFull: fullPass });
|
|
2088
|
+
if (outcome?.ok) {
|
|
2089
|
+
instances++;
|
|
2090
|
+
if (outcome.skew) versionSkew.push({ path: logicalPath, ...outcome.skew });
|
|
2091
|
+
} else if (outcome) skipped.push({ path: logicalPath, reason: outcome.reason });
|
|
1238
2092
|
} catch (err) {
|
|
1239
2093
|
console.warn(`Vaults | foundry instantiation failed for ${logicalPath}:`, err);
|
|
2094
|
+
skipped.push({ path: logicalPath, reason: "threw" });
|
|
1240
2095
|
}
|
|
1241
2096
|
}
|
|
1242
2097
|
} catch (err) {
|
|
1243
2098
|
console.warn(`Vaults | upsert failed for ${logicalPath}:`, err);
|
|
2099
|
+
failedPages.set(bodyPath, err?.message || "upsert threw");
|
|
1244
2100
|
}
|
|
1245
2101
|
}
|
|
1246
2102
|
let removed = 0;
|
|
2103
|
+
const failedDeletes = /* @__PURE__ */ new Set();
|
|
2104
|
+
if (toDelete.length > 0) phase("Removing", toDelete.length);
|
|
1247
2105
|
for (const bodyPath of toDelete) {
|
|
1248
2106
|
const logicalPath = bodyPath.replace(/\.body\.html$/i, ".md");
|
|
2107
|
+
step(logicalPath.split("/").pop());
|
|
1249
2108
|
try {
|
|
1250
|
-
await deleteFile(vault, logicalPath);
|
|
2109
|
+
await deleteFile(target, vault, logicalPath);
|
|
1251
2110
|
removed++;
|
|
1252
2111
|
} catch (err) {
|
|
1253
2112
|
console.warn(`Vaults | delete failed for ${logicalPath}:`, err);
|
|
2113
|
+
failedDeletes.add(bodyPath);
|
|
1254
2114
|
}
|
|
1255
2115
|
try {
|
|
1256
|
-
await deleteInstance(vault, logicalPath);
|
|
2116
|
+
await deleteInstance(target, vault, logicalPath);
|
|
1257
2117
|
} catch (err) {
|
|
1258
2118
|
console.warn(`Vaults | delete instance failed for ${logicalPath}:`, err);
|
|
1259
2119
|
}
|
|
1260
2120
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
2121
|
+
const persisted = new Map(remote);
|
|
2122
|
+
for (const bodyPath of failedPages.keys()) {
|
|
2123
|
+
const previous = local.get(bodyPath);
|
|
2124
|
+
if (previous === void 0) persisted.delete(bodyPath);
|
|
2125
|
+
else persisted.set(bodyPath, previous);
|
|
2126
|
+
}
|
|
2127
|
+
for (const bodyPath of failedDeletes) {
|
|
2128
|
+
const previous = local.get(bodyPath);
|
|
2129
|
+
if (previous !== void 0) persisted.set(bodyPath, previous);
|
|
2130
|
+
}
|
|
2131
|
+
await host.setVaultState(vault.id, {
|
|
2132
|
+
lastManifest: Object.fromEntries(persisted),
|
|
2133
|
+
lastMediaRefs: mediaRefs,
|
|
2134
|
+
lastIdScheme: remoteIdScheme
|
|
2135
|
+
});
|
|
2136
|
+
await reconcileEntries(target, vault, folderInfo, bodyMetaIndex);
|
|
2137
|
+
await target.commit();
|
|
2138
|
+
await pruneStalePacks(vault);
|
|
1264
2139
|
const seconds = ((Date.now() - start) / 1e3).toFixed(1);
|
|
1265
2140
|
host.notify("info", host.localize("VAULTS.Sync.Done", { added, modified, removed, seconds }));
|
|
2141
|
+
reportMissingDocs();
|
|
1266
2142
|
if (imageStats.downloaded > 0 || imageStats.removed > 0) {
|
|
1267
2143
|
console.info(`Vaults | ${vault.label} images: ${imageStats.downloaded} downloaded, ${imageStats.removed} removed` + (imageStats.errors ? `, ${imageStats.errors} failed` : ""));
|
|
1268
2144
|
}
|
|
1269
2145
|
if (instances > 0) console.info(`Vaults | ${vault.label} instantiated ${instances} document(s) from page foundry.base.`);
|
|
2146
|
+
if (versionSkew.length > 0) {
|
|
2147
|
+
host.notify("warn", localizeOr(
|
|
2148
|
+
host,
|
|
2149
|
+
"VAULTS.Sync.VersionSkew",
|
|
2150
|
+
"{count} document(s) came from a different Foundry generation and may not render correctly. See the console for which pack, and which version it targets.",
|
|
2151
|
+
{ count: versionSkew.length }
|
|
2152
|
+
));
|
|
2153
|
+
console.warn(
|
|
2154
|
+
`Vaults | ${vault.label}: ${versionSkew.length} document(s) were exported for a different Foundry generation than this world (${game.release?.generation}). They import, but parts of a stale document may not place correctly \u2014 a Foundry 13 Scene keeps its walls and lights but draws its tiles at the canvas origin. Ask the creator for a re-export, or point the base at a pack built for this generation:`,
|
|
2155
|
+
versionSkew
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
if (failedPages.size > 0) {
|
|
2159
|
+
host.notify("warn", host.localize("VAULTS.Sync.PagesFailed", { count: failedPages.size }));
|
|
2160
|
+
console.warn(
|
|
2161
|
+
`Vaults | ${vault.label} failed to sync ${failedPages.size} page(s); they stay in the diff and retry next sync:`,
|
|
2162
|
+
[...failedPages].map(([path, reason]) => ({ path, reason }))
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
if (skipped.length > 0) {
|
|
2166
|
+
host.notify("warn", host.localize("VAULTS.Sync.InstancesSkipped", { count: skipped.length }));
|
|
2167
|
+
console.warn(`Vaults | ${vault.label} no document created for ${skipped.length} page(s):`, skipped);
|
|
2168
|
+
}
|
|
1270
2169
|
return {
|
|
1271
2170
|
ok: true,
|
|
1272
2171
|
refreshHandlerAssets: true,
|
|
@@ -1274,7 +2173,10 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1274
2173
|
modified,
|
|
1275
2174
|
removed,
|
|
1276
2175
|
imageStats,
|
|
1277
|
-
instances
|
|
2176
|
+
instances,
|
|
2177
|
+
skipped,
|
|
2178
|
+
missingDocuments: missingDocs,
|
|
2179
|
+
failed: [...failedPages].map(([path, reason]) => ({ path, reason }))
|
|
1278
2180
|
};
|
|
1279
2181
|
}
|
|
1280
2182
|
function arraysEqual(a, b) {
|
|
@@ -1286,16 +2188,15 @@ function arraysEqual(a, b) {
|
|
|
1286
2188
|
|
|
1287
2189
|
// ../foundry/scripts/importer-entry.mjs
|
|
1288
2190
|
var REQUIRED_HOST_VERSION = 1;
|
|
1289
|
-
async function
|
|
2191
|
+
async function runSync2(host, vault, options = {}) {
|
|
1290
2192
|
return sync(host, vault, options);
|
|
1291
2193
|
}
|
|
1292
2194
|
async function runRemove(_host, vault) {
|
|
1293
|
-
await
|
|
2195
|
+
await deleteVaultPacks(vault.id);
|
|
1294
2196
|
await deleteVaultCache(vault.id);
|
|
1295
|
-
await deleteVaultInstances(vault.id);
|
|
1296
2197
|
}
|
|
1297
2198
|
export {
|
|
1298
2199
|
REQUIRED_HOST_VERSION,
|
|
1299
2200
|
runRemove,
|
|
1300
|
-
runSync
|
|
2201
|
+
runSync2 as runSync
|
|
1301
2202
|
};
|