From 426786397c2e2b71483ce729e0755ccc8d03be80 Mon Sep 17 00:00:00 2001 From: Misaki Date: Thu, 5 Feb 2026 19:25:48 +0900 Subject: [PATCH] Modify AssetService --- .../AssetHandle/AssetDatabase.FileOps.cs | 22 +-- .../AssetHandle/AssetDatabase.Importer.cs | 38 ++-- .../AssetHandle/AssetDatabase.Loader.cs | 62 +++---- .../AssetHandle/AssetDatabase.Lookup.cs | 34 ++-- .../AssetHandle/AssetDatabase.Meta.cs | 46 ++--- .../AssetHandle/AssetDatabase.Open.cs | 14 +- .../AssetHandle/AssetDatabase.SQLite.cs | 86 ++++----- .../AssetHandle/AssetDatabase.cs | 173 +++++++++--------- .../AssetHandle/AssetImporter.cs | 8 +- .../AssetHandle/Importers/TextImporter.cs | 5 +- .../AssetHandle/Importers/TextureImporter.cs | 20 +- Ghost.Editor.Core/Contracts/IAssetService.cs | 57 ++++++ Ghost.Editor.Core/EditorApplication.cs | 5 + Ghost.Editor/ActivationHandler.cs | 5 +- Ghost.Editor/App.xaml.cs | 1 + .../Controls/ProjectBrowserViewModel.cs | 6 +- .../Pages/EngineEditor/ProjectViewModel.cs | 9 +- .../AssetDatabaseIntegrationTest.cs | 2 +- 18 files changed, 332 insertions(+), 261 deletions(-) diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.FileOps.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.FileOps.cs index 1914cf3..b95a65a 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetDatabase.FileOps.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.FileOps.cs @@ -2,7 +2,7 @@ using Ghost.Core; namespace Ghost.Editor.Core.AssetHandle; -public static partial class AssetService +public partial class AssetService { /// /// Create a new asset at the specified path. @@ -11,7 +11,7 @@ public static partial class AssetService /// Path to create the asset at. /// Content to write to the asset file. /// Result indicating success or failure. - public static async ValueTask CreateAssetAsync(string assetPath, ReadOnlyMemory content, CancellationToken token = default) + public async ValueTask CreateAssetAsync(string assetPath, ReadOnlyMemory content, CancellationToken token = default) { if (AssetsDirectory == null) { @@ -57,7 +57,7 @@ public static partial class AssetService /// /// Path to create the asset at. /// Result indicating success or failure. - public static ValueTask CreateAssetAsync(string assetPath, CancellationToken token = default) + public ValueTask CreateAssetAsync(string assetPath, CancellationToken token = default) { return CreateAssetAsync(assetPath, ReadOnlyMemory.Empty, token); } @@ -67,7 +67,7 @@ public static partial class AssetService /// /// GUID of the asset to delete. /// Result indicating success or failure. - public static async ValueTask DeleteAssetAsync(Guid guid, CancellationToken token = default) + public async ValueTask DeleteAssetAsync(Guid guid, CancellationToken token = default) { var pathResult = GuidToPath(guid); if (pathResult.IsFailure) @@ -114,7 +114,7 @@ public static partial class AssetService /// /// Path to the asset to delete. /// Result indicating success or failure. - public static ValueTask DeleteAssetAsync(string assetPath, CancellationToken token = default) + public ValueTask DeleteAssetAsync(string assetPath, CancellationToken token = default) { var guidResult = PathToGuid(assetPath); if (guidResult.IsFailure) @@ -131,7 +131,7 @@ public static partial class AssetService /// GUID of the asset to move. /// New path for the asset (relative or absolute). /// Result indicating success or failure. - public static async ValueTask MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default) + public async ValueTask MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default) { var oldPathResult = GuidToPath(guid); if (oldPathResult.IsFailure) @@ -211,7 +211,7 @@ public static partial class AssetService /// CurrentApplication path of the asset. /// New path for the asset (relative or absolute). /// Result indicating success or failure. - public static ValueTask MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default) + public ValueTask MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default) { var guidResult = PathToGuid(oldPath); if (guidResult.IsFailure) @@ -228,7 +228,7 @@ public static partial class AssetService /// GUID of the asset to copy. /// New path for the copied asset (relative or absolute). /// Result containing the new asset's GUID. - public static async ValueTask> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default) + public async ValueTask> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default) { var oldPathResult = GuidToPath(guid); if (oldPathResult.IsFailure) @@ -299,7 +299,7 @@ public static partial class AssetService /// Path of the asset to copy. /// New path for the copied asset (relative or absolute). /// Result containing the new asset's GUID. - public static ValueTask> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default) + public ValueTask> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default) { var guidResult = PathToGuid(sourcePath); if (guidResult.IsFailure) @@ -315,7 +315,7 @@ public static partial class AssetService /// /// GUID of the asset to mark dirty. /// Result indicating success or failure. - public static Result MarkDirtyAsync(Guid guid, CancellationToken token = default) + public Result MarkDirtyAsync(Guid guid, CancellationToken token = default) { MarkDirty(guid); return Result.Success(); @@ -325,7 +325,7 @@ public static partial class AssetService /// Import all dirty assets. /// /// Result indicating success or failure. - public static async Task ImportDirtyAssetsAsync(CancellationToken token = default) + public async Task ImportDirtyAssetsAsync(CancellationToken token = default) { var dirtyGuids = GetDirtyAssets(); diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Importer.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Importer.cs index cafe15d..4af1208 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Importer.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Importer.cs @@ -3,27 +3,27 @@ using System.Reflection; namespace Ghost.Editor.Core.AssetHandle; -public static partial class AssetService +public partial class AssetService { - private static readonly Dictionary s_importerInstances = new(); + private readonly Dictionary _importerInstances = new(); /// /// Import an asset at the specified path. /// /// Full path to the asset file. /// Result indicating success or failure. - private static async ValueTask ImportAssetAsync(string assetPath, CancellationToken token = default) + private async ValueTask ImportAssetAsync(string assetPath, CancellationToken token = default) { var extension = Path.GetExtension(assetPath); - if (!s_importerTypeLookup.TryGetValue(extension, out var importerType)) + if (!_importerTypeLookup.TryGetValue(extension, out var importerType)) { // No importer registered for this file type return Result.Success(); } // Get or create importer instance - if (!s_importerInstances.TryGetValue(importerType, out var importerInstance)) + if (!_importerInstances.TryGetValue(importerType, out var importerInstance)) { importerInstance = Activator.CreateInstance(importerType) as AssetImporter; if (importerInstance is null) @@ -31,7 +31,7 @@ public static partial class AssetService return Result.Failure($"Failed to create importer instance for type {importerType.Name}"); } - s_importerInstances[importerType] = importerInstance; + _importerInstances[importerType] = importerInstance; } // Read metadata @@ -41,7 +41,7 @@ public static partial class AssetService return Result.Failure($"Failed to read asset metadata: {metaResult.Message}"); } - return await importerInstance.ImportAsync(assetPath, metaResult.Value, token); + return await importerInstance.ImportAsync(assetPath, metaResult.Value, this, token); } /// @@ -49,9 +49,9 @@ public static partial class AssetService /// /// File extension (e.g., ".png"). /// The importer type if found, otherwise null. - public static Type? GetImporterType(string extension) + public Type? GetImporterType(string extension) { - s_importerTypeLookup.TryGetValue(extension, out var importerType); + _importerTypeLookup.TryGetValue(extension, out var importerType); return importerType; } @@ -59,9 +59,9 @@ public static partial class AssetService /// Get all registered importer types and their supported extensions. /// /// Dictionary mapping extensions to importer types. - public static Dictionary GetAllImporters() + public Dictionary GetAllImporters() { - return new Dictionary(s_importerTypeLookup); + return new Dictionary(_importerTypeLookup); } /// @@ -72,17 +72,18 @@ public static partial class AssetService /// Full path where the asset should be saved. /// In-memory asset data to export. /// Result with the GUID of the exported asset. - public static async ValueTask> ExportAssetAsync(string assetPath, T assetData, CancellationToken token = default) where T : class + public async ValueTask> ExportAssetAsync(string assetPath, T assetData, CancellationToken token = default) + where T : class { var extension = Path.GetExtension(assetPath); - if (!s_importerTypeLookup.TryGetValue(extension, out var importerType)) + if (!_importerTypeLookup.TryGetValue(extension, out var importerType)) { return Result.Failure($"No importer registered for extension {extension}"); } // Get or create importer instance - if (!s_importerInstances.TryGetValue(importerType, out var importerInstance)) + if (!_importerInstances.TryGetValue(importerType, out var importerInstance)) { importerInstance = Activator.CreateInstance(importerType) as AssetImporter; if (importerInstance is null) @@ -90,14 +91,7 @@ public static partial class AssetService return Result.Failure($"Failed to create importer instance for type {importerType.Name}"); } - s_importerInstances[importerType] = importerInstance; - } - - // Find and invoke the ExportAsync method - var exportMethod = importerType.GetMethod("ExportAsync", BindingFlags.Public | BindingFlags.Instance); - if (exportMethod == null) - { - return Result.Failure($"ExportAsync method not found on importer {importerType.Name}. This importer does not support exporting."); + _importerInstances[importerType] = importerInstance; } // Generate metadata for the new asset diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Loader.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Loader.cs index 404c118..30f9280 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Loader.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Loader.cs @@ -4,21 +4,21 @@ using System.Text.Json; namespace Ghost.Editor.Core.AssetHandle; -public static partial class AssetService +public partial class AssetService { // Asset cache - stores loaded assets by GUID - private static readonly ConcurrentDictionary s_assetCache = new(); + private readonly ConcurrentDictionary _assetCache = new(); // LRU tracking - stores access time for each cached asset - private static readonly ConcurrentDictionary s_assetAccessTime = new(); + private readonly ConcurrentDictionary _assetAccessTime = new(); // Maximum number of cached assets before eviction starts - private const int MAX_CACHED_ASSETS = 1000; + 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; - private static Result GetImportedAssetsDirectory() + private Result GetImportedAssetsDirectory() { if (AssetsDirectory == null) { @@ -34,7 +34,7 @@ public static partial class AssetService return cacheDir; } - private static Result GetImportedAssetPath(Guid guid) + private Result GetImportedAssetPath(Guid guid) { var importedDirResult = GetImportedAssetsDirectory(); if (importedDirResult.IsFailure) @@ -47,13 +47,13 @@ public static partial class AssetService return assetDataPath; } - private static Result LoadAssetInternal(Guid guid) where T : Asset + private Result LoadAssetInternal(Guid guid) where T : Asset { // Check cache first - if (s_assetCache.TryGetValue(guid, out var cachedAsset)) + if (_assetCache.TryGetValue(guid, out var cachedAsset)) { // Update access time for LRU - s_assetAccessTime[guid] = DateTime.UtcNow; + _assetAccessTime[guid] = DateTime.UtcNow; if (cachedAsset is T typedAsset) { @@ -98,7 +98,7 @@ public static partial class AssetService } } - public static Result LoadAssetAtPath(string assetPath) where T : Asset + public Result LoadAssetAtPath(string assetPath) where T : Asset { var guidResult = PathToGuid(assetPath); if (guidResult.IsFailure) @@ -109,24 +109,24 @@ public static partial class AssetService return LoadAsset(guidResult.Value); } - private static void CacheAsset(Guid guid, Asset asset) + private void CacheAsset(Guid guid, Asset asset) { // Check if we need to evict old assets - if (s_assetCache.Count >= MAX_CACHED_ASSETS) + if (_assetCache.Count >= _MAX_CACHED_ASSETS) { EvictOldestAssets(); } - s_assetCache[guid] = asset; - s_assetAccessTime[guid] = DateTime.UtcNow; + _assetCache[guid] = asset; + _assetAccessTime[guid] = DateTime.UtcNow; } - private static void EvictOldestAssets() + private void EvictOldestAssets() { - var evictionCount = (int)(MAX_CACHED_ASSETS * _CACHE_EVICTION_PERCENTAGE); + var evictionCount = (int)(_MAX_CACHED_ASSETS * _CACHE_EVICTION_PERCENTAGE); // Sort by access time and remove oldest entries - var oldestAssets = s_assetAccessTime + var oldestAssets = _assetAccessTime .OrderBy(kvp => kvp.Value) .Take(evictionCount) .Select(kvp => kvp.Key) @@ -134,8 +134,8 @@ public static partial class AssetService foreach (var guid in oldestAssets) { - s_assetCache.TryRemove(guid, out _); - s_assetAccessTime.TryRemove(guid, out _); + _assetCache.TryRemove(guid, out _); + _assetAccessTime.TryRemove(guid, out _); } } @@ -143,19 +143,19 @@ public static partial class AssetService /// Unload a specific asset from cache. /// /// GUID of the asset to unload. - public static void UnloadAsset(Guid guid) + public void UnloadAsset(Guid guid) { - s_assetCache.TryRemove(guid, out _); - s_assetAccessTime.TryRemove(guid, out _); + _assetCache.TryRemove(guid, out _); + _assetAccessTime.TryRemove(guid, out _); } /// /// Unload all assets from cache. /// - public static void UnloadAllAssets() + public void UnloadAllAssets() { - s_assetCache.Clear(); - s_assetAccessTime.Clear(); + _assetCache.Clear(); + _assetAccessTime.Clear(); } /// @@ -163,18 +163,18 @@ public static partial class AssetService /// /// GUID of the asset. /// True if the asset is in cache. - public static bool IsAssetLoaded(Guid guid) + public bool IsAssetLoaded(Guid guid) { - return s_assetCache.ContainsKey(guid); + return _assetCache.ContainsKey(guid); } /// /// Get cache statistics. /// /// Tuple of (current cache size, max cache size). - public static (int currentSize, int maxSize) GetCacheStats() + public (int currentSize, int maxSize) GetCacheStats() { - return (s_assetCache.Count, MAX_CACHED_ASSETS); + return (_assetCache.Count, _MAX_CACHED_ASSETS); } /// @@ -185,7 +185,7 @@ public static partial class AssetService /// GUID of the asset. /// Processed asset data to save. /// Result indicating success or failure. - public static Result SaveImportedAsset(Guid guid, T assetData) + public Result SaveImportedAsset(Guid guid, T assetData) where T : Asset { var assetPathResult = GetImportedAssetPath(guid); @@ -196,7 +196,7 @@ public static partial class AssetService try { - var json = JsonSerializer.Serialize(assetData, s_defaultJsonOptions); + var json = JsonSerializer.Serialize(assetData, _defaultJsonOptions); File.WriteAllText(assetPathResult.Value, json); // Invalidate cache for this asset so it gets reloaded next time diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Lookup.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Lookup.cs index 004d2de..1084086 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Lookup.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Lookup.cs @@ -3,12 +3,12 @@ using System.Text.Json; namespace Ghost.Editor.Core.AssetHandle; -public static partial class AssetService +public partial class AssetService { /// /// Get the relative path from the assets directory. /// - private static Result GetRelativePath(string fullPath) + private Result GetRelativePath(string fullPath) { if (AssetsDirectory == null) { @@ -26,7 +26,7 @@ public static partial class AssetService /// /// Get the full path from a relative path. /// - private static Result GetFullPath(string relativePath) + private Result GetFullPath(string relativePath) { if (AssetsDirectory == null) { @@ -41,7 +41,7 @@ public static partial class AssetService /// /// Full or relative path to the asset. /// The GUID of the asset if found. - public static Result PathToGuid(string assetPath) + public Result PathToGuid(string assetPath) { var relativePath = assetPath; @@ -59,9 +59,9 @@ public static partial class AssetService // Normalize path separators relativePath = relativePath.Replace('\\', '/'); - lock (s_dbLock) + lock (_dbLock) { - if (s_pathAssetLookup.TryGetValue(relativePath, out var guid)) + if (_pathAssetLookup.TryGetValue(relativePath, out var guid)) { return guid; } @@ -75,11 +75,11 @@ public static partial class AssetService /// /// GUID of the asset. /// The relative path to the asset if found. - public static Result GuidToPath(Guid guid) + public Result GuidToPath(Guid guid) { - lock (s_dbLock) + lock (_dbLock) { - if (s_assetPathLookup.TryGetValue(guid, out var path)) + if (_assetPathLookup.TryGetValue(guid, out var path)) { return path; } @@ -94,7 +94,7 @@ public static partial class AssetService /// Type of asset to load. /// GUID of the asset. /// The loaded asset. - public static Result LoadAsset(Guid guid) where T : Asset + public Result LoadAsset(Guid guid) where T : Asset { // Implemented in AssetService.Loader.cs return LoadAssetInternal(guid); @@ -105,7 +105,7 @@ public static partial class AssetService /// /// GUID of the asset. /// List of tags associated with the asset. - public static async ValueTask>> GetAssetTagsAsync(Guid guid, CancellationToken token = default) + public async ValueTask>> GetAssetTagsAsync(Guid guid, CancellationToken token = default) { var pathResult = GuidToPath(guid); if (pathResult.IsFailure) @@ -134,7 +134,7 @@ public static partial class AssetService /// GUID of the asset. /// New tags for the asset. /// Result indicating success or failure. - public static async ValueTask SetAssetTagsAsync(Guid guid, List tags, CancellationToken token = default) + public async ValueTask SetAssetTagsAsync(Guid guid, List tags, CancellationToken token = default) { var pathResult = GuidToPath(guid); if (pathResult.IsFailure) @@ -174,7 +174,7 @@ public static partial class AssetService /// /// Search pattern (e.g., "*.txt", "player?", "test*"). /// List of matching asset GUIDs. - public static async Task> FindAssetsByNameAsync(string namePattern, CancellationToken token = default) + public async Task> FindAssetsByNameAsync(string namePattern, CancellationToken token = default) { return await GetAssetsByNameAsync(namePattern, token); } @@ -184,7 +184,7 @@ public static partial class AssetService /// /// Tag to search for. /// List of asset GUIDs with the specified tag. - public static async Task> FindAssetsByTagAsync(string tag, CancellationToken token = default) + public async Task> FindAssetsByTagAsync(string tag, CancellationToken token = default) { return await GetAssetsByTagAsync(tag, token); } @@ -193,11 +193,11 @@ public static partial class AssetService /// Get all assets in the database. /// /// Dictionary mapping GUIDs to relative paths. - public static IReadOnlyDictionary GetAllAssets() + public IReadOnlyDictionary GetAllAssets() { - lock (s_dbLock) + lock (_dbLock) { - return s_assetPathLookup.AsReadOnly(); + return _assetPathLookup.AsReadOnly(); } } } diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Meta.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Meta.cs index c5a4679..14ad358 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Meta.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Meta.cs @@ -7,13 +7,13 @@ using System.Text.Json; namespace Ghost.Editor.Core.AssetHandle; -public static partial class AssetService +public partial class AssetService { - private static readonly Dictionary s_importerTypeLookup = new(); + private readonly Dictionary _importerTypeLookup = new(); - private static void InitializeMetaData() + private void InitializeMetaData() { - if (s_watcher == null) + if (_watcher == null) { throw new InvalidOperationException("AssetDatabase is not initialized. Ensure that Initialize() is called before registering asset importers."); } @@ -24,17 +24,17 @@ public static partial class AssetService var attribute = type.GetCustomAttribute()!; foreach (var extension in attribute.SupportedExtensions) { - s_importerTypeLookup[extension] = type; + _importerTypeLookup[extension] = type; } } - s_watcher.Created += OnFSEvent; - s_watcher.Deleted += OnFSEvent; - s_watcher.Changed += OnFSEvent; - s_watcher.Renamed += OnAssetRenamed; + _watcher.Created += OnFSEvent; + _watcher.Deleted += OnFSEvent; + _watcher.Changed += OnFSEvent; + _watcher.Renamed += OnAssetRenamed; } - private static Result GetMetaFilePath(string assetPath) + private Result GetMetaFilePath(string assetPath) { if (Directory.Exists(assetPath)) { @@ -49,11 +49,11 @@ public static partial class AssetService return assetPath + FileExtensions.META_FILE_EXTENSION; } - private static ImporterSettings? GetDefaultSettingsForAsset(string assetPath) + private ImporterSettings? GetDefaultSettingsForAsset(string assetPath) { var extension = Path.GetExtension(assetPath); - if (s_importerTypeLookup.TryGetValue(extension, out var importerType)) + if (_importerTypeLookup.TryGetValue(extension, out var importerType)) { var settingsType = importerType.BaseType?.GetGenericArguments()[0]; if (settingsType == null || !typeof(ImporterSettings).IsAssignableFrom(settingsType)) @@ -70,7 +70,7 @@ public static partial class AssetService /// /// Calculate SHA256 hash of a file for change detection. /// - private static async Task CalculateFileHashAsync(string filePath, CancellationToken token = default) + private async Task CalculateFileHashAsync(string filePath, CancellationToken token = default) { try { @@ -84,12 +84,12 @@ public static partial class AssetService } } - private static async Task WriteMetaFileAsync(string metaFilePath, AssetMeta metaData, CancellationToken token = default) + private async Task WriteMetaFileAsync(string metaFilePath, AssetMeta metaData, CancellationToken token = default) { try { await using var fileStream = File.Create(metaFilePath); - await JsonSerializer.SerializeAsync(fileStream, metaData, s_defaultJsonOptions, token); + await JsonSerializer.SerializeAsync(fileStream, metaData, _defaultJsonOptions, token); return Result.Success(); } catch (Exception ex) @@ -101,7 +101,7 @@ public static partial class AssetService /// /// Read metadata from a .gmeta file. /// - private static async ValueTask> ReadMetaFileAsync(string assetPath, CancellationToken token = default) + private async ValueTask> ReadMetaFileAsync(string assetPath, CancellationToken token = default) { var metaFileResult = GetMetaFilePath(assetPath); if (metaFileResult.IsFailure) @@ -117,7 +117,7 @@ public static partial class AssetService try { await using var fileStream = File.OpenRead(metaFileResult.Value); - var meta = await JsonSerializer.DeserializeAsync(fileStream, s_defaultJsonOptions, token); + var meta = await JsonSerializer.DeserializeAsync(fileStream, _defaultJsonOptions, token); if (meta == null) { return Result.Failure("Failed to deserialize metadata"); @@ -131,7 +131,7 @@ public static partial class AssetService } } - internal static async ValueTask GenerateMetaFileAsync(string assetPath, CancellationToken token = default) + internal async ValueTask GenerateMetaFileAsync(string assetPath, CancellationToken token = default) { Result r; @@ -147,7 +147,7 @@ public static partial class AssetService if (existingMetaResult.IsSuccess) { var existingMeta = existingMetaResult.Value; - if (s_assetPathLookup.TryGetValue(existingMeta.Guid, out var path)) + if (_assetPathLookup.TryGetValue(existingMeta.Guid, out var path)) { var relResult = GetRelativePath(assetPath); if (relResult.IsSuccess && assetPath != path) @@ -196,12 +196,12 @@ public static partial class AssetService } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsMetaFile(string path) + private bool IsMetaFile(string path) { return Path.GetExtension(path).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase); } - private static async void OnFSEvent(object sender, FileSystemEventArgs e) + private async void OnFSEvent(object sender, FileSystemEventArgs e) { if (IsMetaFile(e.FullPath)) { @@ -219,7 +219,7 @@ public static partial class AssetService await PostCommandAsync(new AssetCommand(type, e.FullPath, Timestamp: DateTime.UtcNow)); } - private static async void OnAssetRenamed(object sender, RenamedEventArgs e) + private async void OnAssetRenamed(object sender, RenamedEventArgs e) { if (IsMetaFile(e.FullPath)) { @@ -232,7 +232,7 @@ public static partial class AssetService /// /// Mark all assets that depend on the specified asset as dirty. /// - private static async Task MarkDependentAssetsDirtyAsync(Guid assetGuid) + private async Task MarkDependentAssetsDirtyAsync(Guid assetGuid) { // TODO: We should have a reverse dependency lookup in the database to avoid scanning all assets. diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Open.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Open.cs index bc05396..b106251 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Open.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Open.cs @@ -5,11 +5,11 @@ using System.Reflection; namespace Ghost.Editor.Core.AssetHandle; -public static partial class AssetService +public partial class AssetService { - private static readonly Dictionary> s_assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase); - private static void InitializeAssetHandle() + private void InitializeAssetHandle() { var methods = TypeCache.GetTypes() .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) @@ -23,20 +23,20 @@ public static partial class AssetService var del = (Action)Delegate.CreateDelegate(typeof(Action), method); foreach (var ext in attr.Extensions) { - if (s_assetOpenHandlers.ContainsKey(ext)) + if (_assetOpenHandlers.ContainsKey(ext)) { Logger.LogError($"Duplicate asset open handler for extension '{ext}' found in method '{method.Name}'. Existing handler will be overwritten."); } - s_assetOpenHandlers[ext] = del; + _assetOpenHandlers[ext] = del; } } } - public static void OpenAsset(string path) + public void OpenAsset(string path) { var extension = Path.GetExtension(path); - if (s_assetOpenHandlers.TryGetValue(extension, out var handler)) + if (_assetOpenHandlers.TryGetValue(extension, out var handler)) { handler(path); } diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.SQLite.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.SQLite.cs index 358c80d..19d57ca 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetDatabase.SQLite.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.SQLite.cs @@ -4,14 +4,14 @@ using System.Text.Json; namespace Ghost.Editor.Core.AssetHandle; -public static partial class AssetService +public partial class AssetService { - private static SqliteConnection? s_dbConnection; + private SqliteConnection? _dbConnection; /// - /// Initialize the SQLite database for asset caching. + /// Init the SQLite database for asset caching. /// - private static async Task InitializeDatabaseAsync(CancellationToken token = default) + private async Task InitializeDatabaseAsync(CancellationToken token = default) { if (AssetsDirectory == null) { @@ -32,11 +32,11 @@ public static partial class AssetService Cache = SqliteCacheMode.Shared }.ToString(); - s_dbConnection = new SqliteConnection(connectionString); - await s_dbConnection.OpenAsync(token); + _dbConnection = new SqliteConnection(connectionString); + await _dbConnection.OpenAsync(token); // Create tables - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = @" CREATE TABLE IF NOT EXISTS Assets ( Guid TEXT PRIMARY KEY, @@ -60,9 +60,9 @@ public static partial class AssetService /// Asset metadata from .gmeta file. /// SHA256 hash of the asset file content. /// List of GUIDs this asset depends on (extracted during import). - private static async ValueTask UpsertAssetAsync(string assetPath, AssetMeta meta, string fileHash, List? dependencies = null, CancellationToken token = default) + private async ValueTask UpsertAssetAsync(string assetPath, AssetMeta meta, string fileHash, List? dependencies = null, CancellationToken token = default) { - if (s_dbConnection == null) + if (_dbConnection == null) { return Result.Failure("Database not initialized"); } @@ -75,21 +75,21 @@ public static partial class AssetService try { - lock (s_dbLock) + lock (_dbLock) { // If this GUID already exists with a different path, remove the old path mapping - if (s_assetPathLookup.TryGetValue(meta.Guid, out var oldPath) && oldPath != relativePath.Value) + if (_assetPathLookup.TryGetValue(meta.Guid, out var oldPath) && oldPath != relativePath.Value) { - s_pathAssetLookup.Remove(oldPath); + _pathAssetLookup.Remove(oldPath); } // Update lookups with new path (normalize path separators for consistency) var normalizedPath = relativePath.Value.Replace('\\', '/'); - s_assetPathLookup[meta.Guid] = normalizedPath; - s_pathAssetLookup[normalizedPath] = meta.Guid; + _assetPathLookup[meta.Guid] = normalizedPath; + _pathAssetLookup[normalizedPath] = meta.Guid; } - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = @" INSERT OR REPLACE INTO Assets (Guid, Path, Version, Tags, FileHash, DependencyGuids, LastModified) VALUES (@guid, @path, @version, @tags, @fileHash, @deps, @modified) @@ -114,25 +114,25 @@ public static partial class AssetService /// /// Remove an asset from the database. /// - private static async Task RemoveAssetFromDatabaseAsync(Guid guid, CancellationToken token = default) + private async Task RemoveAssetFromDatabaseAsync(Guid guid, CancellationToken token = default) { - if (s_dbConnection == null) + if (_dbConnection == null) { return Result.Failure("Database not initialized"); } try { - lock (s_dbLock) + lock (_dbLock) { - if (s_assetPathLookup.TryGetValue(guid, out var path)) + if (_assetPathLookup.TryGetValue(guid, out var path)) { - s_assetPathLookup.Remove(guid); - s_pathAssetLookup.Remove(path); + _assetPathLookup.Remove(guid); + _pathAssetLookup.Remove(path); } } - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "DELETE FROM Assets WHERE Guid = @guid"; cmd.Parameters.AddWithValue("@guid", guid.ToString()); @@ -150,16 +150,16 @@ public static partial class AssetService /// /// Load all assets from the database into memory cache. /// - private static async Task LoadAssetCacheFromDatabaseAsync(CancellationToken token = default) + private async Task LoadAssetCacheFromDatabaseAsync(CancellationToken token = default) { - if (s_dbConnection == null) + if (_dbConnection == null) { return; } try { - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "SELECT Guid, Path FROM Assets"; await using var reader = await cmd.ExecuteReaderAsync(token); @@ -170,10 +170,10 @@ public static partial class AssetService if (Guid.TryParse(guidStr, out var guid)) { - lock (s_dbLock) + lock (_dbLock) { - s_assetPathLookup[guid] = path; - s_pathAssetLookup[path] = guid; + _assetPathLookup[guid] = path; + _pathAssetLookup[path] = guid; } } } @@ -187,18 +187,18 @@ public static partial class AssetService /// /// Get assets by tag. /// - private static async Task> GetAssetsByTagAsync(string tag, CancellationToken token = default) + private async Task> GetAssetsByTagAsync(string tag, CancellationToken token = default) { var result = new List(); - if (s_dbConnection == null) + if (_dbConnection == null) { return result; } try { - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "SELECT Guid, Tags FROM Assets"; await using var reader = await cmd.ExecuteReaderAsync(token); @@ -228,16 +228,16 @@ public static partial class AssetService /// /// Get the file hash for an asset from the database. /// - private static async Task GetFileHashAsync(Guid guid, CancellationToken token = default) + private async Task GetFileHashAsync(Guid guid, CancellationToken token = default) { - if (s_dbConnection == null) + if (_dbConnection == null) { return null; } try { - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "SELECT FileHash FROM Assets WHERE Guid = @guid"; cmd.Parameters.AddWithValue("@guid", guid.ToString()); @@ -253,16 +253,16 @@ public static partial class AssetService /// /// Get the dependencies for an asset from the database. /// - private static async Task> GetDependenciesAsync(Guid guid, CancellationToken token = default) + private async Task> GetDependenciesAsync(Guid guid, CancellationToken token = default) { - if (s_dbConnection == null) + if (_dbConnection == null) { return new List(); } try { - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "SELECT DependencyGuids FROM Assets WHERE Guid = @guid"; cmd.Parameters.AddWithValue("@guid", guid.ToString()); @@ -285,11 +285,11 @@ public static partial class AssetService /// Find assets by name pattern using database query with wildcards. /// /// Pattern supporting * (any chars) and ? (single char). - private static async Task> GetAssetsByNameAsync(string namePattern, CancellationToken token = default) + private async Task> GetAssetsByNameAsync(string namePattern, CancellationToken token = default) { var results = new List(); - if (s_dbConnection == null) + if (_dbConnection == null) { return results; } @@ -299,7 +299,7 @@ public static partial class AssetService // Convert wildcard pattern to SQL LIKE pattern var sqlPattern = namePattern.Replace('*', '%').Replace('?', '_'); - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); // Extract just the filename from the path for matching // SQLite doesn't have a built-in path manipulation, so we search in the full path @@ -344,9 +344,9 @@ public static partial class AssetService /// /// Remove orphaned entries from database (assets that no longer exist on disk). /// - private static async Task RemoveOrphanedEntriesAsync(CancellationToken token = default) + private async Task RemoveOrphanedEntriesAsync(CancellationToken token = default) { - if (s_dbConnection == null || AssetsDirectory == null) + if (_dbConnection == null || AssetsDirectory == null) { return; } @@ -355,7 +355,7 @@ public static partial class AssetService { var orphanedGuids = new List(); - await using var cmd = s_dbConnection.CreateCommand(); + await using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "SELECT Guid, Path FROM Assets"; await using var reader = await cmd.ExecuteReaderAsync(token); diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.cs index 67f496e..08e2aaf 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetDatabase.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.cs @@ -1,4 +1,5 @@ using Ghost.Core; +using Ghost.Editor.Core.Contracts; using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization; @@ -33,31 +34,31 @@ internal readonly record struct AssetCommand( /// Handles asset registration, lookup, importing, and dependency management. /// Uses SQLite for persistent storage and efficient querying. /// -public static partial class AssetService +public partial class AssetService : IAssetService { - private static FileSystemWatcher? s_watcher; - private static readonly Lock s_dbLock = new(); - private static readonly Dictionary s_assetPathLookup = new(); - private static readonly Dictionary s_pathAssetLookup = new(); + private FileSystemWatcher? _watcher; + private readonly Lock _dbLock = new(); + private readonly Dictionary _assetPathLookup = new(); + private readonly Dictionary _pathAssetLookup = new(); // In-memory dirty asset tracking (for runtime modifications only) // TODO: We do not handle the reimporting of dirty assets yet - private static readonly HashSet s_dirtyAssets = new(); + private readonly HashSet _dirtyAssets = new(); // Command buffer pattern - Channel for file system event commands - private static Channel? s_commandChannel; - private static Timer? s_commandProcessorTimer; - private static readonly ConcurrentQueue s_waitingCommands = new(); // Commands waiting for manual refresh - private static bool s_autoRefreshEnabled = true; + private Channel? _commandChannel; + private Timer? _commandProcessorTimer; + private readonly ConcurrentQueue _waitingCommands = new(); // Commands waiting for manual refresh + private bool _autoRefreshEnabled = true; // Initialization guard - private static readonly Lock s_initializationLock = new(); - private static bool s_initialized = false; + private readonly Lock _initializationLock = new(); + private bool _initialized = false; - private static readonly TimeSpan s_debounceDelay = TimeSpan.FromMilliseconds(100); - private static readonly ManualResetEventSlim s_resetEventSlim = new(false); + private readonly TimeSpan _debounceDelay = TimeSpan.FromMilliseconds(100); + private readonly ManualResetEventSlim _resetEventSlim = new(false); - private static readonly JsonSerializerOptions s_defaultJsonOptions = new() + private readonly JsonSerializerOptions _defaultJsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, @@ -67,43 +68,43 @@ public static partial class AssetService } }; - public static DirectoryInfo? AssetsDirectory + public DirectoryInfo? AssetsDirectory { get; private set; } /// - /// Initialize the asset database. + /// Init the asset database. /// Must be called after project is loaded. /// - internal static async Task Initialize(CancellationToken token = default) + internal async Task Init(CancellationToken token = default) { - lock (s_initializationLock) + lock (_initializationLock) { - if (s_initialized) + if (_initialized) { return; } - s_initialized = true; + _initialized = true; } AssetsDirectory = new DirectoryInfo(Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME)); - s_commandChannel = Channel.CreateUnbounded(new UnboundedChannelOptions + _commandChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false }); - // Initialize command processor timer (starts disabled, triggered by events) - s_commandProcessorTimer = new Timer(ProcessPendingCommands, null, Timeout.Infinite, Timeout.Infinite); + // Init command processor timer (starts disabled, triggered by events) + _commandProcessorTimer = new Timer(ProcessPendingCommands, null, Timeout.Infinite, Timeout.Infinite); await InitializeDatabaseAsync(token); await LoadAssetCacheFromDatabaseAsync(token); - s_watcher = new FileSystemWatcher + _watcher = new FileSystemWatcher { Path = AssetsDirectory.FullName, IncludeSubdirectories = true, @@ -122,7 +123,7 @@ public static partial class AssetService /// Validate the asset database and fix any inconsistencies. /// Checks for missing/corrupted assets and regenerates metadata as needed. /// - private static async Task ValidateAndFixDatabaseAsync(CancellationToken token = default) + private async Task ValidateAndFixDatabaseAsync(CancellationToken token = default) { if (AssetsDirectory == null) { @@ -175,19 +176,19 @@ public static partial class AssetService /// Refresh the asset database manually. /// Scans the project directory for changes and processes any queued file system events. /// - public static async Task RefreshAsync(CancellationToken token = default) + public async Task RefreshAsync(CancellationToken token = default) { // Flush waiting commands to channel - while (s_waitingCommands.TryDequeue(out var cmd)) + while (_waitingCommands.TryDequeue(out var cmd)) { - s_commandChannel?.Writer.TryWrite(cmd); + _commandChannel?.Writer.TryWrite(cmd); } - s_resetEventSlim.Reset(); - s_commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty)); - s_commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan); + _resetEventSlim.Reset(); + _commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty)); + _commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan); - await Task.Run(s_resetEventSlim.Wait, token); + await Task.Run(_resetEventSlim.Wait, token); return Result.Success(); } @@ -195,55 +196,55 @@ public static partial class AssetService /// Mark an asset as dirty (modified in memory but not yet saved). /// This state is NOT persisted and will be lost on application restart. /// - public static void MarkDirty(Guid assetGuid) + public void MarkDirty(Guid assetGuid) { - lock (s_dbLock) + lock (_dbLock) { - s_dirtyAssets.Add(assetGuid); + _dirtyAssets.Add(assetGuid); } } /// /// Check if an asset is marked as dirty. /// - public static bool IsDirty(Guid assetGuid) + public bool IsDirty(Guid assetGuid) { - lock (s_dbLock) + lock (_dbLock) { - return s_dirtyAssets.Contains(assetGuid); + return _dirtyAssets.Contains(assetGuid); } } /// /// Get all dirty assets. /// - public static Guid[] GetDirtyAssets() + public Guid[] GetDirtyAssets() { - lock (s_dbLock) + lock (_dbLock) { - return s_dirtyAssets.ToArray(); + return _dirtyAssets.ToArray(); } } /// /// Clear dirty flag for an asset (typically after saving). /// - public static void ClearDirty(Guid assetGuid) + public void ClearDirty(Guid assetGuid) { - lock (s_dbLock) + lock (_dbLock) { - s_dirtyAssets.Remove(assetGuid); + _dirtyAssets.Remove(assetGuid); } } /// /// Clear all dirty flags. /// - public static void ClearAllDirty() + public void ClearAllDirty() { - lock (s_dbLock) + lock (_dbLock) { - s_dirtyAssets.Clear(); + _dirtyAssets.Clear(); } } @@ -251,15 +252,15 @@ public static partial class AssetService /// Enable or disable automatic asset database refresh. /// When disabled, file system events are queued and processed only when RefreshAsync() is called. /// - public static void SetAutoRefresh(bool enabled) + public void SetAutoRefresh(bool enabled) { - s_autoRefreshEnabled = enabled; + _autoRefreshEnabled = enabled; } - internal static void FlushPendingCommands() + internal void FlushPendingCommands() { // Stop timer temporarily - s_commandProcessorTimer?.Change(Timeout.Infinite, Timeout.Infinite); + _commandProcessorTimer?.Change(Timeout.Infinite, Timeout.Infinite); // Give a tiny bit of time for any in-flight file watcher events to post to channel Thread.Sleep(50); @@ -268,27 +269,27 @@ public static partial class AssetService ProcessPendingCommands(null); } - private static async ValueTask PostCommandAsync(AssetCommand command, CancellationToken token = default) + private async ValueTask PostCommandAsync(AssetCommand command, CancellationToken token = default) { - if (s_commandChannel == null) + if (_commandChannel == null) { return; } - if (s_autoRefreshEnabled) + if (_autoRefreshEnabled) { - await s_commandChannel.Writer.WriteAsync(command, token); - s_commandProcessorTimer?.Change(s_debounceDelay, Timeout.InfiniteTimeSpan); + await _commandChannel.Writer.WriteAsync(command, token); + _commandProcessorTimer?.Change(_debounceDelay, Timeout.InfiniteTimeSpan); } else { - s_waitingCommands.Enqueue(command); + _waitingCommands.Enqueue(command); } } - private static async void ProcessPendingCommands(object? state) + private async void ProcessPendingCommands(object? state) { - if (s_commandChannel == null) + if (_commandChannel == null) { return; } @@ -298,7 +299,7 @@ public static partial class AssetService // // Collect all pending commands // var commands = new List(); // - // while (s_commandChannel.Reader.TryRead(out var cmd)) + // while (_commandChannel.Reader.TryRead(out var cmd)) // { // commands.Add(cmd); // } @@ -333,12 +334,12 @@ public static partial class AssetService // Execute commands // NOTE: We many don't need to collect all commands first, just process as we read. // Channel in c# is thread-safe for multiple readers/writers. - //await foreach (var cmd in s_commandChannel.Reader.ReadAllAsync()) + //await foreach (var cmd in _commandChannel.Reader.ReadAllAsync()) //{ // await ExecuteCommandAsync(cmd); //} - while (s_commandChannel.Reader.TryRead(out var cmd)) + while (_commandChannel.Reader.TryRead(out var cmd)) { await ExecuteCommandAsync(cmd); } @@ -351,11 +352,11 @@ public static partial class AssetService } finally { - s_resetEventSlim.Set(); + _resetEventSlim.Set(); } } - private static async ValueTask ExecuteCommandAsync(AssetCommand command) + private async ValueTask ExecuteCommandAsync(AssetCommand command) { switch (command.Type) { @@ -384,7 +385,7 @@ public static partial class AssetService } } - private static async ValueTask HandleFileCreatedAsync(string path) + private async ValueTask HandleFileCreatedAsync(string path) { if (!File.Exists(path)) { @@ -394,7 +395,7 @@ public static partial class AssetService await GenerateMetaFileAsync(path, CancellationToken.None); } - private static async ValueTask HandleFileModifiedAsync(string path) + private async ValueTask HandleFileModifiedAsync(string path) { if (!File.Exists(path)) { @@ -421,7 +422,7 @@ public static partial class AssetService } } - private static async ValueTask HandleFileDeletedAsync(string path) + private async ValueTask HandleFileDeletedAsync(string path) { var metaFileResult = GetMetaFilePath(path); if (metaFileResult.IsSuccess && File.Exists(metaFileResult.Value)) @@ -449,7 +450,7 @@ public static partial class AssetService } } - private static async ValueTask HandleFileRenamedAsync(string oldPath, string newPath) + private async ValueTask HandleFileRenamedAsync(string oldPath, string newPath) { var oldMetaPath = oldPath + Utilities.FileExtensions.META_FILE_EXTENSION; var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION; @@ -491,33 +492,33 @@ public static partial class AssetService } } - internal static void Shutdown() + internal void Shutdown() { - lock (s_initializationLock) + lock (_initializationLock) { - if (!s_initialized) + if (!_initialized) { return; } - s_watcher?.Dispose(); - s_watcher = null; + _watcher?.Dispose(); + _watcher = null; - s_commandProcessorTimer?.Dispose(); - s_commandProcessorTimer = null; + _commandProcessorTimer?.Dispose(); + _commandProcessorTimer = null; - s_dbConnection?.Close(); - s_dbConnection?.Dispose(); - s_dbConnection = null; + _dbConnection?.Close(); + _dbConnection?.Dispose(); + _dbConnection = null; - s_assetPathLookup.Clear(); - s_pathAssetLookup.Clear(); - s_dirtyAssets.Clear(); - s_waitingCommands.Clear(); - s_importerInstances.Clear(); - s_importerTypeLookup.Clear(); + _assetPathLookup.Clear(); + _pathAssetLookup.Clear(); + _dirtyAssets.Clear(); + _waitingCommands.Clear(); + _importerInstances.Clear(); + _importerTypeLookup.Clear(); - s_initialized = false; + _initialized = false; } } } diff --git a/Ghost.Editor.Core/AssetHandle/AssetImporter.cs b/Ghost.Editor.Core/AssetHandle/AssetImporter.cs index 9f04ba4..90954b2 100644 --- a/Ghost.Editor.Core/AssetHandle/AssetImporter.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetImporter.cs @@ -1,4 +1,5 @@ using Ghost.Core; +using Ghost.Editor.Core.Contracts; namespace Ghost.Editor.Core.AssetHandle; @@ -11,7 +12,7 @@ public abstract class AssetImporter /// Metadata for the asset. /// Cancellation token. /// Result indicating success or failure. - public abstract ValueTask ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default); + public abstract ValueTask ImportAsync(string assetPath, AssetMeta meta, IAssetService assetService, CancellationToken token = default); /// /// Export in-memory asset data to disk. @@ -34,12 +35,13 @@ public abstract class AssetImporter /// Dependencies are extracted from asset content during import and stored in the database. /// /// List of dependency GUIDs extracted from the asset. + /// The asset service instance. /// Result indicating if all dependencies are valid. - protected virtual ValueTask ValidateDependenciesAsync(List dependencies, CancellationToken token = default) + protected virtual ValueTask ValidateDependenciesAsync(List dependencies, IAssetService assetService, CancellationToken token = default) { foreach (var dependencyGuid in dependencies) { - var path = AssetService.GuidToPath(dependencyGuid); + var path = assetService.GuidToPath(dependencyGuid); if (path.IsFailure) { return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}")); diff --git a/Ghost.Editor.Core/AssetHandle/Importers/TextImporter.cs b/Ghost.Editor.Core/AssetHandle/Importers/TextImporter.cs index 4c72a47..13404ad 100644 --- a/Ghost.Editor.Core/AssetHandle/Importers/TextImporter.cs +++ b/Ghost.Editor.Core/AssetHandle/Importers/TextImporter.cs @@ -1,4 +1,5 @@ using Ghost.Core; +using Ghost.Editor.Core.Contracts; namespace Ghost.Editor.Core.AssetHandle.Importers; @@ -27,7 +28,7 @@ internal class TextImporterSettings : ImporterSettings [AssetImporter(".txt", ".md")] internal class TextImporter : AssetImporter { - public override async ValueTask ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default) + public override async ValueTask ImportAsync(string assetPath, AssetMeta meta, IAssetService assetService, CancellationToken token = default) { var settings = GetSettings(meta); @@ -36,7 +37,7 @@ internal class TextImporter : AssetImporter var dependencies = new List(); // Validate dependencies - var depResult = await ValidateDependenciesAsync(dependencies); + var depResult = await ValidateDependenciesAsync(dependencies, assetService, token); if (depResult.IsFailure) { return depResult; diff --git a/Ghost.Editor.Core/AssetHandle/Importers/TextureImporter.cs b/Ghost.Editor.Core/AssetHandle/Importers/TextureImporter.cs index 13f0702..168de03 100644 --- a/Ghost.Editor.Core/AssetHandle/Importers/TextureImporter.cs +++ b/Ghost.Editor.Core/AssetHandle/Importers/TextureImporter.cs @@ -1,4 +1,5 @@ using Ghost.Core; +using Ghost.Editor.Core.Contracts; using System.Text.Json; namespace Ghost.Editor.Core.AssetHandle.Importers; @@ -73,20 +74,19 @@ internal class TextureImporterSettings : ImporterSettings [AssetImporter(".png", ".jpg", ".jpeg", ".dds", ".tga", ".bmp")] internal class TextureImporter : AssetImporter { - public override async ValueTask ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default) + public override async ValueTask ImportAsync(string assetPath, AssetMeta meta, IAssetService assetService, CancellationToken token = default) { var settings = GetSettings(meta); // Textures typically don't reference other assets as dependencies - // If they did (e.g., normal maps referencing base textures), extract here - var dependencies = new List(); + //var dependencies = new List(); - // Validate dependencies - var depResult = await ValidateDependenciesAsync(dependencies, token); - if (depResult.IsFailure) - { - return depResult; - } + //// Validate dependencies + //var depResult = await ValidateDependenciesAsync(dependencies, assetService, token); + //if (depResult.IsFailure) + //{ + // return depResult; + //} try { @@ -134,7 +134,7 @@ internal class TextureImporter : AssetImporter }; // Save the imported asset data - var saveResult = AssetService.SaveImportedAsset(meta.Guid, textureAsset); + var saveResult = assetService.SaveImportedAsset(meta.Guid, textureAsset); if (saveResult.IsFailure) { return Result.Failure($"Failed to save texture asset: {saveResult.Message}"); diff --git a/Ghost.Editor.Core/Contracts/IAssetService.cs b/Ghost.Editor.Core/Contracts/IAssetService.cs index bcc0901..b07138d 100644 --- a/Ghost.Editor.Core/Contracts/IAssetService.cs +++ b/Ghost.Editor.Core/Contracts/IAssetService.cs @@ -1,5 +1,62 @@ +using Ghost.Core; +using Ghost.Editor.Core.AssetHandle; + namespace Ghost.Editor.Core.Contracts; public interface IAssetService { + DirectoryInfo? AssetsDirectory { get; } + + // Lifecycle + Task RefreshAsync(CancellationToken token = default); + + // Dirty tracking + void MarkDirty(Guid assetGuid); + bool IsDirty(Guid assetGuid); + Guid[] GetDirtyAssets(); + void ClearDirty(Guid assetGuid); + void ClearAllDirty(); + void SetAutoRefresh(bool enabled); + + // Path <-> GUID lookup + Result PathToGuid(string assetPath); + Result GuidToPath(Guid guid); + + // Asset loading + Result LoadAsset(Guid guid) where T : Asset; + Result LoadAssetAtPath(string assetPath) where T : Asset; + void UnloadAsset(Guid guid); + void UnloadAllAssets(); + bool IsAssetLoaded(Guid guid); + (int currentSize, int maxSize) GetCacheStats(); + Result SaveImportedAsset(Guid guid, T assetData) where T : Asset; + + // Asset tags + ValueTask>> GetAssetTagsAsync(Guid guid, CancellationToken token = default); + ValueTask SetAssetTagsAsync(Guid guid, List tags, CancellationToken token = default); + + // Asset search + Task> FindAssetsByNameAsync(string namePattern, CancellationToken token = default); + Task> FindAssetsByTagAsync(string tag, CancellationToken token = default); + IReadOnlyDictionary GetAllAssets(); + + // Asset file operations + ValueTask CreateAssetAsync(string assetPath, ReadOnlyMemory content, CancellationToken token = default); + ValueTask CreateAssetAsync(string assetPath, CancellationToken token = default); + ValueTask DeleteAssetAsync(Guid guid, CancellationToken token = default); + ValueTask DeleteAssetAsync(string assetPath, CancellationToken token = default); + ValueTask MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default); + ValueTask MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default); + ValueTask> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default); + ValueTask> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default); + Result MarkDirtyAsync(Guid guid, CancellationToken token = default); + Task ImportDirtyAssetsAsync(CancellationToken token = default); + + // Importer management + Type? GetImporterType(string extension); + Dictionary GetAllImporters(); + ValueTask> ExportAssetAsync(string assetPath, T assetData, CancellationToken token = default) where T : class; + + // Asset opening + void OpenAsset(string path); } diff --git a/Ghost.Editor.Core/EditorApplication.cs b/Ghost.Editor.Core/EditorApplication.cs index 4bc20ff..67cad2a 100644 --- a/Ghost.Editor.Core/EditorApplication.cs +++ b/Ghost.Editor.Core/EditorApplication.cs @@ -1,3 +1,4 @@ +using Ghost.Editor.Core.Contracts; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; @@ -56,5 +57,9 @@ public static class EditorApplication internal static void Shutdown() { + if (s_serviceProvider?.GetService(typeof(IAssetService)) is AssetHandle.AssetService assetService) + { + assetService.Shutdown(); + } } } \ No newline at end of file diff --git a/Ghost.Editor/ActivationHandler.cs b/Ghost.Editor/ActivationHandler.cs index a7296c6..94a7045 100644 --- a/Ghost.Editor/ActivationHandler.cs +++ b/Ghost.Editor/ActivationHandler.cs @@ -1,4 +1,5 @@ using Ghost.Editor.Core; +using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Utilities; using Ghost.Editor.Models; using Ghost.Engine; @@ -61,7 +62,9 @@ internal static class ActivationHandler ((EngineCore)App.GetService()).Init(); }); - // TODO: Initialize other subsystems here. + await ((Core.AssetHandle.AssetService)App.GetService()).Init(); + + // TODO: Init other subsystems here. // await Task.Delay(10000); // Wait 10 seconds to simulate work. } } \ No newline at end of file diff --git a/Ghost.Editor/App.xaml.cs b/Ghost.Editor/App.xaml.cs index f76e075..f14ff4c 100644 --- a/Ghost.Editor/App.xaml.cs +++ b/Ghost.Editor/App.xaml.cs @@ -1,5 +1,6 @@ using Ghost.Core; using Ghost.Editor.Core; +using Ghost.Editor.Core.AssetHandle; using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Services; using Ghost.Editor.View.Pages.EngineEditor; diff --git a/Ghost.Editor/ViewModels/Controls/ProjectBrowserViewModel.cs b/Ghost.Editor/ViewModels/Controls/ProjectBrowserViewModel.cs index 9a7da24..b0d6a81 100644 --- a/Ghost.Editor/ViewModels/Controls/ProjectBrowserViewModel.cs +++ b/Ghost.Editor/ViewModels/Controls/ProjectBrowserViewModel.cs @@ -11,6 +11,7 @@ namespace Ghost.Editor.ViewModels.Controls; internal partial class ProjectBrowserViewModel : ObservableObject { private readonly IInspectorService _inspectorService; + private readonly IAssetService _assetService; private readonly Dictionary _pathToDirectoryItemMap = new(); private ExplorerItem? _selectedItem; @@ -40,9 +41,10 @@ internal partial class ProjectBrowserViewModel : ObservableObject get; set; } = string.Empty; - public ProjectBrowserViewModel(IInspectorService inspectorService) + public ProjectBrowserViewModel(IInspectorService inspectorService, IAssetService assetService) { _inspectorService = inspectorService; + _assetService = assetService; var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); LoadSubFolderRecursive(assetsRootItem); @@ -108,7 +110,7 @@ internal partial class ProjectBrowserViewModel : ObservableObject } else { - AssetService.OpenAsset(SelectedItem.FullName); + _assetService.OpenAsset(SelectedItem.FullName); return (null, 1); } } diff --git a/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs b/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs index 2b56b03..6a199e8 100644 --- a/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs +++ b/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs @@ -1,6 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Ghost.Editor.Core; using Ghost.Editor.Core.AssetHandle; +using Ghost.Editor.Core.Contracts; using Ghost.Editor.Models; using System.Collections.ObjectModel; @@ -8,6 +9,8 @@ namespace Ghost.Editor.ViewModels.Pages.EngineEditor; internal partial class ProjectViewModel : ObservableObject { + private readonly IAssetService _assetService; + public ObservableCollection SubDirectories { get; @@ -34,8 +37,10 @@ internal partial class ProjectViewModel : ObservableObject set; } - public ProjectViewModel() + public ProjectViewModel(IAssetService assetService) { + _assetService = assetService; + var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); LoadSubFolderRecursive(ref assetsRootItem); @@ -124,7 +129,7 @@ internal partial class ProjectViewModel : ObservableObject } else { - AssetService.OpenAsset(SelectedAsset.FullName); + _assetService.OpenAsset(SelectedAsset.FullName); } } diff --git a/Ghost.UnitTest/AssetDatabaseIntegrationTest.cs b/Ghost.UnitTest/AssetDatabaseIntegrationTest.cs index f77a161..e1c30e6 100644 --- a/Ghost.UnitTest/AssetDatabaseIntegrationTest.cs +++ b/Ghost.UnitTest/AssetDatabaseIntegrationTest.cs @@ -49,7 +49,7 @@ public class AssetDatabaseIntegrationTest var projectMetadataInfo = new Data.Models.ProjectMetadataInfo(projectPath, metadata); ProjectService.CurrentProject = projectMetadataInfo; - // Initialize AssetService + // Init AssetService await AssetService.Initialize(TestContext.CancellationToken); // Give the file system watcher time to start