UNPKG

unpak.js

Version:

Modern TypeScript library for reading Unreal Engine pak files and assets, inspired by CUE4Parse

574 lines 20.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EnhancedAssetRegistry = void 0; const AssetRegistry_1 = require("./AssetRegistry"); /** * Enhanced Asset Registry with advanced features */ class EnhancedAssetRegistry extends AssetRegistry_1.AssetRegistry { dependencyCache = new Map(); assetsByClass = new Map(); assetsByPackage = new Map(); initialized = false; /** * Initialize enhanced features and build caches */ initialize() { if (this.initialized) return; this.buildClassIndex(); this.buildPackageIndex(); this.buildDependencyMaps(); this.initialized = true; } /** * Build index of assets by class name */ buildClassIndex() { this.assetsByClass.clear(); for (const asset of this.preallocatedAssetDataBuffer) { const className = asset.assetClass.text; if (!this.assetsByClass.has(className)) { this.assetsByClass.set(className, []); } this.assetsByClass.get(className).push(asset); } } /** * Build index of assets by package name */ buildPackageIndex() { this.assetsByPackage.clear(); for (const asset of this.preallocatedAssetDataBuffer) { const packageName = asset.packageName.text; if (!this.assetsByPackage.has(packageName)) { this.assetsByPackage.set(packageName, []); } this.assetsByPackage.get(packageName).push(asset); } } /** * Build comprehensive dependency maps for all assets */ buildDependencyMaps() { this.dependencyCache.clear(); // First pass: build direct dependency maps for (const asset of this.preallocatedAssetDataBuffer) { const assetId = this.getAssetId(asset); const dependencyMap = { direct: [], transitive: [], dependents: [], circular: [] }; // Find the depends node for this asset const dependsNode = this.findDependsNode(asset); if (dependsNode) { dependencyMap.direct = this.extractDirectDependencies(dependsNode); } this.dependencyCache.set(assetId, dependencyMap); } // Second pass: build reverse dependencies for (const [assetId, depMap] of this.dependencyCache) { for (const dependency of depMap.direct) { const depDependencyMap = this.dependencyCache.get(dependency); if (depDependencyMap) { depDependencyMap.dependents.push(assetId); } } } // Third pass: compute transitive dependencies and detect cycles for (const [assetId] of this.dependencyCache) { this.computeTransitiveDependencies(assetId); } } /** * Get unique identifier for an asset */ getAssetId(asset) { return `${asset.packageName.text}:${asset.assetName.text}`; } /** * Find depends node for an asset */ findDependsNode(asset) { // This would need to be implemented based on the actual relationship // between FAssetData and FDependsNode in the registry structure return null; // Placeholder } /** * Extract direct dependencies from a depends node */ extractDirectDependencies(node) { // This would extract the actual dependencies from the node return []; // Placeholder } /** * Compute transitive dependencies using DFS */ computeTransitiveDependencies(assetId) { const visited = new Set(); const stack = new Set(); const transitive = new Set(); const circular = []; const dfs = (currentId) => { if (stack.has(currentId)) { circular.push(currentId); return; } if (visited.has(currentId)) { return; } visited.add(currentId); stack.add(currentId); const depMap = this.dependencyCache.get(currentId); if (depMap) { for (const dependency of depMap.direct) { transitive.add(dependency); dfs(dependency); } } stack.delete(currentId); }; dfs(assetId); const depMap = this.dependencyCache.get(assetId); if (depMap) { depMap.transitive = Array.from(transitive); depMap.circular = circular; } } /** * Search assets with advanced filtering */ searchAssets(query, filter) { if (!this.initialized) { this.initialize(); } const results = []; const queryLower = query.toLowerCase(); for (const asset of this.preallocatedAssetDataBuffer) { let score = 0; const matchReasons = []; // Apply filters first if (filter && !this.matchesFilter(asset, filter)) { continue; } // Text search scoring const assetName = asset.assetName.text.toLowerCase(); const packageName = asset.packageName.text.toLowerCase(); const className = asset.assetClass.text.toLowerCase(); // Exact matches get highest score if (assetName === queryLower) { score += 1.0; matchReasons.push("Exact asset name match"); } else if (assetName.includes(queryLower)) { score += 0.8; matchReasons.push("Partial asset name match"); } if (className === queryLower) { score += 0.6; matchReasons.push("Exact class name match"); } else if (className.includes(queryLower)) { score += 0.4; matchReasons.push("Partial class name match"); } if (packageName.includes(queryLower)) { score += 0.3; matchReasons.push("Package name match"); } // Only include results with some score if (score > 0) { results.push({ asset, score, matchReasons }); } } // Sort by score descending return results.sort((a, b) => b.score - a.score); } /** * Check if an asset matches the given filter */ matchesFilter(asset, filter) { if (filter.className && asset.assetClass.text !== filter.className) { return false; } if (filter.packageName && !asset.packageName.text.includes(filter.packageName)) { return false; } if (filter.assetName && !asset.assetName.text.includes(filter.assetName)) { return false; } if (filter.hasDependency) { const assetId = this.getAssetId(asset); const depMap = this.dependencyCache.get(assetId); if (!depMap || !depMap.direct.includes(filter.hasDependency)) { return false; } } if (filter.hasDependents !== undefined) { const assetId = this.getAssetId(asset); const depMap = this.dependencyCache.get(assetId); const hasDependents = depMap && depMap.dependents.length > 0; if (filter.hasDependents !== hasDependents) { return false; } } return true; } /** * Get assets by class name */ getAssetsByClass(className) { if (!this.initialized) { this.initialize(); } return this.assetsByClass.get(className) || []; } /** * Get assets by package name */ getAssetsByPackage(packageName) { if (!this.initialized) { this.initialize(); } return this.assetsByPackage.get(packageName) || []; } /** * Get dependency map for an asset */ getDependencyMap(asset) { if (!this.initialized) { this.initialize(); } const assetId = this.getAssetId(asset); return this.dependencyCache.get(assetId) || null; } /** * Find all assets that depend on the given asset */ findDependents(asset) { const dependencyMap = this.getDependencyMap(asset); if (!dependencyMap) { return []; } const dependents = []; for (const dependentId of dependencyMap.dependents) { const dependent = this.findAssetById(dependentId); if (dependent) { dependents.push(dependent); } } return dependents; } /** * Find all dependencies of the given asset */ findDependencies(asset, includeTransitive = false) { const dependencyMap = this.getDependencyMap(asset); if (!dependencyMap) { return []; } const dependencies = []; const dependencyIds = includeTransitive ? [...dependencyMap.direct, ...dependencyMap.transitive] : dependencyMap.direct; for (const dependencyId of dependencyIds) { const dependency = this.findAssetById(dependencyId); if (dependency) { dependencies.push(dependency); } } return dependencies; } /** * Find an asset by its ID */ findAssetById(assetId) { const [packageName, assetName] = assetId.split(':'); return this.preallocatedAssetDataBuffer.find(asset => asset.packageName.text === packageName && asset.assetName.text === assetName) || null; } /** * Get statistics about the registry */ getStatistics() { if (!this.initialized) { this.initialize(); } const assetsByClass = {}; for (const [className, assets] of this.assetsByClass) { assetsByClass[className] = assets.length; } let totalDependencies = 0; let maxDependencies = 0; let circularCount = 0; for (const depMap of this.dependencyCache.values()) { totalDependencies += depMap.direct.length; maxDependencies = Math.max(maxDependencies, depMap.direct.length); if (depMap.circular.length > 0) { circularCount++; } } ; return { totalAssets: this.preallocatedAssetDataBuffer.length, assetsByClass, packagesCount: this.assetsByPackage.size, dependencyStats: { averageDependencies: this.preallocatedAssetDataBuffer.length > 0 ? totalDependencies / this.preallocatedAssetDataBuffer.length : 0, maxDependencies, circularDependencies: circularCount } }; } /** * Asset Bundle Information Support * Track and manage asset bundles for streaming */ getAssetBundles() { if (!this.initialized) { this.initialize(); } const bundles = new Map(); // Group assets by their packages into logical bundles for (const [packageName, assets] of this.assetsByPackage) { const bundle = { id: packageName, name: packageName.split('/').pop() || packageName, assets: assets.map(asset => this.getAssetId(asset)), sizeEstimate: this.estimateBundleSize(assets), dependencies: this.getBundleDependencies(assets), streamingLevel: this.getStreamingLevel(packageName), isCore: this.isCoreBundle(packageName), platform: this.detectBundlePlatform(packageName) }; bundles.set(packageName, bundle); } return bundles; } /** * Streaming Level Registry Support * Manage level-specific asset collections */ getStreamingLevels() { if (!this.initialized) { this.initialize(); } const streamingLevels = new Map(); // Find level assets const levelAssets = this.getAssetsByClass('Level') || []; const worldAssets = this.getAssetsByClass('World') || []; const mapAssets = [...levelAssets, ...worldAssets]; for (const mapAsset of mapAssets) { const levelId = this.getAssetId(mapAsset); const dependencies = this.findDependencies(mapAsset, true); const streamingLevel = { id: levelId, name: mapAsset.assetName.text, packageName: mapAsset.packageName.text, worldAssets: dependencies.filter(dep => dep.assetClass.text === 'World').map(dep => this.getAssetId(dep)), staticMeshes: dependencies.filter(dep => dep.assetClass.text === 'StaticMesh').map(dep => this.getAssetId(dep)), materials: dependencies.filter(dep => dep.assetClass.text.includes('Material')).map(dep => this.getAssetId(dep)), textures: dependencies.filter(dep => dep.assetClass.text === 'Texture2D').map(dep => this.getAssetId(dep)), sounds: dependencies.filter(dep => dep.assetClass.text.includes('Sound')).map(dep => this.getAssetId(dep)), loadPriority: this.determineLoadPriority(mapAsset), estimatedMemoryUsage: this.estimateMemoryUsage(dependencies), streamingDistance: this.getStreamingDistance(mapAsset) }; streamingLevels.set(levelId, streamingLevel); } return streamingLevels; } /** * Plugin Asset Registry Support * Track assets from plugins separately */ getPluginAssets() { if (!this.initialized) { this.initialize(); } const pluginAssets = new Map(); for (const asset of this.preallocatedAssetDataBuffer) { const pluginInfo = this.extractPluginInfo(asset); if (pluginInfo) { if (!pluginAssets.has(pluginInfo.name)) { pluginAssets.set(pluginInfo.name, { pluginName: pluginInfo.name, version: pluginInfo.version, assets: [], dependencies: new Set(), isEnabled: pluginInfo.enabled, mountPath: pluginInfo.mountPath }); } const collection = pluginAssets.get(pluginInfo.name); collection.assets.push(this.getAssetId(asset)); // Add plugin dependencies const deps = this.findDependencies(asset); deps.forEach(dep => { const depPluginInfo = this.extractPluginInfo(dep); if (depPluginInfo && depPluginInfo.name !== pluginInfo.name) { collection.dependencies.add(depPluginInfo.name); } }); } } return pluginAssets; } /** * Custom Asset Registry Format Support * Handle custom registry formats beyond the standard AssetRegistry.bin */ loadCustomRegistryFormat(format, data) { try { switch (format) { case 'JSON': return this.loadJSONRegistry(data); case 'XML': return this.loadXMLRegistry(data); case 'Binary': return this.loadBinaryRegistry(data); default: console.warn(`Unsupported registry format: ${format}`); return false; } } catch (error) { console.error(`Failed to load custom registry format ${format}: ${error}`); return false; } } // Private helper methods for bundle information estimateBundleSize(assets) { // Estimate based on asset types and typical sizes let totalSize = 0; for (const asset of assets) { const className = asset.assetClass.text; switch (className) { case 'Texture2D': totalSize += 2 * 1024 * 1024; // ~2MB per texture break; case 'StaticMesh': totalSize += 1024 * 1024; // ~1MB per mesh break; case 'Material': totalSize += 512 * 1024; // ~512KB per material break; case 'SoundWave': totalSize += 5 * 1024 * 1024; // ~5MB per sound break; default: totalSize += 256 * 1024; // ~256KB default } } return totalSize; } getBundleDependencies(assets) { const dependencies = new Set(); for (const asset of assets) { const deps = this.findDependencies(asset); deps.forEach(dep => { const depPackage = dep.packageName.text; dependencies.add(depPackage); }); } return Array.from(dependencies); } getStreamingLevel(packageName) { // Determine streaming level based on package path if (packageName.includes('/Core/') || packageName.includes('/Engine/')) { return 0; // Core assets } else if (packageName.includes('/Content/')) { return 1; // Game content } else if (packageName.includes('/Plugins/')) { return 2; // Plugin content } return 3; // Other content } isCoreBundle(packageName) { return packageName.includes('/Engine/') || packageName.includes('/Core/') || packageName.startsWith('Engine'); } detectBundlePlatform(packageName) { if (packageName.includes('_PC') || packageName.includes('/PC/')) return 'PC'; if (packageName.includes('_Console') || packageName.includes('/Console/')) return 'Console'; if (packageName.includes('_Mobile') || packageName.includes('/Mobile/')) return 'Mobile'; return 'All'; } // Helper methods for streaming levels determineLoadPriority(mapAsset) { const assetName = mapAsset.assetName.text.toLowerCase(); if (assetName.includes('main') || assetName.includes('persistent')) { return 0; // Highest priority } else if (assetName.includes('menu') || assetName.includes('ui')) { return 1; // High priority } else if (assetName.includes('lobby') || assetName.includes('hub')) { return 2; // Medium priority } return 3; // Default priority } estimateMemoryUsage(dependencies) { return this.estimateBundleSize(dependencies); } getStreamingDistance(mapAsset) { // Default streaming distance based on asset type const assetName = mapAsset.assetName.text.toLowerCase(); if (assetName.includes('large') || assetName.includes('open')) { return 50000; // Large open world } else if (assetName.includes('medium')) { return 25000; // Medium sized level } return 10000; // Default distance } // Helper methods for plugin assets extractPluginInfo(asset) { const packagePath = asset.packageName.text; // Check if asset is from a plugin const pluginMatch = packagePath.match(/\/Plugins\/([^\/]+)\//); if (pluginMatch) { return { name: pluginMatch[1], version: '1.0.0', // Default version enabled: true, mountPath: `/Game/Plugins/${pluginMatch[1]}/` }; } return null; } // Custom registry format loaders loadJSONRegistry(data) { const text = new TextDecoder().decode(data); const registryData = JSON.parse(text); // Process JSON registry data console.log('Loading JSON registry format', registryData); return true; } loadXMLRegistry(data) { const text = new TextDecoder().decode(data); // Parse XML registry data console.log('Loading XML registry format'); return true; } loadBinaryRegistry(data) { // Parse custom binary registry format console.log('Loading custom binary registry format'); return true; } } exports.EnhancedAssetRegistry = EnhancedAssetRegistry; //# sourceMappingURL=EnhancedAssetRegistry.js.map