202 lines
7.0 KiB
JavaScript
202 lines
7.0 KiB
JavaScript
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.ScanFiles"),
|
|
currentSource: 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.ReadReferences"),
|
|
currentSource: format("KSA.Progress.ReadPack", { pack: 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 };
|
|
}
|