← Files ModRetro Chromatic PluginARCHIVED FILE

dist/project-snapshot.js

28 KB · Oct 2, 2026 · 00:37 UTC

↓ Download file

import { createHash } from "node:crypto";
import { readdir, readFile, realpath } from "node:fs/promises";
import path from "node:path";
import { PROJECT_REVISION_ALGORITHM, } from "./project-access.js";
import { discoverProject, GameStudioProjectError, inventoryProject, } from "./project.js";
import { ProjectRevisionTree } from "./project-revision.js";
const RESOURCE_ORDER = [
    "scene", "actor", "trigger", "palette", "asset", "variable", "diagnostic",
];
const RESOURCE_KINDS = new Set(RESOURCE_ORDER);
const MAXIMUM_PAGE_BYTES = 32 * 1024;
const MAXIMUM_LEGACY_BYTES = 512 * 1024;
const MAXIMUM_QUERY_LENGTH = 128;
const INSPECTION_MAXIMUM_BYTES = 4 * 1024;
const ALLOWED_FIELDS = new Set([
    "id", "name", "resourcePath", "sceneId", "type", "width", "height", "x", "y",
    "backgroundId", "spriteSheetId", "actorIds", "triggerIds", "filename",
    "metadataPath", "hasMetadata", "colors", "symbol", "code", "severity", "message",
    "paletteIds", "spritePaletteIds", "defaultPlayerSprites", "startSceneId",
    "startX", "startY", "colorMode", "defaultSceneTypeId", "defaultFontId", "compilerPreset",
]);
const INSPECTION_SETTINGS = [
    "colorMode", "startSceneId", "startX", "startY", "defaultSceneTypeId",
    "defaultFontId", "defaultPlayerSprites", "compilerPreset",
];
class FrozenMap {
    #entries;
    constructor(entries) {
        this.#entries = new Map(entries);
        Object.freeze(this);
    }
    get size() {
        return this.#entries.size;
    }
    get(key) {
        return this.#entries.get(key);
    }
    has(key) {
        return this.#entries.has(key);
    }
    entries() {
        return this.#entries.entries();
    }
    keys() {
        return this.#entries.keys();
    }
    values() {
        return this.#entries.values();
    }
    [Symbol.iterator]() {
        return this.#entries[Symbol.iterator]();
    }
    forEach(callbackfn, thisArg) {
        this.#entries.forEach((value, key) => callbackfn.call(thisArg, value, key, this));
    }
}
function isObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function withinProject(root, candidate) {
    const relative = path.relative(root, candidate);
    if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
        throw new GameStudioProjectError("PATH_OUTSIDE_PROJECT", `Refusing to access a path outside the native game project: ${candidate}`, candidate);
    }
}
function systemError(error, code) {
    return typeof error === "object" && error !== null && "code" in error && error.code === code;
}
function deepFreeze(value) {
    if (typeof value !== "object" || value === null || Object.isFrozen(value))
        return value;
    for (const child of Object.values(value))
        deepFreeze(child);
    return Object.freeze(value);
}
function mapResources(resources) {
    return new FrozenMap(resources.filter((resource) => resource.id).map((resource) => [resource.id, resource]));
}
async function authoredFiles(root, directory) {
    let canonical;
    try {
        canonical = await realpath(directory);
    }
    catch (error) {
        if (systemError(error, "ENOENT"))
            return [];
        throw error;
    }
    withinProject(root, canonical);
    const entries = await readdir(canonical, { withFileTypes: true });
    entries.sort((left, right) => left.name.localeCompare(right.name));
    const files = [];
    for (const entry of entries) {
        const candidate = path.join(canonical, entry.name);
        if (entry.isSymbolicLink()) {
            let destination;
            try {
                destination = await realpath(candidate);
            }
            catch {
                throw new GameStudioProjectError("UNSAFE_SYMLINK", `Project contains an unreadable symbolic link: ${candidate}`, candidate);
            }
            withinProject(root, destination);
            continue;
        }
        if (entry.isDirectory())
            files.push(...await authoredFiles(root, candidate));
        else if (entry.isFile())
            files.push(candidate);
    }
    return files;
}
async function discoveryRevision(discovery, exclude = new Set()) {
    const root = await realpath(discovery.projectRoot);
    if (root !== discovery.projectRoot) {
        throw new GameStudioProjectError("STALE_PROJECT", "The selected native game project root changed during collection", root);
    }
    const descriptor = await realpath(discovery.projectPath);
    withinProject(root, descriptor);
    if (descriptor !== discovery.projectPath) {
        throw new GameStudioProjectError("STALE_PROJECT", "The selected project descriptor changed during collection", descriptor);
    }
    const files = [
        descriptor,
        ...await authoredFiles(root, path.join(root, "project")),
        ...await authoredFiles(root, path.join(root, "assets")),
    ];
    const entries = [];
    for (const file of files) {
        const canonical = await realpath(file);
        withinProject(root, canonical);
        if (canonical !== file) {
            throw new GameStudioProjectError("STALE_PROJECT", "An authored project resource changed during collection", file);
        }
        const relative = path.relative(root, file).split(path.sep).join("/");
        if (exclude.has(relative))
            continue;
        const content = await readFile(file);
        entries.push({
            relativePath: relative,
            sha256: createHash("sha256").update(content).digest("hex"),
        });
    }
    return new ProjectRevisionTree(entries).revision();
}
function assertMatchingAccess(projectPath, access) {
    const requestedPath = path.resolve(projectPath);
    if (requestedPath !== access.projectPath && requestedPath !== access.projectRoot) {
        throw new GameStudioProjectError("STALE_PROJECT", "Indexed project access does not belong to the requested native game project", requestedPath);
    }
    if (access.revisionAlgorithm !== PROJECT_REVISION_ALGORITHM) {
        throw new GameStudioProjectError("STALE_PROJECT", "Indexed project access uses an obsolete project revision algorithm");
    }
}
export async function projectRevision(projectPath, access) {
    if (access) {
        assertMatchingAccess(projectPath, access);
        await access.ensureFresh();
        return access.revision;
    }
    return discoveryRevision(await discoverProject(projectPath));
}
/** Internal create-only transaction check: remove only its new outputs from the
 * current census to prove that the preexisting authored project stayed unchanged. */
export async function projectRevisionBeforeAdditions(projectPath, additions) {
    return discoveryRevision(await discoverProject(projectPath), new Set(additions));
}
async function projectVariables(inventory) {
    let entries;
    let resourcePath;
    if (inventory.format === "legacy") {
        entries = inventory.descriptor.variables;
        resourcePath = path.basename(inventory.projectPath);
    }
    else {
        const filename = path.join(inventory.projectRoot, "project", "variables.gbsres");
        let canonical;
        try {
            canonical = await realpath(filename);
        }
        catch (error) {
            if (systemError(error, "ENOENT"))
                return [];
            throw error;
        }
        withinProject(inventory.projectRoot, canonical);
        let parsed;
        try {
            parsed = JSON.parse(await readFile(canonical, "utf8"));
        }
        catch (error) {
            throw new GameStudioProjectError("INVALID_RESOURCE", `Could not parse project variables: ${error instanceof Error ? error.message : String(error)}`, canonical);
        }
        if (!isObject(parsed)) {
            throw new GameStudioProjectError("INVALID_RESOURCE", "project variables resource must contain a JSON object", canonical);
        }
        entries = parsed.variables;
        resourcePath = "project/variables.gbsres";
    }
    return (Array.isArray(entries) ? entries : [])
        .filter(isObject)
        .map((resource) => ({
        ...resource,
        id: typeof resource.id === "string" ? resource.id : "",
        name: typeof resource.name === "string" ? resource.name : "",
        resourcePath,
    }));
}
function projectHealth(diagnostics) {
    const byCode = {};
    let errorCount = 0;
    let warningCount = 0;
    let infoCount = 0;
    for (const diagnostic of diagnostics) {
        byCode[diagnostic.code] = (byCode[diagnostic.code] ?? 0) + 1;
        if (diagnostic.severity === "error")
            errorCount += 1;
        else if (diagnostic.severity === "warning")
            warningCount += 1;
        else
            infoCount += 1;
    }
    return { errorCount, warningCount, infoCount, byCode };
}
export async function loadProjectSnapshot(projectPath, options = {}, access) {
    const attempts = options.maxAttempts ?? 3;
    if (!Number.isSafeInteger(attempts) || attempts < 1 || attempts > 10) {
        throw new GameStudioProjectError("INVALID_INPUT", "Snapshot maxAttempts must be an integer between 1 and 10");
    }
    const generation = options.selectionGeneration ?? 0;
    if (!Number.isSafeInteger(generation) || generation < 0) {
        throw new GameStudioProjectError("INVALID_INPUT", "Snapshot selectionGeneration must be a non-negative integer");
    }
    if (access) {
        assertMatchingAccess(projectPath, access);
        if (options.selectionGeneration !== undefined && generation !== access.selectionGeneration) {
            throw new GameStudioProjectError("STALE_PROJECT", "Indexed project access belongs to a different project selection");
        }
        await access.ensureFresh();
        return access.snapshot();
    }
    for (let attempt = 0; attempt < attempts; attempt += 1) {
        const discovery = await discoverProject(projectPath);
        let before;
        let inventory;
        let variables;
        let after;
        try {
            before = await discoveryRevision(discovery);
            inventory = await inventoryProject(discovery.projectPath);
            variables = await projectVariables(inventory);
            after = await discoveryRevision(discovery);
        }
        catch (error) {
            if ((systemError(error, "ENOENT") || (error instanceof GameStudioProjectError && error.code === "STALE_PROJECT")) && attempt + 1 < attempts) {
                continue;
            }
            throw error;
        }
        if (before !== after)
            continue;
        deepFreeze(inventory);
        deepFreeze(variables);
        const counts = { ...inventory.counts, variables: variables.length };
        const health = projectHealth(inventory.diagnostics);
        return Object.freeze({
            projectPath: inventory.projectPath,
            projectRoot: inventory.projectRoot,
            revision: after,
            revisionAlgorithm: PROJECT_REVISION_ALGORITHM,
            selectionGeneration: generation,
            descriptor: inventory.descriptor,
            settings: inventory.settings,
            scenesById: mapResources(inventory.scenes),
            actorsById: mapResources(inventory.actors),
            triggersById: mapResources(inventory.triggers),
            assetsById: mapResources(inventory.assets),
            palettesById: mapResources(inventory.palettes),
            variablesById: mapResources(variables),
            diagnostics: inventory.diagnostics,
            counts: deepFreeze(counts),
            health: deepFreeze(health),
            inventory,
        });
    }
    throw new GameStudioProjectError("STALE_PROJECT", "Native game project resources kept changing while collecting a consistent snapshot", projectPath);
}
function compactSettings(settings) {
    const result = {};
    for (const key of INSPECTION_SETTINGS) {
        const value = settings[key];
        if (value !== undefined)
            result[key] = value;
    }
    return result;
}
export function inspectProjectSnapshot(snapshot, options = {}) {
    const limit = options.diagnosticLimit ?? 5;
    if (!Number.isSafeInteger(limit) || limit < 0 || limit > 50) {
        throw new GameStudioProjectError("INVALID_INPUT", "Diagnostic limit must be an integer between 0 and 50");
    }
    const result = {
        projectPath: snapshot.projectPath,
        projectRoot: snapshot.projectRoot,
        name: snapshot.inventory.name,
        author: snapshot.inventory.author,
        format: snapshot.inventory.format,
        version: snapshot.inventory.version,
        revision: snapshot.revision,
        revisionAlgorithm: snapshot.revisionAlgorithm,
        settings: compactSettings(snapshot.settings),
        counts: { ...snapshot.counts },
        health: { ...snapshot.health, byCode: { ...snapshot.health.byCode } },
        diagnostics: snapshot.diagnostics.slice(0, limit),
        diagnosticCount: snapshot.diagnostics.length,
        diagnosticsTruncated: snapshot.diagnostics.length > limit,
    };
    while (Buffer.byteLength(JSON.stringify(result)) > INSPECTION_MAXIMUM_BYTES && result.diagnostics.length > 0) {
        result.diagnostics = result.diagnostics.slice(0, -1);
        result.diagnosticsTruncated = true;
    }
    if (Buffer.byteLength(JSON.stringify(result)) > INSPECTION_MAXIMUM_BYTES) {
        throw new GameStudioProjectError("PROJECT_INSPECTION_TOO_LARGE", "Compact project inspection exceeds the 4 KiB response limit");
    }
    return result;
}
function sortRecords(left, right) {
    return RESOURCE_ORDER.indexOf(left.kind) - RESOURCE_ORDER.indexOf(right.kind)
        || left.resourcePath.localeCompare(right.resourcePath)
        || left.id.localeCompare(right.id);
}
function normalizedQuery(options) {
    return options.query?.toLowerCase();
}
function cursorFilter(options) {
    const query = normalizedQuery(options);
    const filters = {
        sceneId: options.sceneId ?? null,
        resourceTypes: [...(options.resourceTypes ?? [])].sort(),
        assetTypes: [...(options.assetTypes ?? [])].sort(),
        detail: options.detail ?? "standard",
        fields: [...(options.fields ?? [])].sort(),
        includeSettings: options.includeSettings ?? false,
        includeReferencedResources: options.includeReferencedResources ?? false,
        layout: options.layout ?? "normalized",
        ...(query === undefined ? {} : { query }),
    };
    return createHash("sha256").update(JSON.stringify(filters)).digest("base64url").slice(0, 16);
}
function parseCursor(snapshot, options) {
    if (!options.cursor)
        return 0;
    let payload;
    try {
        payload = JSON.parse(Buffer.from(options.cursor, "base64url").toString("utf8"));
    }
    catch {
        throw new GameStudioProjectError("INVALID_CURSOR", "Project inventory cursor is not a valid opaque cursor");
    }
    if (!isObject(payload) || typeof payload.revision !== "string" || typeof payload.filter !== "string"
        || (payload.revisionAlgorithm !== undefined && typeof payload.revisionAlgorithm !== "string")
        || !Number.isSafeInteger(payload.generation) || !Number.isSafeInteger(payload.offset)
        || typeof payload.offset !== "number" || payload.offset < 0) {
        throw new GameStudioProjectError("INVALID_CURSOR", "Project inventory cursor is malformed");
    }
    if (payload.revisionAlgorithm !== snapshot.revisionAlgorithm || payload.revision !== snapshot.revision
        || payload.generation !== snapshot.selectionGeneration || payload.filter !== cursorFilter(options)) {
        throw new GameStudioProjectError("STALE_CURSOR", "Project inventory cursor no longer matches the selected project, revision, or filters");
    }
    return payload.offset;
}
function nextCursor(snapshot, options, offset) {
    const cursor = {
        revision: snapshot.revision,
        revisionAlgorithm: snapshot.revisionAlgorithm,
        generation: snapshot.selectionGeneration,
        filter: cursorFilter(options),
        offset,
    };
    return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
}
function validateOptions(options) {
    const limit = options.limit ?? 50;
    if (!Number.isSafeInteger(limit) || limit < 1 || limit > 250) {
        throw new GameStudioProjectError("INVALID_INPUT", "Project inventory limit must be an integer between 1 and 250");
    }
    if (options.detail && !["summary", "standard", "full"].includes(options.detail)) {
        throw new GameStudioProjectError("INVALID_INPUT", "Project inventory detail must be summary, standard, or full");
    }
    if (options.layout && !["normalized", "legacy"].includes(options.layout)) {
        throw new GameStudioProjectError("INVALID_INPUT", "Project inventory layout must be normalized or legacy");
    }
    if (options.query !== undefined && (typeof options.query !== "string" || options.query.trim().length === 0
        || options.query.length > MAXIMUM_QUERY_LENGTH)) {
        throw new GameStudioProjectError("INVALID_INPUT", `Project inventory query must be a nonblank string of at most ${MAXIMUM_QUERY_LENGTH} characters`);
    }
    for (const kind of options.resourceTypes ?? []) {
        if (!RESOURCE_KINDS.has(kind)) {
            throw new GameStudioProjectError("INVALID_INPUT", `Unsupported project inventory resource type: ${kind}`);
        }
    }
    for (const field of options.fields ?? []) {
        if (!ALLOWED_FIELDS.has(field)) {
            throw new GameStudioProjectError("INVALID_INPUT", `Unsupported project inventory field: ${field}`);
        }
    }
    return limit;
}
function selectFields(result, resource, options) {
    if (!options.fields?.length)
        return result;
    const selected = {};
    for (const required of ["id", "sceneId"]) {
        if (result[required] !== undefined)
            selected[required] = result[required];
    }
    for (const field of options.fields) {
        const value = result[field] ?? resource[field];
        if (value !== undefined)
            selected[field] = value;
    }
    return selected;
}
function projectRecord(kind, resource, options, sceneId) {
    if (options.detail === "full") {
        const full = { ...resource };
        if (sceneId)
            full.sceneId = sceneId;
        if (kind === "scene") {
            delete full.actors;
            delete full.triggers;
            const scene = resource;
            full.actorIds = scene.actors.map((actor) => actor.id);
            full.triggerIds = scene.triggers.map((trigger) => trigger.id);
        }
        return selectFields(full, resource, options);
    }
    const summary = { id: resource.id, name: resource.name };
    if (sceneId)
        summary.sceneId = sceneId;
    if (kind === "asset")
        summary.type = resource.type;
    if (options.detail === "summary")
        return selectFields(summary, resource, options);
    if (kind === "scene") {
        const scene = resource;
        summary.type = scene.type;
        summary.width = scene.width;
        summary.height = scene.height;
        summary.backgroundId = scene.backgroundId;
        summary.actorIds = scene.actors.map((actor) => actor.id);
        summary.triggerIds = scene.triggers.map((trigger) => trigger.id);
    }
    else if (kind === "actor") {
        if (resource.x !== undefined)
            summary.x = resource.x;
        if (resource.y !== undefined)
            summary.y = resource.y;
        if (resource.spriteSheetId !== undefined)
            summary.spriteSheetId = resource.spriteSheetId;
    }
    else if (kind === "trigger") {
        for (const field of ["x", "y", "width", "height"]) {
            if (resource[field] !== undefined)
                summary[field] = resource[field];
        }
    }
    else if (kind === "palette") {
        if (resource.colors !== undefined)
            summary.colors = resource.colors;
    }
    else if (kind === "asset") {
        const asset = resource;
        summary.filename = asset.filename;
        summary.width = asset.width;
        summary.height = asset.height;
        summary.hasMetadata = asset.hasMetadata;
    }
    else if (kind === "variable" && resource.symbol !== undefined) {
        summary.symbol = resource.symbol;
    }
    summary.resourcePath = resource.resourcePath;
    return selectFields(summary, resource, options);
}
function referencedIds(scene) {
    const assets = new Set();
    const palettes = new Set();
    if (typeof scene.backgroundId === "string" && scene.backgroundId)
        assets.add(scene.backgroundId);
    for (const actor of scene.actors) {
        if (typeof actor.spriteSheetId === "string" && actor.spriteSheetId)
            assets.add(actor.spriteSheetId);
    }
    for (const field of ["paletteIds", "spritePaletteIds"]) {
        for (const value of Array.isArray(scene[field]) ? scene[field] : []) {
            if (typeof value === "string")
                palettes.add(value);
        }
    }
    return { assets, palettes };
}
function inventoryRecords(snapshot, options) {
    const scene = options.sceneId ? snapshot.scenesById.get(options.sceneId) : undefined;
    if (options.sceneId && !scene) {
        throw new GameStudioProjectError("SCENE_NOT_FOUND", `No scene with id ${options.sceneId} exists in this project`);
    }
    const selectedKinds = new Set(options.resourceTypes?.length
        ? options.resourceTypes
        : options.sceneId
            ? ["scene", "actor", "trigger", ...(options.includeReferencedResources ? ["asset", "palette"] : [])]
            : RESOURCE_ORDER);
    const referenced = scene ? referencedIds(scene) : undefined;
    const query = normalizedQuery(options);
    const records = [];
    const add = (kind, resource, owner) => {
        if (!selectedKinds.has(kind))
            return;
        if (query !== undefined && !resource.name.toLowerCase().includes(query) && !resource.id.toLowerCase().includes(query)
            && !(kind === "asset" && resource.filename.toLowerCase().includes(query)))
            return;
        records.push({
            kind,
            resourcePath: resource.resourcePath,
            id: resource.id,
            resource: projectRecord(kind, resource, options, owner),
        });
    };
    for (const candidate of snapshot.inventory.scenes) {
        if (scene && candidate.id !== scene.id)
            continue;
        add("scene", candidate);
        for (const actor of candidate.actors)
            add("actor", actor, candidate.id);
        for (const trigger of candidate.triggers)
            add("trigger", trigger, candidate.id);
    }
    for (const palette of snapshot.inventory.palettes) {
        if (scene && !referenced?.palettes.has(palette.id))
            continue;
        add("palette", palette);
    }
    for (const asset of snapshot.inventory.assets) {
        if (scene && !referenced?.assets.has(asset.id))
            continue;
        if (options.assetTypes?.length && !options.assetTypes.includes(asset.type))
            continue;
        add("asset", asset);
    }
    for (const variable of snapshot.variablesById.values())
        add("variable", variable);
    if (selectedKinds.has("diagnostic") && query === undefined) {
        for (let index = 0; index < snapshot.diagnostics.length; index += 1) {
            const diagnostic = snapshot.diagnostics[index];
            if (!diagnostic)
                continue;
            if (scene && diagnostic.resourcePath && !diagnostic.resourcePath.startsWith(path.dirname(scene.resourcePath)))
                continue;
            records.push({
                kind: "diagnostic",
                resourcePath: diagnostic.resourcePath ?? "",
                id: `${diagnostic.code}:${index}`,
                resource: diagnostic,
            });
        }
    }
    records.sort(sortRecords);
    return records;
}
function recordArray(result, kind) {
    switch (kind) {
        case "scene": return result.scenes;
        case "actor": return result.actors;
        case "trigger": return result.triggers;
        case "palette": return result.palettes;
        case "asset": return result.assets;
        case "variable": return result.variables;
        case "diagnostic": return result.diagnostics;
    }
}
export function inventoryProjectSnapshot(snapshot, options = {}) {
    const limit = validateOptions(options);
    if (options.layout === "legacy") {
        if (options.detail !== "full") {
            throw new GameStudioProjectError("INVALID_INPUT", "Legacy project inventory requires explicitly setting detail to full");
        }
        if (options.cursor || options.sceneId || options.resourceTypes?.length || options.assetTypes?.length || options.fields?.length
            || options.query !== undefined) {
            throw new GameStudioProjectError("INVALID_INPUT", "Legacy full project inventory does not support normalized filters or cursors");
        }
        if (Buffer.byteLength(JSON.stringify(snapshot.inventory)) > MAXIMUM_LEGACY_BYTES) {
            throw new GameStudioProjectError("INVENTORY_PAGE_TOO_LARGE", "Legacy full project inventory exceeds the 512 KiB compatibility limit");
        }
        return snapshot.inventory;
    }
    const offset = parseCursor(snapshot, options);
    const records = inventoryRecords(snapshot, options);
    if (offset > records.length) {
        throw new GameStudioProjectError("INVALID_CURSOR", "Project inventory cursor points beyond the available resources");
    }
    const result = {
        projectPath: snapshot.projectPath,
        projectRoot: snapshot.projectRoot,
        name: snapshot.inventory.name,
        format: snapshot.inventory.format,
        version: snapshot.inventory.version,
        revision: snapshot.revision,
        revisionAlgorithm: snapshot.revisionAlgorithm,
        counts: snapshot.counts,
        health: snapshot.health,
        ...(options.includeSettings ? { settings: compactSettings(snapshot.settings) } : {}),
        scenes: [],
        actors: [],
        triggers: [],
        palettes: [],
        assets: [],
        variables: [],
        diagnostics: [],
        total: records.length,
        returned: 0,
        nextCursor: null,
    };
    for (const record of records.slice(offset, offset + limit)) {
        const destination = recordArray(result, record.kind);
        destination.push(record.resource);
        result.returned += 1;
        const followingOffset = offset + result.returned;
        result.nextCursor = followingOffset < records.length ? nextCursor(snapshot, options, followingOffset) : null;
        if (Buffer.byteLength(JSON.stringify(result)) > MAXIMUM_PAGE_BYTES) {
            destination.pop();
            result.returned -= 1;
            if (result.returned === 0) {
                throw new GameStudioProjectError("INVENTORY_PAGE_TOO_LARGE", "One project resource exceeds the 32 KiB normalized page limit; request summary detail or specific fields");
            }
            result.nextCursor = nextCursor(snapshot, options, offset + result.returned);
            break;
        }
    }
    return result;
}
export async function inventoryProjectNormalized(projectPath, options = {}, selectionGeneration = 0, access) {
    if (access) {
        assertMatchingAccess(projectPath, access);
        if (selectionGeneration !== 0 && selectionGeneration !== access.selectionGeneration) {
            throw new GameStudioProjectError("STALE_PROJECT", "Indexed project access belongs to a different project selection");
        }
        await access.ensureFresh();
        if (options.layout !== "legacy")
            return access.normalizedInventory(options);
        validateOptions(options);
        if (options.detail !== "full") {
            throw new GameStudioProjectError("INVALID_INPUT", "Legacy project inventory requires explicitly setting detail to full");
        }
        if (options.cursor || options.sceneId || options.resourceTypes?.length || options.assetTypes?.length || options.fields?.length
            || options.query !== undefined) {
            throw new GameStudioProjectError("INVALID_INPUT", "Legacy full project inventory does not support normalized filters or cursors");
        }
        const inventory = await access.legacyInventory();
        if (Buffer.byteLength(JSON.stringify(inventory)) > MAXIMUM_LEGACY_BYTES) {
            throw new GameStudioProjectError("INVENTORY_PAGE_TOO_LARGE", "Legacy full project inventory exceeds the 512 KiB compatibility limit");
        }
        return inventory;
    }
    const snapshot = await loadProjectSnapshot(projectPath, { selectionGeneration });
    return inventoryProjectSnapshot(snapshot, options);
}
//# sourceMappingURL=project-snapshot.js.map

SHA-256: d45ae7dcaebf6c990ac985e4967acfca7c79c5431b280e7a25e3443e369f1366