using Ghost.Core; using Ghost.Data.Services; using System.Collections.Concurrent; using System.Text.Json; namespace Ghost.Editor.Core.AssetHandle; public static partial class AssetDatabase { // Asset cache - stores loaded assets by GUID private static readonly ConcurrentDictionary s_assetCache = new(); // LRU tracking - stores access time for each cached asset private static readonly ConcurrentDictionary s_assetAccessTime = new(); // Maximum number of cached assets before eviction starts private const int MAX_CACHED_ASSETS = 1000; // Percentage of cache to evict when limit is reached (evict oldest 20%) private const float CACHE_EVICTION_PERCENTAGE = 0.2f; /// /// Get the path to the imported asset data directory. /// private static Result GetImportedAssetsDirectory() { if (AssetsDirectory == null) { return Result.Failure("AssetsDirectory not initialized"); } var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "ImportedAssets"); if (!Directory.Exists(cacheDir)) { Directory.CreateDirectory(cacheDir); } return cacheDir; } /// /// Get the path where imported asset data is stored for a specific GUID. /// /// GUID of the asset. /// Full path to the imported asset data file. private static Result GetImportedAssetPath(Guid guid) { var importedDirResult = GetImportedAssetsDirectory(); if (importedDirResult.IsFailure) { return Result.Failure(importedDirResult.Message); } // Store imported assets as {GUID}.asset var assetDataPath = Path.Combine(importedDirResult.Value, $"{guid}.asset"); return assetDataPath; } /// /// Load asset by GUID with caching (internal implementation). /// /// Type of asset to load. /// GUID of the asset. /// The loaded asset. private static Result LoadAssetInternal(Guid guid) where T : Asset { // Check cache first if (s_assetCache.TryGetValue(guid, out var cachedAsset)) { // Update access time for LRU s_assetAccessTime[guid] = DateTime.UtcNow; if (cachedAsset is T typedAsset) { return typedAsset; } else { return Result.Failure($"Cached asset is of type {cachedAsset.GetType().Name}, expected {typeof(T).Name}"); } } // Asset not in cache, load from disk var assetPathResult = GetImportedAssetPath(guid); if (assetPathResult.IsFailure) { return Result.Failure(assetPathResult.Message); } var assetDataPath = assetPathResult.Value; if (!File.Exists(assetDataPath)) { return Result.Failure($"Imported asset data not found at {assetDataPath}. Asset may not have been imported yet."); } try { // Read and deserialize asset data var json = File.ReadAllText(assetDataPath); var asset = JsonSerializer.Deserialize(json); if (asset == null) { return Result.Failure("Failed to deserialize asset data"); } // Add to cache CacheAsset(guid, asset); return asset; } catch (Exception ex) { return Result.Failure($"Failed to load asset: {ex.Message}"); } } /// /// Load asset by path with caching. /// /// Type of asset to load. /// Full or relative path to the asset. /// The loaded asset. public static Result LoadAssetAtPath(string assetPath) where T : Asset { var guidResult = PathToGuid(assetPath); if (guidResult.IsFailure) { return Result.Failure(guidResult.Message); } return LoadAsset(guidResult.Value); } /// /// Add an asset to the cache with LRU eviction if needed. /// private static void CacheAsset(Guid guid, Asset asset) { // Check if we need to evict old assets if (s_assetCache.Count >= MAX_CACHED_ASSETS) { EvictOldestAssets(); } s_assetCache[guid] = asset; s_assetAccessTime[guid] = DateTime.UtcNow; } /// /// Evict the oldest assets from cache based on LRU. /// private static void EvictOldestAssets() { var evictionCount = (int)(MAX_CACHED_ASSETS * CACHE_EVICTION_PERCENTAGE); // Sort by access time and remove oldest entries var oldestAssets = s_assetAccessTime .OrderBy(kvp => kvp.Value) .Take(evictionCount) .Select(kvp => kvp.Key) .ToList(); foreach (var guid in oldestAssets) { s_assetCache.TryRemove(guid, out _); s_assetAccessTime.TryRemove(guid, out _); } } /// /// Unload a specific asset from cache. /// /// GUID of the asset to unload. public static void UnloadAsset(Guid guid) { s_assetCache.TryRemove(guid, out _); s_assetAccessTime.TryRemove(guid, out _); } /// /// Unload all assets from cache. /// public static void UnloadAllAssets() { s_assetCache.Clear(); s_assetAccessTime.Clear(); } /// /// Check if an asset is currently loaded in cache. /// /// GUID of the asset. /// True if the asset is in cache. public static bool IsAssetLoaded(Guid guid) { return s_assetCache.ContainsKey(guid); } /// /// Get cache statistics. /// /// Tuple of (current cache size, max cache size). public static (int currentSize, int maxSize) GetCacheStats() { return (s_assetCache.Count, MAX_CACHED_ASSETS); } /// /// Save an imported asset to disk for later loading. /// This should be called by importers after processing the source file. /// /// Type of asset data. /// GUID of the asset. /// Processed asset data to save. /// Result indicating success or failure. internal static Result SaveImportedAsset(Guid guid, T assetData) where T : Asset { var assetPathResult = GetImportedAssetPath(guid); if (assetPathResult.IsFailure) { return Result.Failure(assetPathResult.Message); } try { var json = JsonSerializer.Serialize(assetData, s_defaultJsonOptions); File.WriteAllText(assetPathResult.Value, json); // Invalidate cache for this asset so it gets reloaded next time UnloadAsset(guid); return Result.Success(); } catch (Exception ex) { return Result.Failure($"Failed to save imported asset: {ex.Message}"); } } }