import { analyzeStorage } from "../core/analyzer.js"; import { isMediaPath, normalizePath } from "../core/path-utils.js"; async function* walkFilePicker(storage, target = "", onProgress = null) { onProgress?.({ phase: "files", label: format("KSA.Progress.BrowseStorage", { storage, path: target || "/" }) }); const result = await FilePicker.browse(storage, target); for (const file of result.files ?? []) { const path = normalizePath(file); if (!isMediaPath(path)) continue; yield { storage, path }; } for (const directory of result.dirs ?? []) { const path = normalizePath(directory); yield* walkFilePicker(storage, path, onProgress); } } async function* listFoundryFiles(onProgress = null) { for (const storage of ["data", "public"]) { if (!game.data.files.storages.includes(storage)) continue; yield* walkFilePicker(storage, "", onProgress); } } function worldCollectionEntries() { if (!game.world) return []; const entries = []; for (const collection of game.collections ?? []) { const collectionDocumentName = collection.documentName ?? null; if (collectionDocumentName === "ChatMessage") continue; const docs = Array.from(collection.values?.() ?? []); for (const doc of docs) { entries.push({ sourceType: "world-document", sourceScope: { ownerType: "world", ownerId: game.world.id, systemId: game.world.system, subtype: doc.documentName?.toLowerCase() ?? collectionDocumentName?.toLowerCase() ?? "document" }, sourceLabel: `${doc.documentName ?? "Document"} ${doc.id}`, sourceName: doc.name ?? null, sourceUuid: doc.uuid ?? null, resolveSourceTrail: candidatePath => resolveSourceTrail(doc, candidatePath), value: doc.toObject ? doc.toObject() : doc }); } } return entries; } function packageMetadataEntries() { const entries = []; for (const module of game.modules.values()) { entries.push({ sourceType: "module-manifest", sourceScope: { ownerType: "module", ownerId: module.id, subtype: "manifest" }, sourceLabel: `module.json ${module.id}`, value: module.toObject ? module.toObject() : module }); } if (game.system) { entries.push({ sourceType: "system-manifest", sourceScope: { ownerType: "system", ownerId: game.system.id, subtype: "manifest" }, sourceLabel: `system.json ${game.system.id}`, value: game.system.toObject ? game.system.toObject() : game.system }); } if (game.world) { entries.push({ sourceType: "world-manifest", sourceScope: { ownerType: "world", ownerId: game.world.id, systemId: game.world.system, subtype: "manifest" }, sourceLabel: `world.json ${game.world.id}`, value: game.world.toObject ? game.world.toObject() : game.world }); } return entries; } async function* packagePackEntries(onProgress = null) { for (const pack of game.packs.values()) { const ownerType = pack.metadata.packageType; const ownerId = pack.metadata.packageName; if (!["module", "system"].includes(ownerType) || !ownerId) continue; onProgress?.({ phase: "sources", label: format("KSA.Progress.ReadPack", { pack: pack.collection }), currentSource: pack.collection }); const documents = await pack.getDocuments(); for (const document of documents) { yield { sourceType: "package-pack-document", sourceScope: { ownerType, ownerId, subtype: `pack:${pack.collection}` }, sourceLabel: `${pack.collection} ${document.id}`, sourceName: document.name ?? null, sourceUuid: document.uuid ?? null, resolveSourceTrail: candidatePath => resolveSourceTrail(document, candidatePath), value: document.toObject ? document.toObject() : document }; } } } async function* listFoundrySources(onProgress = null) { yield* worldCollectionEntries(); yield* packageMetadataEntries(); yield* packagePackEntries(onProgress); } export async function runRuntimeAnalysis({ onProgress }={}) { return analyzeStorage({ listFiles: () => listFoundryFiles(onProgress), listSources: () => listFoundrySources(onProgress), onProgress, i18n: { format } }); } function format(key, data) { return game.i18n?.format(key, data) ?? key; } function resolveSourceTrail(document, candidatePath = []) { const trail = [createTrailNode(document.uuid, document.name ?? `${document.documentName ?? "Document"} ${document.id}`)]; const path = Array.isArray(candidatePath) ? candidatePath : []; if (!path.length) return trail; if (document.documentName === "Actor") { const itemNode = resolveActorItemTrail(document, path); if (itemNode) trail.push(itemNode); return trail; } if (document.documentName === "Scene") { const sceneTrail = resolveSceneTrail(document, path); if (sceneTrail.length) trail.push(...sceneTrail); } return trail; } function resolveActorItemTrail(actor, path) { if ((path[0] !== "items") || !Number.isInteger(path[1])) return null; const itemData = actor.items?.contents?.[path[1]] ?? actor.toObject?.().items?.[path[1]] ?? null; const itemId = itemData?.id ?? itemData?._id ?? null; if (!itemId) return null; const itemName = itemData?.name ?? `Item ${itemId}`; return createTrailNode(`${actor.uuid}.Item.${itemId}`, itemName); } function resolveSceneTrail(scene, path) { if ((path[0] !== "tokens") || !Number.isInteger(path[1])) return []; const token = scene.tokens?.contents?.[path[1]] ?? scene.toObject?.().tokens?.[path[1]] ?? null; const actorId = token?.actorId ?? token?.actor?.id ?? null; const actor = actorId ? game.actors?.get(actorId) ?? null : null; if (!actor) return []; const trail = [createTrailNode(actor.uuid, actor.name ?? `Actor ${actor.id}`)]; const itemPath = extractTokenItemPath(path); if (!itemPath) return trail; const tokenData = scene.toObject?.().tokens?.[path[1]] ?? null; const itemData = resolveTokenItemData(tokenData, itemPath.itemIndex); const itemId = itemData?._id ?? itemData?.id ?? null; if (!itemId) return trail; trail.push(createTrailNode(`${actor.uuid}.Item.${itemId}`, itemData?.name ?? `Item ${itemId}`)); return trail; } function extractTokenItemPath(path) { if ((path[2] === "actorData") && (path[3] === "items") && Number.isInteger(path[4])) { return { itemIndex: path[4] }; } if ((path[2] === "delta") && (path[3] === "items") && Number.isInteger(path[4])) { return { itemIndex: path[4] }; } return null; } function resolveTokenItemData(tokenData, itemIndex) { if (!tokenData || !Number.isInteger(itemIndex)) return null; if (Array.isArray(tokenData.actorData?.items)) return tokenData.actorData.items[itemIndex] ?? null; if (Array.isArray(tokenData.delta?.items)) return tokenData.delta.items[itemIndex] ?? null; return null; } function createTrailNode(uuid, label) { return { uuid, label }; }