diff --git a/packages/asset-system/module.json b/packages/asset-system/module.json new file mode 100644 index 00000000..e8d9ea1d --- /dev/null +++ b/packages/asset-system/module.json @@ -0,0 +1,41 @@ +{ + "id": "asset-system", + "name": "@esengine/asset-system", + "displayName": "Asset System", + "description": "Asset loading, caching and management | 资源加载、缓存和管理", + "version": "1.0.0", + "category": "Core", + "icon": "FolderOpen", + "tags": [ + "asset", + "resource", + "loader" + ], + "isCore": true, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": false, + "platforms": [ + "web", + "desktop", + "mobile" + ], + "dependencies": [ + "core" + ], + "exports": { + "loaders": [ + "TextureLoader", + "JsonLoader", + "TextLoader", + "BinaryLoader" + ], + "other": [ + "AssetManager", + "AssetDatabase", + "AssetCache" + ] + }, + "requiresWasm": false, + "outputPath": "dist/index.js" +} diff --git a/packages/asset-system/src/bundle/BundleFormat.ts b/packages/asset-system/src/bundle/BundleFormat.ts new file mode 100644 index 00000000..572a4682 --- /dev/null +++ b/packages/asset-system/src/bundle/BundleFormat.ts @@ -0,0 +1,272 @@ +/** + * Asset Bundle Format Definitions + * 资产包格式定义 + * + * Binary format for efficient asset storage and loading. + * 用于高效资产存储和加载的二进制格式。 + */ + +import { AssetGUID, AssetType } from '../types/AssetTypes'; + +/** + * Bundle file magic number + * 包文件魔数 + */ +export const BUNDLE_MAGIC = 'ESBNDL'; + +/** + * Bundle format version + * 包格式版本 + */ +export const BUNDLE_VERSION = 1; + +/** + * Bundle compression types + * 包压缩类型 + */ +export enum BundleCompression { + None = 0, + Gzip = 1, + Brotli = 2 +} + +/** + * Bundle flags + * 包标志 + */ +export enum BundleFlags { + None = 0, + Compressed = 1 << 0, + Encrypted = 1 << 1, + Streaming = 1 << 2 +} + +/** + * Asset type codes for binary serialization + * 用于二进制序列化的资产类型代码 + */ +export const AssetTypeCode: Record = { + texture: 1, + audio: 2, + json: 3, + text: 4, + binary: 5, + scene: 6, + prefab: 7, + font: 8, + shader: 9, + material: 10, + mesh: 11, + animation: 12, + tilemap: 20, + tileset: 21, + 'behavior-tree': 22, + blueprint: 23 +}; + +/** + * Bundle header structure (32 bytes) + * 包头结构 (32 字节) + */ +export interface IBundleHeader { + /** Magic number "ESBNDL" | 魔数 */ + magic: string; + /** Format version | 格式版本 */ + version: number; + /** Bundle flags | 包标志 */ + flags: BundleFlags; + /** Compression type | 压缩类型 */ + compression: BundleCompression; + /** Number of assets | 资产数量 */ + assetCount: number; + /** TOC offset from start | TOC 偏移量 */ + tocOffset: number; + /** Data offset from start | 数据偏移量 */ + dataOffset: number; +} + +/** + * Table of Contents entry (40 bytes per entry) + * 目录条目 (每条 40 字节) + */ +export interface IBundleTocEntry { + /** Asset GUID (16 bytes as UUID binary) | 资产 GUID */ + guid: AssetGUID; + /** Asset type code | 资产类型代码 */ + typeCode: number; + /** Offset from data section start | 相对于数据段起始的偏移 */ + offset: number; + /** Compressed size in bytes | 压缩后大小 */ + compressedSize: number; + /** Uncompressed size in bytes | 未压缩大小 */ + uncompressedSize: number; +} + +/** + * Bundle manifest (JSON sidecar file) + * 包清单 (JSON 附属文件) + */ +export interface IBundleManifest { + /** Bundle name | 包名称 */ + name: string; + /** Bundle version | 包版本 */ + version: string; + /** Content hash for integrity | 内容哈希 */ + hash: string; + /** Compression type | 压缩类型 */ + compression: 'none' | 'gzip' | 'brotli'; + /** Total bundle size | 包总大小 */ + size: number; + /** Assets in this bundle | 包含的资产 */ + assets: IBundleAssetInfo[]; + /** Dependencies on other bundles | 依赖的其他包 */ + dependencies: string[]; + /** Creation timestamp | 创建时间戳 */ + createdAt: number; +} + +/** + * Asset info in bundle manifest + * 包清单中的资产信息 + */ +export interface IBundleAssetInfo { + /** Asset GUID | 资产 GUID */ + guid: AssetGUID; + /** Asset name (for debugging) | 资产名称 (用于调试) */ + name: string; + /** Asset type | 资产类型 */ + type: AssetType; + /** Offset in bundle | 包内偏移 */ + offset: number; + /** Size in bytes | 大小 */ + size: number; +} + +/** + * Runtime catalog format (loaded in browser) + * 运行时目录格式 (在浏览器中加载) + */ +export interface IRuntimeCatalog { + /** Catalog version | 目录版本 */ + version: string; + /** Creation timestamp | 创建时间戳 */ + createdAt: number; + /** Available bundles | 可用的包 */ + bundles: Record; + /** Asset GUID to location mapping | 资产 GUID 到位置的映射 */ + assets: Record; +} + +/** + * Bundle info in runtime catalog + * 运行时目录中的包信息 + */ +export interface IRuntimeBundleInfo { + /** Bundle URL (relative to catalog) | 包 URL */ + url: string; + /** Bundle size in bytes | 包大小 */ + size: number; + /** Content hash | 内容哈希 */ + hash: string; + /** Whether bundle is preloaded | 是否预加载 */ + preload?: boolean; +} + +/** + * Asset location in runtime catalog + * 运行时目录中的资产位置 + */ +export interface IRuntimeAssetLocation { + /** Bundle name containing this asset | 包含此资产的包名 */ + bundle: string; + /** Offset within bundle | 包内偏移 */ + offset: number; + /** Size in bytes | 大小 */ + size: number; + /** Asset type | 资产类型 */ + type: AssetType; + /** Asset name (for debugging) | 资产名称 */ + name?: string; +} + +/** + * Bundle packing options + * 包打包选项 + */ +export interface IBundlePackOptions { + /** Bundle name | 包名称 */ + name: string; + /** Compression type | 压缩类型 */ + compression?: BundleCompression; + /** Maximum bundle size (split if exceeded) | 最大包大小 */ + maxSize?: number; + /** Group assets by type | 按类型分组资产 */ + groupByType?: boolean; + /** Include asset names in bundle | 在包中包含资产名称 */ + includeNames?: boolean; +} + +/** + * Asset to pack + * 要打包的资产 + */ +export interface IAssetToPack { + /** Asset GUID | 资产 GUID */ + guid: AssetGUID; + /** Asset path (for reading) | 资产路径 */ + path: string; + /** Asset type | 资产类型 */ + type: AssetType; + /** Asset name | 资产名称 */ + name: string; + /** Raw data (or null to read from path) | 原始数据 */ + data?: ArrayBuffer; +} + +/** + * Parse GUID from 16-byte binary + * 从 16 字节二进制解析 GUID + */ +export function parseGUIDFromBinary(bytes: Uint8Array): AssetGUID { + const hex = Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; +} + +/** + * Serialize GUID to 16-byte binary + * 将 GUID 序列化为 16 字节二进制 + */ +export function serializeGUIDToBinary(guid: AssetGUID): Uint8Array { + const hex = guid.replace(/-/g, ''); + const bytes = new Uint8Array(16); + + for (let i = 0; i < 16; i++) { + bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + + return bytes; +} + +/** + * Get type code from asset type string + * 从资产类型字符串获取类型代码 + */ +export function getAssetTypeCode(type: AssetType): number { + return AssetTypeCode[type] || 0; +} + +/** + * Get asset type string from type code + * 从类型代码获取资产类型字符串 + */ +export function getAssetTypeFromCode(code: number): AssetType { + for (const [type, typeCode] of Object.entries(AssetTypeCode)) { + if (typeCode === code) { + return type as AssetType; + } + } + return 'binary'; +} diff --git a/packages/asset-system/src/core/AssetDatabase.ts b/packages/asset-system/src/core/AssetDatabase.ts index 76387483..a11fd016 100644 --- a/packages/asset-system/src/core/AssetDatabase.ts +++ b/packages/asset-system/src/core/AssetDatabase.ts @@ -22,6 +22,76 @@ export class AssetDatabase { private readonly _dependencies = new Map>(); private readonly _dependents = new Map>(); + /** Project root path for resolving relative paths. | 项目根路径,用于解析相对路径。 */ + private _projectRoot: string | null = null; + + /** + * Set project root path. + * 设置项目根路径。 + * + * @param path - Absolute path to project root. | 项目根目录的绝对路径。 + */ + setProjectRoot(path: string): void { + this._projectRoot = path; + } + + /** + * Get project root path. + * 获取项目根路径。 + */ + getProjectRoot(): string | null { + return this._projectRoot; + } + + /** + * Resolve relative path to absolute path. + * 将相对路径解析为绝对路径。 + * + * @param relativePath - Relative asset path (e.g., "assets/texture.png"). | 相对资产路径。 + * @returns Absolute file system path. | 绝对文件系统路径。 + */ + resolveAbsolutePath(relativePath: string): string { + // Already absolute path (Windows or Unix). + // 已经是绝对路径。 + if (relativePath.match(/^[a-zA-Z]:/) || relativePath.startsWith('/')) { + return relativePath; + } + + // No project root set, return as-is. + // 未设置项目根路径,原样返回。 + if (!this._projectRoot) { + return relativePath; + } + + // Join with project root. + // 与项目根路径拼接。 + const separator = this._projectRoot.includes('\\') ? '\\' : '/'; + const normalizedPath = relativePath.replace(/[/\\]/g, separator); + return `${this._projectRoot}${separator}${normalizedPath}`; + } + + /** + * Convert absolute path to relative path. + * 将绝对路径转换为相对路径。 + * + * @param absolutePath - Absolute file system path. | 绝对文件系统路径。 + * @returns Relative asset path, or null if not under project root. | 相对资产路径。 + */ + toRelativePath(absolutePath: string): string | null { + if (!this._projectRoot) { + return null; + } + + const normalizedAbs = absolutePath.replace(/\\/g, '/'); + const normalizedRoot = this._projectRoot.replace(/\\/g, '/'); + + if (normalizedAbs.startsWith(normalizedRoot)) { + return normalizedAbs.substring(normalizedRoot.length + 1); + } + + return null; + } + /** * Add asset to database * 添加资产到数据库 diff --git a/packages/asset-system/src/core/AssetManager.ts b/packages/asset-system/src/core/AssetManager.ts index 5b2a113e..e9e38e25 100644 --- a/packages/asset-system/src/core/AssetManager.ts +++ b/packages/asset-system/src/core/AssetManager.ts @@ -21,7 +21,8 @@ import { IAssetManager, IAssetLoadQueue } from '../interfaces/IAssetManager'; -import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader'; +import { IAssetLoader, IAssetLoaderFactory, IAssetParseContext } from '../interfaces/IAssetLoader'; +import { IAssetReader, IAssetContent } from '../interfaces/IAssetReader'; import { AssetCache } from './AssetCache'; import { AssetLoadQueue } from './AssetLoadQueue'; import { AssetLoaderFactory } from '../loaders/AssetLoaderFactory'; @@ -55,6 +56,9 @@ export class AssetManager implements IAssetManager { private readonly _loaderFactory: IAssetLoaderFactory; private readonly _database: AssetDatabase; + /** Asset reader for file operations. | 用于文件操作的资产读取器。 */ + private _reader: IAssetReader | null = null; + private _nextHandle: AssetHandle = 1; private _statistics = { @@ -71,12 +75,35 @@ export class AssetManager implements IAssetManager { this._loaderFactory = new AssetLoaderFactory(); this._database = new AssetDatabase(); - // 如果提供了目录,初始化数据库 / Initialize database if catalog provided if (catalog) { this.initializeFromCatalog(catalog); } } + /** + * Set asset reader. + * 设置资产读取器。 + */ + setReader(reader: IAssetReader): void { + this._reader = reader; + } + + /** + * Set project root path for resolving relative paths. + * 设置项目根路径用于解析相对路径。 + */ + setProjectRoot(path: string): void { + this._database.setProjectRoot(path); + } + + /** + * Get the asset database. + * 获取资产数据库。 + */ + getDatabase(): AssetDatabase { + return this._database; + } + /** * Initialize from catalog * 从目录初始化 @@ -196,32 +223,89 @@ export class AssetManager implements IAssetManager { startTime: number, entry: AssetEntry ): Promise> { - // 加载依赖 / Load dependencies + if (!this._reader) { + throw new Error('Asset reader not set. Call setReader() first.'); + } + + // Load dependencies first. + // 先加载依赖。 if (metadata.dependencies.length > 0) { await this.loadDependencies(metadata.dependencies, options); } - // 执行加载 / Execute loading - const result = await loader.load(metadata.path, metadata, options); + // Resolve absolute path. + // 解析绝对路径。 + const absolutePath = this._database.resolveAbsolutePath(metadata.path); - // 更新条目 / Update entry - entry.asset = result.asset; + // Read content based on loader's content type. + // 根据加载器的内容类型读取内容。 + const content = await this.readContent(loader.contentType, absolutePath); + + // Create parse context. + // 创建解析上下文。 + const context: IAssetParseContext = { + metadata, + options, + loadDependency: async (relativePath: string) => { + const result = await this.loadAssetByPath(relativePath, options); + return result.asset; + } + }; + + // Parse asset. + // 解析资产。 + const asset = await loader.parse(content, context); + + // Update entry. + // 更新条目。 + entry.asset = asset; entry.state = AssetState.Loaded; - // 缓存资产 / Cache asset - this._cache.set(metadata.guid, result.asset); + // Cache asset. + // 缓存资产。 + this._cache.set(metadata.guid, asset); - // 更新统计 / Update statistics + // Update statistics. + // 更新统计。 this._statistics.loadedCount++; - const loadResult: IAssetLoadResult = { - asset: result.asset as T, + return { + asset: asset as T, handle: entry.handle, metadata, loadTime: performance.now() - startTime }; + } - return loadResult; + /** + * Read content based on content type. + * 根据内容类型读取内容。 + */ + private async readContent(contentType: string, absolutePath: string): Promise { + if (!this._reader) { + throw new Error('Asset reader not set'); + } + + switch (contentType) { + case 'text': { + const text = await this._reader.readText(absolutePath); + return { type: 'text', text }; + } + case 'binary': { + const binary = await this._reader.readBinary(absolutePath); + return { type: 'binary', binary }; + } + case 'image': { + const image = await this._reader.loadImage(absolutePath); + return { type: 'image', image }; + } + case 'audio': { + const audioBuffer = await this._reader.loadAudio(absolutePath); + return { type: 'audio', audioBuffer }; + } + default: + throw new Error(`Unknown content type: ${contentType}`); + } } /** @@ -429,6 +513,19 @@ export class AssetManager implements IAssetManager { return this.getAsset(guid); } + /** + * Get loaded asset by path (synchronous) + * 通过路径获取已加载的资产(同步) + * + * Returns the asset if it's already loaded, null otherwise. + * 如果资产已加载则返回资产,否则返回 null。 + */ + getAssetByPath(path: string): T | null { + const guid = this._pathToGuid.get(path); + if (!guid) return null; + return this.getAsset(guid); + } + /** * Check if asset is loaded * 检查资产是否已加载 diff --git a/packages/asset-system/src/index.ts b/packages/asset-system/src/index.ts index 5d807290..f1642e23 100644 --- a/packages/asset-system/src/index.ts +++ b/packages/asset-system/src/index.ts @@ -1,14 +1,28 @@ /** * Asset System for ECS Framework * ECS框架的资产系统 + * + * Runtime-focused asset management: + * - Asset loading and caching + * - GUID-based asset resolution + * - Bundle loading + * + * For editor-side functionality (meta files, packing), use @esengine/asset-system-editor */ // Types export * from './types/AssetTypes'; +// Bundle format (shared types for runtime and editor) +export * from './bundle/BundleFormat'; + +// Runtime catalog +export { RuntimeCatalog, runtimeCatalog } from './runtime/RuntimeCatalog'; + // Interfaces export * from './interfaces/IAssetLoader'; export * from './interfaces/IAssetManager'; +export * from './interfaces/IAssetReader'; export * from './interfaces/IResourceComponent'; // Core @@ -51,9 +65,12 @@ export const assetManager = new AssetManager(); * Initialize asset system with catalog * 使用目录初始化资产系统 */ -export function initializeAssetSystem(catalog?: any): AssetManager { +export function initializeAssetSystem(catalog?: IAssetCatalog): AssetManager { if (catalog) { return new AssetManager(catalog); } return assetManager; } + +// Re-export IAssetCatalog for initializeAssetSystem signature +import type { IAssetCatalog } from './types/AssetTypes'; diff --git a/packages/asset-system/src/integration/EngineIntegration.ts b/packages/asset-system/src/integration/EngineIntegration.ts index b4dcd1ee..d653176d 100644 --- a/packages/asset-system/src/integration/EngineIntegration.ts +++ b/packages/asset-system/src/integration/EngineIntegration.ts @@ -65,8 +65,8 @@ export class EngineIntegration { * Load texture for component * 为组件加载纹理 * - * 统一的路径解析入口:相对路径会被转换为 Tauri 可用的 asset:// URL - * Unified path resolution entry: relative paths will be converted to Tauri-compatible asset:// URLs + * AssetManager 内部会处理路径解析,这里只需传入原始路径。 + * AssetManager handles path resolution internally, just pass the original path here. */ async loadTextureForComponent(texturePath: string): Promise { // 检查缓存(使用原始路径作为键) @@ -76,19 +76,18 @@ export class EngineIntegration { return existingId; } - // 使用 globalPathResolver 转换路径 - // Use globalPathResolver to transform the path - const resolvedPath = globalPathResolver.resolve(texturePath); - - // 通过资产系统加载(使用解析后的路径) - // Load through asset system (using resolved path) - const result = await this._assetManager.loadAssetByPath(resolvedPath); + // 通过资产系统加载(AssetManager 内部会解析路径) + // Load through asset system (AssetManager resolves path internally) + const result = await this._assetManager.loadAssetByPath(texturePath); const textureAsset = result.asset; - // 如果有引擎桥接,上传到GPU(使用解析后的路径) - // Upload to GPU if bridge exists (using resolved path) + // 如果有引擎桥接,上传到GPU + // Upload to GPU if bridge exists + // 使用 globalPathResolver 将路径转换为引擎可用的 URL + // Use globalPathResolver to convert path to engine-compatible URL if (this._engineBridge && textureAsset.data) { - await this._engineBridge.loadTexture(textureAsset.textureId, resolvedPath); + const engineUrl = globalPathResolver.resolve(texturePath); + await this._engineBridge.loadTexture(textureAsset.textureId, engineUrl); } // 缓存映射(使用原始路径作为键,避免重复解析) diff --git a/packages/asset-system/src/interfaces/IAssetLoader.ts b/packages/asset-system/src/interfaces/IAssetLoader.ts index 9bb44b7d..b88979ae 100644 --- a/packages/asset-system/src/interfaces/IAssetLoader.ts +++ b/packages/asset-system/src/interfaces/IAssetLoader.ts @@ -7,40 +7,64 @@ import { AssetType, AssetGUID, IAssetLoadOptions, - IAssetMetadata, - IAssetLoadResult + IAssetMetadata } from '../types/AssetTypes'; +import type { IAssetContent, AssetContentType } from './IAssetReader'; /** - * Base asset loader interface - * 基础资产加载器接口 + * Parse context provided to loaders. + * 提供给加载器的解析上下文。 + */ +export interface IAssetParseContext { + /** Asset metadata. | 资产元数据。 */ + metadata: IAssetMetadata; + /** Load options. | 加载选项。 */ + options?: IAssetLoadOptions; + /** + * Load a dependency asset by relative path. + * 通过相对路径加载依赖资产。 + */ + loadDependency(relativePath: string): Promise; +} + +/** + * Asset loader interface. + * 资产加载器接口。 + * + * Loaders only parse content, file reading is handled by AssetManager. + * 加载器只负责解析内容,文件读取由 AssetManager 处理。 */ export interface IAssetLoader { - /** 支持的资产类型 / Supported asset type */ + /** Supported asset type. | 支持的资产类型。 */ readonly supportedType: AssetType; - /** 支持的文件扩展名 / Supported file extensions */ + /** Supported file extensions. | 支持的文件扩展名。 */ readonly supportedExtensions: string[]; /** - * Load an asset from the given path - * 从指定路径加载资产 + * Required content type for this loader. + * 此加载器需要的内容类型。 + * + * - 'text': For JSON, shader, material files + * - 'binary': For binary formats + * - 'image': For textures + * - 'audio': For audio files */ - load( - path: string, - metadata: IAssetMetadata, - options?: IAssetLoadOptions - ): Promise>; + readonly contentType: AssetContentType; /** - * Validate if the loader can handle this asset - * 验证加载器是否可以处理此资产 + * Parse asset from content. + * 从内容解析资产。 + * + * @param content - File content. | 文件内容。 + * @param context - Parse context. | 解析上下文。 + * @returns Parsed asset. | 解析后的资产。 */ - canLoad(path: string, metadata: IAssetMetadata): boolean; + parse(content: IAssetContent, context: IAssetParseContext): Promise; /** - * Dispose loaded asset and free resources - * 释放已加载的资产并释放资源 + * Dispose loaded asset and free resources. + * 释放已加载的资产。 */ dispose(asset: T): void; } diff --git a/packages/asset-system/src/interfaces/IAssetManager.ts b/packages/asset-system/src/interfaces/IAssetManager.ts index e2545918..7523b796 100644 --- a/packages/asset-system/src/interfaces/IAssetManager.ts +++ b/packages/asset-system/src/interfaces/IAssetManager.ts @@ -69,6 +69,12 @@ export interface IAssetManager { */ getAssetByHandle(handle: AssetHandle): T | null; + /** + * Get loaded asset by path (synchronous) + * 通过路径获取已加载的资产(同步) + */ + getAssetByPath(path: string): T | null; + /** * Check if asset is loaded * 检查资产是否已加载 diff --git a/packages/asset-system/src/interfaces/IAssetReader.ts b/packages/asset-system/src/interfaces/IAssetReader.ts new file mode 100644 index 00000000..f8acafc8 --- /dev/null +++ b/packages/asset-system/src/interfaces/IAssetReader.ts @@ -0,0 +1,90 @@ +/** + * Asset Reader Interface + * 资产读取器接口 + * + * Provides unified file reading abstraction across different platforms. + * 提供跨平台的统一文件读取抽象。 + */ + +/** + * Asset content types. + * 资产内容类型。 + */ +export type AssetContentType = 'text' | 'binary' | 'image' | 'audio'; + +/** + * Asset content result. + * 资产内容结果。 + */ +export interface IAssetContent { + /** Content type. | 内容类型。 */ + type: AssetContentType; + /** Text content (for text/json files). | 文本内容。 */ + text?: string; + /** Binary content. | 二进制内容。 */ + binary?: ArrayBuffer; + /** Image element (for textures). | 图片元素。 */ + image?: HTMLImageElement; + /** Audio buffer (for audio files). | 音频缓冲区。 */ + audioBuffer?: AudioBuffer; +} + +/** + * Asset reader interface. + * 资产读取器接口。 + * + * Abstracts platform-specific file reading operations. + * 抽象平台特定的文件读取操作。 + */ +export interface IAssetReader { + /** + * Read file as text. + * 读取文件为文本。 + * + * @param absolutePath - Absolute file path. | 绝对文件路径。 + * @returns Text content. | 文本内容。 + */ + readText(absolutePath: string): Promise; + + /** + * Read file as binary. + * 读取文件为二进制。 + * + * @param absolutePath - Absolute file path. | 绝对文件路径。 + * @returns Binary content. | 二进制内容。 + */ + readBinary(absolutePath: string): Promise; + + /** + * Load image from file. + * 从文件加载图片。 + * + * @param absolutePath - Absolute file path. | 绝对文件路径。 + * @returns Image element. | 图片元素。 + */ + loadImage(absolutePath: string): Promise; + + /** + * Load audio from file. + * 从文件加载音频。 + * + * @param absolutePath - Absolute file path. | 绝对文件路径。 + * @returns Audio buffer. | 音频缓冲区。 + */ + loadAudio(absolutePath: string): Promise; + + /** + * Check if file exists. + * 检查文件是否存在。 + * + * @param absolutePath - Absolute file path. | 绝对文件路径。 + * @returns True if exists. | 是否存在。 + */ + exists(absolutePath: string): Promise; +} + +/** + * Service identifier for IAssetReader. + * IAssetReader 的服务标识符。 + */ +export const IAssetReaderService = Symbol.for('IAssetReaderService'); diff --git a/packages/asset-system/src/loaders/BinaryLoader.ts b/packages/asset-system/src/loaders/BinaryLoader.ts index 0d27af78..c7cd2c14 100644 --- a/packages/asset-system/src/loaders/BinaryLoader.ts +++ b/packages/asset-system/src/loaders/BinaryLoader.ts @@ -3,14 +3,9 @@ * 二进制资产加载器 */ -import { - AssetType, - IAssetLoadOptions, - IAssetMetadata, - IAssetLoadResult, - AssetLoadError -} from '../types/AssetTypes'; -import { IAssetLoader, IBinaryAsset } from '../interfaces/IAssetLoader'; +import { AssetType } from '../types/AssetTypes'; +import { IAssetLoader, IBinaryAsset, IAssetParseContext } from '../interfaces/IAssetLoader'; +import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader'; /** * Binary loader implementation @@ -22,144 +17,27 @@ export class BinaryLoader implements IAssetLoader { '.bin', '.dat', '.raw', '.bytes', '.wasm', '.so', '.dll', '.dylib' ]; + readonly contentType: AssetContentType = 'binary'; /** - * Load binary asset - * 加载二进制资产 + * Parse binary from content. + * 从内容解析二进制。 */ - async load( - path: string, - metadata: IAssetMetadata, - options?: IAssetLoadOptions - ): Promise> { - const startTime = performance.now(); - - try { - const response = await this.fetchWithTimeout(path, options?.timeout); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - // 获取MIME类型 / Get MIME type - const mimeType = response.headers.get('content-type') || undefined; - - // 获取总大小用于进度回调 / Get total size for progress callback - const contentLength = response.headers.get('content-length'); - const total = contentLength ? parseInt(contentLength, 10) : 0; - - // 读取响应 / Read response - let data: ArrayBuffer; - if (options?.onProgress && total > 0) { - data = await this.readResponseWithProgress(response, total, options.onProgress); - } else { - data = await response.arrayBuffer(); - } - - const asset: IBinaryAsset = { - data, - mimeType - }; - - return { - asset, - handle: 0, - metadata, - loadTime: performance.now() - startTime - }; - } catch (error) { - if (error instanceof Error) { - throw new AssetLoadError( - `Failed to load binary: ${error.message}`, - metadata.guid, - AssetType.Binary, - error - ); - } - throw AssetLoadError.fileNotFound(metadata.guid, path); + async parse(content: IAssetContent, _context: IAssetParseContext): Promise { + if (!content.binary) { + throw new Error('Binary content is empty'); } + + return { + data: content.binary + }; } - /** - * Fetch with timeout - * 带超时的fetch - */ - private async fetchWithTimeout(url: string, timeout = 30000): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - try { - const response = await fetch(url, { - signal: controller.signal, - mode: 'cors', - credentials: 'same-origin' - }); - return response; - } finally { - clearTimeout(timeoutId); - } - } - - /** - * Read response with progress - * 带进度读取响应 - */ - private async readResponseWithProgress( - response: Response, - total: number, - onProgress: (progress: number) => void - ): Promise { - const reader = response.body?.getReader(); - if (!reader) { - return response.arrayBuffer(); - } - - const chunks: Uint8Array[] = []; - let receivedLength = 0; - - while (true) { - const { done, value } = await reader.read(); - - if (done) break; - - chunks.push(value); - receivedLength += value.length; - - // 报告进度 / Report progress - onProgress(receivedLength / total); - } - - // 合并chunks到ArrayBuffer / Merge chunks into ArrayBuffer - const result = new Uint8Array(receivedLength); - let position = 0; - for (const chunk of chunks) { - result.set(chunk, position); - position += chunk.length; - } - - return result.buffer; - } - - /** - * Validate if the loader can handle this asset - * 验证加载器是否可以处理此资产 - */ - canLoad(path: string, _metadata: IAssetMetadata): boolean { - const ext = path.toLowerCase().substring(path.lastIndexOf('.')); - return this.supportedExtensions.includes(ext); - } - - /** - * Estimate memory usage for the asset - * 估算资产的内存使用量 - */ - /** * Dispose loaded asset * 释放已加载的资产 */ dispose(asset: IBinaryAsset): void { - // ArrayBuffer无法直接释放,但可以清空引用 / Can't directly release ArrayBuffer, but clear reference (asset as any).data = null; } } diff --git a/packages/asset-system/src/loaders/JsonLoader.ts b/packages/asset-system/src/loaders/JsonLoader.ts index 9935c3d9..8f404d03 100644 --- a/packages/asset-system/src/loaders/JsonLoader.ts +++ b/packages/asset-system/src/loaders/JsonLoader.ts @@ -3,14 +3,9 @@ * JSON资产加载器 */ -import { - AssetType, - IAssetLoadOptions, - IAssetMetadata, - IAssetLoadResult, - AssetLoadError -} from '../types/AssetTypes'; -import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader'; +import { AssetType } from '../types/AssetTypes'; +import { IAssetLoader, IJsonAsset, IAssetParseContext } from '../interfaces/IAssetLoader'; +import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader'; /** * JSON loader implementation @@ -19,144 +14,27 @@ import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader'; export class JsonLoader implements IAssetLoader { readonly supportedType = AssetType.Json; readonly supportedExtensions = ['.json', '.jsonc']; + readonly contentType: AssetContentType = 'text'; /** - * Load JSON asset - * 加载JSON资产 + * Parse JSON from text content. + * 从文本内容解析JSON。 */ - async load( - path: string, - metadata: IAssetMetadata, - options?: IAssetLoadOptions - ): Promise> { - const startTime = performance.now(); - - try { - const response = await this.fetchWithTimeout(path, options?.timeout); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - // 获取总大小用于进度回调 / Get total size for progress callback - const contentLength = response.headers.get('content-length'); - const total = contentLength ? parseInt(contentLength, 10) : 0; - - // 读取响应 / Read response - let jsonData: unknown; - if (options?.onProgress && total > 0) { - jsonData = await this.readResponseWithProgress(response, total, options.onProgress); - } else { - jsonData = await response.json(); - } - - const asset: IJsonAsset = { - data: jsonData - }; - - return { - asset, - handle: 0, - metadata, - loadTime: performance.now() - startTime - }; - } catch (error) { - if (error instanceof Error) { - throw new AssetLoadError( - `Failed to load JSON: ${error.message}`, - metadata.guid, - AssetType.Json, - error - ); - } - throw AssetLoadError.fileNotFound(metadata.guid, path); + async parse(content: IAssetContent, _context: IAssetParseContext): Promise { + if (!content.text) { + throw new Error('JSON content is empty'); } + + return { + data: JSON.parse(content.text) + }; } - /** - * Fetch with timeout - * 带超时的fetch - */ - private async fetchWithTimeout(url: string, timeout = 30000): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - try { - const response = await fetch(url, { - signal: controller.signal, - mode: 'cors', - credentials: 'same-origin' - }); - return response; - } finally { - clearTimeout(timeoutId); - } - } - - /** - * Read response with progress - * 带进度读取响应 - */ - private async readResponseWithProgress( - response: Response, - total: number, - onProgress: (progress: number) => void - ): Promise { - const reader = response.body?.getReader(); - if (!reader) { - return response.json(); - } - - const chunks: Uint8Array[] = []; - let receivedLength = 0; - - while (true) { - const { done, value } = await reader.read(); - - if (done) break; - - chunks.push(value); - receivedLength += value.length; - - // 报告进度 / Report progress - onProgress(receivedLength / total); - } - - // 合并chunks / Merge chunks - const allChunks = new Uint8Array(receivedLength); - let position = 0; - for (const chunk of chunks) { - allChunks.set(chunk, position); - position += chunk.length; - } - - // 解码为字符串并解析JSON / Decode to string and parse JSON - const decoder = new TextDecoder('utf-8'); - const jsonString = decoder.decode(allChunks); - return JSON.parse(jsonString); - } - - /** - * Validate if the loader can handle this asset - * 验证加载器是否可以处理此资产 - */ - canLoad(path: string, _metadata: IAssetMetadata): boolean { - const ext = path.toLowerCase().substring(path.lastIndexOf('.')); - return this.supportedExtensions.includes(ext); - } - - /** - * Estimate memory usage for the asset - * 估算资产的内存使用量 - */ - /** * Dispose loaded asset * 释放已加载的资产 */ dispose(asset: IJsonAsset): void { - // JSON资产通常不需要特殊清理 / JSON assets usually don't need special cleanup - // 但可以清空引用以帮助GC / But can clear references to help GC (asset as any).data = null; } } diff --git a/packages/asset-system/src/loaders/TextLoader.ts b/packages/asset-system/src/loaders/TextLoader.ts index 9d4f11bd..4191d9a9 100644 --- a/packages/asset-system/src/loaders/TextLoader.ts +++ b/packages/asset-system/src/loaders/TextLoader.ts @@ -3,14 +3,9 @@ * 文本资产加载器 */ -import { - AssetType, - IAssetLoadOptions, - IAssetMetadata, - IAssetLoadResult, - AssetLoadError -} from '../types/AssetTypes'; -import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader'; +import { AssetType } from '../types/AssetTypes'; +import { IAssetLoader, ITextAsset, IAssetParseContext } from '../interfaces/IAssetLoader'; +import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader'; /** * Text loader implementation @@ -19,115 +14,21 @@ import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader'; export class TextLoader implements IAssetLoader { readonly supportedType = AssetType.Text; readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts']; + readonly contentType: AssetContentType = 'text'; /** - * Load text asset - * 加载文本资产 + * Parse text from content. + * 从内容解析文本。 */ - async load( - path: string, - metadata: IAssetMetadata, - options?: IAssetLoadOptions - ): Promise> { - const startTime = performance.now(); - - try { - const response = await this.fetchWithTimeout(path, options?.timeout); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - // 获取总大小用于进度回调 / Get total size for progress callback - const contentLength = response.headers.get('content-length'); - const total = contentLength ? parseInt(contentLength, 10) : 0; - - // 读取响应 / Read response - let content: string; - if (options?.onProgress && total > 0) { - content = await this.readResponseWithProgress(response, total, options.onProgress); - } else { - content = await response.text(); - } - - // 检测编码 / Detect encoding - const encoding = this.detectEncoding(content); - - const asset: ITextAsset = { - content, - encoding - }; - - return { - asset, - handle: 0, - metadata, - loadTime: performance.now() - startTime - }; - } catch (error) { - if (error instanceof Error) { - throw new AssetLoadError( - `Failed to load text: ${error.message}`, - metadata.guid, - AssetType.Text, - error - ); - } - throw AssetLoadError.fileNotFound(metadata.guid, path); - } - } - - /** - * Fetch with timeout - * 带超时的fetch - */ - private async fetchWithTimeout(url: string, timeout = 30000): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - try { - const response = await fetch(url, { - signal: controller.signal, - mode: 'cors', - credentials: 'same-origin' - }); - return response; - } finally { - clearTimeout(timeoutId); - } - } - - /** - * Read response with progress - * 带进度读取响应 - */ - private async readResponseWithProgress( - response: Response, - total: number, - onProgress: (progress: number) => void - ): Promise { - const reader = response.body?.getReader(); - if (!reader) { - return response.text(); + async parse(content: IAssetContent, _context: IAssetParseContext): Promise { + if (!content.text) { + throw new Error('Text content is empty'); } - const decoder = new TextDecoder('utf-8'); - let result = ''; - let receivedLength = 0; - - while (true) { - const { done, value } = await reader.read(); - - if (done) break; - - receivedLength += value.length; - result += decoder.decode(value, { stream: true }); - - // 报告进度 / Report progress - onProgress(receivedLength / total); - } - - return result; + return { + content: content.text, + encoding: this.detectEncoding(content.text) + }; } /** @@ -135,38 +36,20 @@ export class TextLoader implements IAssetLoader { * 检测文本编码 */ private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' { - // 简单的编码检测 / Simple encoding detection - // 检查是否包含非ASCII字符 / Check for non-ASCII characters for (let i = 0; i < content.length; i++) { const charCode = content.charCodeAt(i); if (charCode > 127) { - // 包含非ASCII字符,可能是UTF-8或UTF-16 / Contains non-ASCII, likely UTF-8 or UTF-16 return charCode > 255 ? 'utf16' : 'utf8'; } } return 'ascii'; } - /** - * Validate if the loader can handle this asset - * 验证加载器是否可以处理此资产 - */ - canLoad(path: string, _metadata: IAssetMetadata): boolean { - const ext = path.toLowerCase().substring(path.lastIndexOf('.')); - return this.supportedExtensions.includes(ext); - } - - /** - * Estimate memory usage for the asset - * 估算资产的内存使用量 - */ - /** * Dispose loaded asset * 释放已加载的资产 */ dispose(asset: ITextAsset): void { - // 清空内容以帮助GC / Clear content to help GC (asset as any).content = ''; } } diff --git a/packages/asset-system/src/loaders/TextureLoader.ts b/packages/asset-system/src/loaders/TextureLoader.ts index 1894dfbf..6d18d42e 100644 --- a/packages/asset-system/src/loaders/TextureLoader.ts +++ b/packages/asset-system/src/loaders/TextureLoader.ts @@ -3,14 +3,9 @@ * 纹理资产加载器 */ -import { - AssetType, - IAssetLoadOptions, - IAssetMetadata, - IAssetLoadResult, - AssetLoadError -} from '../types/AssetTypes'; -import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader'; +import { AssetType } from '../types/AssetTypes'; +import { IAssetLoader, ITextureAsset, IAssetParseContext } from '../interfaces/IAssetLoader'; +import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader'; /** * Texture loader implementation @@ -19,147 +14,36 @@ import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader'; export class TextureLoader implements IAssetLoader { readonly supportedType = AssetType.Texture; readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']; + readonly contentType: AssetContentType = 'image'; private static _nextTextureId = 1; - private readonly _loadedTextures = new Map(); /** - * Load texture asset - * 加载纹理资产 + * Parse texture from image content. + * 从图片内容解析纹理。 */ - async load( - path: string, - metadata: IAssetMetadata, - options?: IAssetLoadOptions - ): Promise> { - const startTime = performance.now(); - - // 检查缓存 / Check cache - if (!options?.forceReload && this._loadedTextures.has(path)) { - const cached = this._loadedTextures.get(path)!; - return { - asset: cached, - handle: cached.textureId, - metadata, - loadTime: 0 - }; + async parse(content: IAssetContent, context: IAssetParseContext): Promise { + if (!content.image) { + throw new Error('Texture content is empty'); } - try { - // 创建图像元素 / Create image element - const image = await this.loadImage(path, options); + const image = content.image; - // 创建纹理资产 / Create texture asset - const textureAsset: ITextureAsset = { - textureId: TextureLoader._nextTextureId++, - width: image.width, - height: image.height, - format: 'rgba', // 默认格式 / Default format - hasMipmaps: false, - data: image - }; + const textureAsset: ITextureAsset = { + textureId: TextureLoader._nextTextureId++, + width: image.width, + height: image.height, + format: 'rgba', + hasMipmaps: false, + data: image + }; - // 缓存纹理 / Cache texture - this._loadedTextures.set(path, textureAsset); - - // 触发引擎纹理加载(如果有引擎桥接) / Trigger engine texture loading if bridge exists - if (typeof window !== 'undefined' && (window as any).engineBridge) { - await this.uploadToGPU(textureAsset, path); - } - - return { - asset: textureAsset, - handle: textureAsset.textureId, - metadata, - loadTime: performance.now() - startTime - }; - } catch (error) { - throw AssetLoadError.fileNotFound(metadata.guid, path); - } - } - - /** - * Load image from URL - * 从URL加载图像 - */ - private async loadImage(url: string, options?: IAssetLoadOptions): Promise { - // For Tauri asset URLs, use fetch to load the image - // 对于Tauri资产URL,使用fetch加载图像 - if (url.startsWith('http://asset.localhost/') || url.startsWith('asset://')) { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch image: ${response.statusText}`); - } - const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); - - return new Promise((resolve, reject) => { - const image = new Image(); - image.onload = () => { - // Clean up blob URL after loading - // 加载后清理blob URL - URL.revokeObjectURL(blobUrl); - resolve(image); - }; - image.onerror = () => { - URL.revokeObjectURL(blobUrl); - reject(new Error(`Failed to load image from blob: ${url}`)); - }; - image.src = blobUrl; - }); - } catch (error) { - throw new Error(`Failed to load Tauri asset: ${url} - ${error}`); - } + // Upload to GPU if bridge exists. + if (typeof window !== 'undefined' && (window as any).engineBridge) { + await this.uploadToGPU(textureAsset, context.metadata.path); } - // For regular URLs, use standard Image loading - // 对于常规URL,使用标准Image加载 - return new Promise((resolve, reject) => { - const image = new Image(); - image.crossOrigin = 'anonymous'; - - // 超时处理 / Timeout handling - const timeout = options?.timeout || 30000; - const timeoutId = setTimeout(() => { - reject(new Error(`Image load timeout: ${url}`)); - }, timeout); - - // 进度回调 / Progress callback - if (options?.onProgress) { - // 图像加载没有真正的进度事件,模拟进度 / Images don't have real progress events, simulate - let progress = 0; - const progressInterval = setInterval(() => { - progress = Math.min(progress + 0.1, 0.9); - options.onProgress!(progress); - }, 100); - - image.onload = () => { - clearInterval(progressInterval); - clearTimeout(timeoutId); - options.onProgress!(1); - resolve(image); - }; - - image.onerror = () => { - clearInterval(progressInterval); - clearTimeout(timeoutId); - reject(new Error(`Failed to load image: ${url}`)); - }; - } else { - image.onload = () => { - clearTimeout(timeoutId); - resolve(image); - }; - - image.onerror = () => { - clearTimeout(timeoutId); - reject(new Error(`Failed to load image: ${url}`)); - }; - } - - image.src = url; - }); + return textureAsset; } /** @@ -173,34 +57,12 @@ export class TextureLoader implements IAssetLoader { } } - /** - * Validate if the loader can handle this asset - * 验证加载器是否可以处理此资产 - */ - canLoad(path: string, _metadata: IAssetMetadata): boolean { - const ext = path.toLowerCase().substring(path.lastIndexOf('.')); - return this.supportedExtensions.includes(ext); - } - - /** - * Estimate memory usage for the asset - * 估算资产的内存使用量 - */ - /** * Dispose loaded asset * 释放已加载的资产 */ dispose(asset: ITextureAsset): void { - // 从缓存中移除 / Remove from cache - for (const [path, cached] of this._loadedTextures.entries()) { - if (cached === asset) { - this._loadedTextures.delete(path); - break; - } - } - - // 释放GPU资源 / Release GPU resources + // Release GPU resources. if (typeof window !== 'undefined' && (window as any).engineBridge) { const bridge = (window as any).engineBridge; if (bridge.unloadTexture) { @@ -208,7 +70,7 @@ export class TextureLoader implements IAssetLoader { } } - // 清理图像数据 / Clean up image data + // Clean up image data. if (asset.data instanceof HTMLImageElement) { asset.data.src = ''; } diff --git a/packages/asset-system/src/runtime/RuntimeCatalog.ts b/packages/asset-system/src/runtime/RuntimeCatalog.ts new file mode 100644 index 00000000..0f17bb6e --- /dev/null +++ b/packages/asset-system/src/runtime/RuntimeCatalog.ts @@ -0,0 +1,275 @@ +/** + * Runtime Catalog for Asset Resolution + * 资产解析的运行时目录 + * + * Provides GUID-based asset lookup at runtime. + * 提供运行时基于 GUID 的资产查找。 + */ + +import { AssetGUID, AssetType } from '../types/AssetTypes'; +import { + IRuntimeCatalog, + IRuntimeAssetLocation, + IRuntimeBundleInfo +} from '../bundle/BundleFormat'; + +/** + * Runtime Catalog Manager + * 运行时目录管理器 + * + * Loads and manages the asset catalog for runtime GUID resolution. + */ +export class RuntimeCatalog { + private _catalog: IRuntimeCatalog | null = null; + private _loadedBundles = new Map(); + private _loadingBundles = new Map>(); + private _baseUrl: string = './'; + + /** + * Set base URL for loading catalog and bundles + * 设置加载目录和包的基础 URL + */ + setBaseUrl(url: string): void { + this._baseUrl = url.endsWith('/') ? url : `${url}/`; + } + + /** + * Load catalog from URL + * 从 URL 加载目录 + */ + async loadCatalog(catalogUrl?: string): Promise { + const url = catalogUrl || `${this._baseUrl}asset-catalog.json`; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to load catalog: ${response.status}`); + } + + const data = await response.json(); + this._catalog = this._parseCatalog(data); + + console.log(`[RuntimeCatalog] Loaded catalog with ${Object.keys(this._catalog.assets).length} assets`); + } catch (error) { + console.error('[RuntimeCatalog] Failed to load catalog:', error); + throw error; + } + } + + /** + * Initialize with pre-loaded catalog data + * 使用预加载的目录数据初始化 + */ + initWithData(catalogData: IRuntimeCatalog): void { + this._catalog = catalogData; + } + + /** + * Check if catalog is loaded + * 检查目录是否已加载 + */ + isLoaded(): boolean { + return this._catalog !== null; + } + + /** + * Get asset location by GUID + * 根据 GUID 获取资产位置 + */ + getAssetLocation(guid: AssetGUID): IRuntimeAssetLocation | null { + if (!this._catalog) { + console.warn('[RuntimeCatalog] Catalog not loaded'); + return null; + } + + return this._catalog.assets[guid] || null; + } + + /** + * Check if asset exists in catalog + * 检查资产是否存在于目录中 + */ + hasAsset(guid: AssetGUID): boolean { + return this._catalog?.assets[guid] !== undefined; + } + + /** + * Get all assets of a specific type + * 获取特定类型的所有资产 + */ + getAssetsByType(type: AssetType): AssetGUID[] { + if (!this._catalog) return []; + + return Object.entries(this._catalog.assets) + .filter(([_, loc]) => loc.type === type) + .map(([guid]) => guid); + } + + /** + * Get bundle info + * 获取包信息 + */ + getBundleInfo(bundleName: string): IRuntimeBundleInfo | null { + return this._catalog?.bundles[bundleName] || null; + } + + /** + * Load a bundle + * 加载包 + */ + async loadBundle(bundleName: string): Promise { + // Return cached bundle + const cached = this._loadedBundles.get(bundleName); + if (cached) { + return cached; + } + + // Return pending load + const pending = this._loadingBundles.get(bundleName); + if (pending) { + return pending; + } + + // Start new load + const bundleInfo = this.getBundleInfo(bundleName); + if (!bundleInfo) { + throw new Error(`Bundle not found in catalog: ${bundleName}`); + } + + const loadPromise = this._fetchBundle(bundleInfo); + this._loadingBundles.set(bundleName, loadPromise); + + try { + const data = await loadPromise; + this._loadedBundles.set(bundleName, data); + return data; + } finally { + this._loadingBundles.delete(bundleName); + } + } + + /** + * Load asset data by GUID + * 根据 GUID 加载资产数据 + */ + async loadAssetData(guid: AssetGUID): Promise { + const location = this.getAssetLocation(guid); + if (!location) { + throw new Error(`Asset not found in catalog: ${guid}`); + } + + // Load the bundle containing this asset + const bundleData = await this.loadBundle(location.bundle); + + // Extract asset data from bundle + return bundleData.slice(location.offset, location.offset + location.size); + } + + /** + * Preload bundles marked for preloading + * 预加载标记为预加载的包 + */ + async preloadBundles(): Promise { + if (!this._catalog) return; + + const preloadPromises: Promise[] = []; + + for (const [name, info] of Object.entries(this._catalog.bundles)) { + if (info.preload) { + preloadPromises.push( + this.loadBundle(name).then(() => { + console.log(`[RuntimeCatalog] Preloaded bundle: ${name}`); + }) + ); + } + } + + await Promise.all(preloadPromises); + } + + /** + * Unload a bundle from memory + * 从内存卸载包 + */ + unloadBundle(bundleName: string): void { + this._loadedBundles.delete(bundleName); + } + + /** + * Clear all loaded bundles + * 清除所有已加载的包 + */ + clearBundles(): void { + this._loadedBundles.clear(); + } + + /** + * Get catalog statistics + * 获取目录统计信息 + */ + getStatistics(): { + totalAssets: number; + totalBundles: number; + loadedBundles: number; + assetsByType: Record; + } { + if (!this._catalog) { + return { + totalAssets: 0, + totalBundles: 0, + loadedBundles: 0, + assetsByType: {} + }; + } + + const assetsByType: Record = {}; + for (const loc of Object.values(this._catalog.assets)) { + assetsByType[loc.type] = (assetsByType[loc.type] || 0) + 1; + } + + return { + totalAssets: Object.keys(this._catalog.assets).length, + totalBundles: Object.keys(this._catalog.bundles).length, + loadedBundles: this._loadedBundles.size, + assetsByType + }; + } + + /** + * Parse catalog JSON to typed structure + * 将目录 JSON 解析为类型化结构 + */ + private _parseCatalog(data: unknown): IRuntimeCatalog { + const raw = data as Record; + + return { + version: (raw.version as string) || '1.0', + createdAt: (raw.createdAt as number) || Date.now(), + bundles: (raw.bundles as Record) || {}, + assets: (raw.assets as Record) || {} + }; + } + + /** + * Fetch bundle data + * 获取包数据 + */ + private async _fetchBundle(info: IRuntimeBundleInfo): Promise { + const url = info.url.startsWith('http') + ? info.url + : `${this._baseUrl}${info.url}`; + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to load bundle: ${url} (${response.status})`); + } + + return response.arrayBuffer(); + } +} + +/** + * Global runtime catalog instance + * 全局运行时目录实例 + */ +export const runtimeCatalog = new RuntimeCatalog(); diff --git a/packages/asset-system/src/utils/PathValidator.ts b/packages/asset-system/src/utils/PathValidator.ts index a837a818..29b07c4a 100644 --- a/packages/asset-system/src/utils/PathValidator.ts +++ b/packages/asset-system/src/utils/PathValidator.ts @@ -6,29 +6,77 @@ * 验证并清理资产路径以确保安全 */ +/** + * Path validation options. + * 路径验证选项。 + */ +export interface PathValidationOptions { + /** Allow absolute paths (for editor environment). | 允许绝对路径(用于编辑器环境)。 */ + allowAbsolutePaths?: boolean; + /** Allow URLs (http://, https://, asset://). | 允许 URL。 */ + allowUrls?: boolean; +} + export class PathValidator { - // Dangerous path patterns - private static readonly DANGEROUS_PATTERNS = [ + // Dangerous path patterns (without absolute path checks) + private static readonly DANGEROUS_PATTERNS_STRICT = [ /\.\.[/\\]/g, // Path traversal attempts (..) /^[/\\]/, // Absolute paths on Unix /^[a-zA-Z]:[/\\]/, // Absolute paths on Windows - /[<>:"|?*]/, // Invalid characters for Windows paths /\0/, // Null bytes /%00/, // URL encoded null bytes /\.\.%2[fF]/ // URL encoded path traversal ]; - // Valid path characters (alphanumeric, dash, underscore, dot, slash) + // Dangerous path patterns (allowing absolute paths) + private static readonly DANGEROUS_PATTERNS_RELAXED = [ + /\.\.[/\\]/g, // Path traversal attempts (..) + /\0/, // Null bytes + /%00/, // URL encoded null bytes + /\.\.%2[fF]/ // URL encoded path traversal + ]; + + // Valid path characters for relative paths (alphanumeric, dash, underscore, dot, slash) private static readonly VALID_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@]+$/; + // Valid path characters for absolute paths (includes colon for Windows drives) + private static readonly VALID_ABSOLUTE_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@:]+$/; + + // URL pattern + private static readonly URL_REGEX = /^(https?|asset|blob|data):\/\//; + // Maximum path length - private static readonly MAX_PATH_LENGTH = 260; + private static readonly MAX_PATH_LENGTH = 1024; + + /** Global options for path validation. | 路径验证的全局选项。 */ + private static _globalOptions: PathValidationOptions = { + allowAbsolutePaths: false, + allowUrls: true + }; + + /** + * Set global validation options. + * 设置全局验证选项。 + */ + static setGlobalOptions(options: PathValidationOptions): void { + this._globalOptions = { ...this._globalOptions, ...options }; + } + + /** + * Get current global options. + * 获取当前全局选项。 + */ + static getGlobalOptions(): PathValidationOptions { + return { ...this._globalOptions }; + } /** * Validate if a path is safe * 验证路径是否安全 */ - static validate(path: string): { valid: boolean; reason?: string } { + static validate(path: string, options?: PathValidationOptions): { valid: boolean; reason?: string } { + const opts = { ...this._globalOptions, ...options }; + // Check for null/undefined/empty if (!path || typeof path !== 'string') { return { valid: false, reason: 'Path is empty or invalid' }; @@ -39,15 +87,29 @@ export class PathValidator { return { valid: false, reason: `Path exceeds maximum length of ${this.MAX_PATH_LENGTH} characters` }; } + // Allow URLs if enabled + if (opts.allowUrls && this.URL_REGEX.test(path)) { + return { valid: true }; + } + + // Choose patterns based on options + const patterns = opts.allowAbsolutePaths + ? this.DANGEROUS_PATTERNS_RELAXED + : this.DANGEROUS_PATTERNS_STRICT; + // Check for dangerous patterns - for (const pattern of this.DANGEROUS_PATTERNS) { + for (const pattern of patterns) { if (pattern.test(path)) { return { valid: false, reason: 'Path contains dangerous pattern' }; } } // Check for valid characters - if (!this.VALID_PATH_REGEX.test(path)) { + const validCharsRegex = opts.allowAbsolutePaths + ? this.VALID_ABSOLUTE_PATH_REGEX + : this.VALID_PATH_REGEX; + + if (!validCharsRegex.test(path)) { return { valid: false, reason: 'Path contains invalid characters' }; }