Modify AssetService

This commit is contained in:
2026-02-05 19:25:48 +09:00
parent 9bbccfc8f8
commit 426786397c
18 changed files with 332 additions and 261 deletions

View File

@@ -2,7 +2,7 @@ using Ghost.Core;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetService public partial class AssetService
{ {
/// <summary> /// <summary>
/// Create a new asset at the specified path. /// Create a new asset at the specified path.
@@ -11,7 +11,7 @@ public static partial class AssetService
/// <param name="assetPath">Path to create the asset at.</param> /// <param name="assetPath">Path to create the asset at.</param>
/// <param name="content">Content to write to the asset file.</param> /// <param name="content">Content to write to the asset file.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> CreateAssetAsync(string assetPath, ReadOnlyMemory<byte> content, CancellationToken token = default) public async ValueTask<Result> CreateAssetAsync(string assetPath, ReadOnlyMemory<byte> content, CancellationToken token = default)
{ {
if (AssetsDirectory == null) if (AssetsDirectory == null)
{ {
@@ -57,7 +57,7 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="assetPath">Path to create the asset at.</param> /// <param name="assetPath">Path to create the asset at.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> CreateAssetAsync(string assetPath, CancellationToken token = default) public ValueTask<Result> CreateAssetAsync(string assetPath, CancellationToken token = default)
{ {
return CreateAssetAsync(assetPath, ReadOnlyMemory<byte>.Empty, token); return CreateAssetAsync(assetPath, ReadOnlyMemory<byte>.Empty, token);
} }
@@ -67,7 +67,7 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="guid">GUID of the asset to delete.</param> /// <param name="guid">GUID of the asset to delete.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> DeleteAssetAsync(Guid guid, CancellationToken token = default) public async ValueTask<Result> DeleteAssetAsync(Guid guid, CancellationToken token = default)
{ {
var pathResult = GuidToPath(guid); var pathResult = GuidToPath(guid);
if (pathResult.IsFailure) if (pathResult.IsFailure)
@@ -114,7 +114,7 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="assetPath">Path to the asset to delete.</param> /// <param name="assetPath">Path to the asset to delete.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> DeleteAssetAsync(string assetPath, CancellationToken token = default) public ValueTask<Result> DeleteAssetAsync(string assetPath, CancellationToken token = default)
{ {
var guidResult = PathToGuid(assetPath); var guidResult = PathToGuid(assetPath);
if (guidResult.IsFailure) if (guidResult.IsFailure)
@@ -131,7 +131,7 @@ public static partial class AssetService
/// <param name="guid">GUID of the asset to move.</param> /// <param name="guid">GUID of the asset to move.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param> /// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default) public async ValueTask<Result> MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default)
{ {
var oldPathResult = GuidToPath(guid); var oldPathResult = GuidToPath(guid);
if (oldPathResult.IsFailure) if (oldPathResult.IsFailure)
@@ -211,7 +211,7 @@ public static partial class AssetService
/// <param name="oldPath">CurrentApplication path of the asset.</param> /// <param name="oldPath">CurrentApplication path of the asset.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param> /// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default) public ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default)
{ {
var guidResult = PathToGuid(oldPath); var guidResult = PathToGuid(oldPath);
if (guidResult.IsFailure) if (guidResult.IsFailure)
@@ -228,7 +228,7 @@ public static partial class AssetService
/// <param name="guid">GUID of the asset to copy.</param> /// <param name="guid">GUID of the asset to copy.</param>
/// <param name="newPath">New path for the copied asset (relative or absolute).</param> /// <param name="newPath">New path for the copied asset (relative or absolute).</param>
/// <returns>Result containing the new asset's GUID.</returns> /// <returns>Result containing the new asset's GUID.</returns>
public static async ValueTask<Result<Guid>> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default) public async ValueTask<Result<Guid>> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default)
{ {
var oldPathResult = GuidToPath(guid); var oldPathResult = GuidToPath(guid);
if (oldPathResult.IsFailure) if (oldPathResult.IsFailure)
@@ -299,7 +299,7 @@ public static partial class AssetService
/// <param name="sourcePath">Path of the asset to copy.</param> /// <param name="sourcePath">Path of the asset to copy.</param>
/// <param name="destPath">New path for the copied asset (relative or absolute).</param> /// <param name="destPath">New path for the copied asset (relative or absolute).</param>
/// <returns>Result containing the new asset's GUID.</returns> /// <returns>Result containing the new asset's GUID.</returns>
public static ValueTask<Result<Guid>> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default) public ValueTask<Result<Guid>> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default)
{ {
var guidResult = PathToGuid(sourcePath); var guidResult = PathToGuid(sourcePath);
if (guidResult.IsFailure) if (guidResult.IsFailure)
@@ -315,7 +315,7 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="guid">GUID of the asset to mark dirty.</param> /// <param name="guid">GUID of the asset to mark dirty.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static Result MarkDirtyAsync(Guid guid, CancellationToken token = default) public Result MarkDirtyAsync(Guid guid, CancellationToken token = default)
{ {
MarkDirty(guid); MarkDirty(guid);
return Result.Success(); return Result.Success();
@@ -325,7 +325,7 @@ public static partial class AssetService
/// Import all dirty assets. /// Import all dirty assets.
/// </summary> /// </summary>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static async Task<Result> ImportDirtyAssetsAsync(CancellationToken token = default) public async Task<Result> ImportDirtyAssetsAsync(CancellationToken token = default)
{ {
var dirtyGuids = GetDirtyAssets(); var dirtyGuids = GetDirtyAssets();

View File

@@ -3,27 +3,27 @@ using System.Reflection;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetService public partial class AssetService
{ {
private static readonly Dictionary<Type, AssetImporter> s_importerInstances = new(); private readonly Dictionary<Type, AssetImporter> _importerInstances = new();
/// <summary> /// <summary>
/// Import an asset at the specified path. /// Import an asset at the specified path.
/// </summary> /// </summary>
/// <param name="assetPath">Full path to the asset file.</param> /// <param name="assetPath">Full path to the asset file.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
private static async ValueTask<Result> ImportAssetAsync(string assetPath, CancellationToken token = default) private async ValueTask<Result> ImportAssetAsync(string assetPath, CancellationToken token = default)
{ {
var extension = Path.GetExtension(assetPath); 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 // No importer registered for this file type
return Result.Success(); return Result.Success();
} }
// Get or create importer instance // 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; importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
if (importerInstance is null) 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}"); return Result.Failure($"Failed to create importer instance for type {importerType.Name}");
} }
s_importerInstances[importerType] = importerInstance; _importerInstances[importerType] = importerInstance;
} }
// Read metadata // Read metadata
@@ -41,7 +41,7 @@ public static partial class AssetService
return Result.Failure($"Failed to read asset metadata: {metaResult.Message}"); 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);
} }
/// <summary> /// <summary>
@@ -49,9 +49,9 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="extension">File extension (e.g., ".png").</param> /// <param name="extension">File extension (e.g., ".png").</param>
/// <returns>The importer type if found, otherwise null.</returns> /// <returns>The importer type if found, otherwise null.</returns>
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; return importerType;
} }
@@ -59,9 +59,9 @@ public static partial class AssetService
/// Get all registered importer types and their supported extensions. /// Get all registered importer types and their supported extensions.
/// </summary> /// </summary>
/// <returns>Dictionary mapping extensions to importer types.</returns> /// <returns>Dictionary mapping extensions to importer types.</returns>
public static Dictionary<string, Type> GetAllImporters() public Dictionary<string, Type> GetAllImporters()
{ {
return new Dictionary<string, Type>(s_importerTypeLookup); return new Dictionary<string, Type>(_importerTypeLookup);
} }
/// <summary> /// <summary>
@@ -72,17 +72,18 @@ public static partial class AssetService
/// <param name="assetPath">Full path where the asset should be saved.</param> /// <param name="assetPath">Full path where the asset should be saved.</param>
/// <param name="assetData">In-memory asset data to export.</param> /// <param name="assetData">In-memory asset data to export.</param>
/// <returns>Result with the GUID of the exported asset.</returns> /// <returns>Result with the GUID of the exported asset.</returns>
public static async ValueTask<Result<Guid>> ExportAssetAsync<T>(string assetPath, T assetData, CancellationToken token = default) where T : class public async ValueTask<Result<Guid>> ExportAssetAsync<T>(string assetPath, T assetData, CancellationToken token = default)
where T : class
{ {
var extension = Path.GetExtension(assetPath); var extension = Path.GetExtension(assetPath);
if (!s_importerTypeLookup.TryGetValue(extension, out var importerType)) if (!_importerTypeLookup.TryGetValue(extension, out var importerType))
{ {
return Result<Guid>.Failure($"No importer registered for extension {extension}"); return Result<Guid>.Failure($"No importer registered for extension {extension}");
} }
// Get or create importer instance // 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; importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
if (importerInstance is null) if (importerInstance is null)
@@ -90,14 +91,7 @@ public static partial class AssetService
return Result<Guid>.Failure($"Failed to create importer instance for type {importerType.Name}"); return Result<Guid>.Failure($"Failed to create importer instance for type {importerType.Name}");
} }
s_importerInstances[importerType] = importerInstance; _importerInstances[importerType] = importerInstance;
}
// Find and invoke the ExportAsync method
var exportMethod = importerType.GetMethod("ExportAsync", BindingFlags.Public | BindingFlags.Instance);
if (exportMethod == null)
{
return Result<Guid>.Failure($"ExportAsync method not found on importer {importerType.Name}. This importer does not support exporting.");
} }
// Generate metadata for the new asset // Generate metadata for the new asset

View File

@@ -4,21 +4,21 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetService public partial class AssetService
{ {
// Asset cache - stores loaded assets by GUID // Asset cache - stores loaded assets by GUID
private static readonly ConcurrentDictionary<Guid, Asset> s_assetCache = new(); private readonly ConcurrentDictionary<Guid, Asset> _assetCache = new();
// LRU tracking - stores access time for each cached asset // LRU tracking - stores access time for each cached asset
private static readonly ConcurrentDictionary<Guid, DateTime> s_assetAccessTime = new(); private readonly ConcurrentDictionary<Guid, DateTime> _assetAccessTime = new();
// Maximum number of cached assets before eviction starts // 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%) // Percentage of cache to evict when limit is reached (evict oldest 20%)
private const float _CACHE_EVICTION_PERCENTAGE = 0.2f; private const float _CACHE_EVICTION_PERCENTAGE = 0.2f;
private static Result<string> GetImportedAssetsDirectory() private Result<string> GetImportedAssetsDirectory()
{ {
if (AssetsDirectory == null) if (AssetsDirectory == null)
{ {
@@ -34,7 +34,7 @@ public static partial class AssetService
return cacheDir; return cacheDir;
} }
private static Result<string> GetImportedAssetPath(Guid guid) private Result<string> GetImportedAssetPath(Guid guid)
{ {
var importedDirResult = GetImportedAssetsDirectory(); var importedDirResult = GetImportedAssetsDirectory();
if (importedDirResult.IsFailure) if (importedDirResult.IsFailure)
@@ -47,13 +47,13 @@ public static partial class AssetService
return assetDataPath; return assetDataPath;
} }
private static Result<T> LoadAssetInternal<T>(Guid guid) where T : Asset private Result<T> LoadAssetInternal<T>(Guid guid) where T : Asset
{ {
// Check cache first // Check cache first
if (s_assetCache.TryGetValue(guid, out var cachedAsset)) if (_assetCache.TryGetValue(guid, out var cachedAsset))
{ {
// Update access time for LRU // Update access time for LRU
s_assetAccessTime[guid] = DateTime.UtcNow; _assetAccessTime[guid] = DateTime.UtcNow;
if (cachedAsset is T typedAsset) if (cachedAsset is T typedAsset)
{ {
@@ -98,7 +98,7 @@ public static partial class AssetService
} }
} }
public static Result<T> LoadAssetAtPath<T>(string assetPath) where T : Asset public Result<T> LoadAssetAtPath<T>(string assetPath) where T : Asset
{ {
var guidResult = PathToGuid(assetPath); var guidResult = PathToGuid(assetPath);
if (guidResult.IsFailure) if (guidResult.IsFailure)
@@ -109,24 +109,24 @@ public static partial class AssetService
return LoadAsset<T>(guidResult.Value); return LoadAsset<T>(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 // Check if we need to evict old assets
if (s_assetCache.Count >= MAX_CACHED_ASSETS) if (_assetCache.Count >= _MAX_CACHED_ASSETS)
{ {
EvictOldestAssets(); EvictOldestAssets();
} }
s_assetCache[guid] = asset; _assetCache[guid] = asset;
s_assetAccessTime[guid] = DateTime.UtcNow; _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 // Sort by access time and remove oldest entries
var oldestAssets = s_assetAccessTime var oldestAssets = _assetAccessTime
.OrderBy(kvp => kvp.Value) .OrderBy(kvp => kvp.Value)
.Take(evictionCount) .Take(evictionCount)
.Select(kvp => kvp.Key) .Select(kvp => kvp.Key)
@@ -134,8 +134,8 @@ public static partial class AssetService
foreach (var guid in oldestAssets) foreach (var guid in oldestAssets)
{ {
s_assetCache.TryRemove(guid, out _); _assetCache.TryRemove(guid, out _);
s_assetAccessTime.TryRemove(guid, out _); _assetAccessTime.TryRemove(guid, out _);
} }
} }
@@ -143,19 +143,19 @@ public static partial class AssetService
/// Unload a specific asset from cache. /// Unload a specific asset from cache.
/// </summary> /// </summary>
/// <param name="guid">GUID of the asset to unload.</param> /// <param name="guid">GUID of the asset to unload.</param>
public static void UnloadAsset(Guid guid) public void UnloadAsset(Guid guid)
{ {
s_assetCache.TryRemove(guid, out _); _assetCache.TryRemove(guid, out _);
s_assetAccessTime.TryRemove(guid, out _); _assetAccessTime.TryRemove(guid, out _);
} }
/// <summary> /// <summary>
/// Unload all assets from cache. /// Unload all assets from cache.
/// </summary> /// </summary>
public static void UnloadAllAssets() public void UnloadAllAssets()
{ {
s_assetCache.Clear(); _assetCache.Clear();
s_assetAccessTime.Clear(); _assetAccessTime.Clear();
} }
/// <summary> /// <summary>
@@ -163,18 +163,18 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="guid">GUID of the asset.</param> /// <param name="guid">GUID of the asset.</param>
/// <returns>True if the asset is in cache.</returns> /// <returns>True if the asset is in cache.</returns>
public static bool IsAssetLoaded(Guid guid) public bool IsAssetLoaded(Guid guid)
{ {
return s_assetCache.ContainsKey(guid); return _assetCache.ContainsKey(guid);
} }
/// <summary> /// <summary>
/// Get cache statistics. /// Get cache statistics.
/// </summary> /// </summary>
/// <returns>Tuple of (current cache size, max cache size).</returns> /// <returns>Tuple of (current cache size, max cache size).</returns>
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);
} }
/// <summary> /// <summary>
@@ -185,7 +185,7 @@ public static partial class AssetService
/// <param name="guid">GUID of the asset.</param> /// <param name="guid">GUID of the asset.</param>
/// <param name="assetData">Processed asset data to save.</param> /// <param name="assetData">Processed asset data to save.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static Result SaveImportedAsset<T>(Guid guid, T assetData) public Result SaveImportedAsset<T>(Guid guid, T assetData)
where T : Asset where T : Asset
{ {
var assetPathResult = GetImportedAssetPath(guid); var assetPathResult = GetImportedAssetPath(guid);
@@ -196,7 +196,7 @@ public static partial class AssetService
try try
{ {
var json = JsonSerializer.Serialize(assetData, s_defaultJsonOptions); var json = JsonSerializer.Serialize(assetData, _defaultJsonOptions);
File.WriteAllText(assetPathResult.Value, json); File.WriteAllText(assetPathResult.Value, json);
// Invalidate cache for this asset so it gets reloaded next time // Invalidate cache for this asset so it gets reloaded next time

View File

@@ -3,12 +3,12 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetService public partial class AssetService
{ {
/// <summary> /// <summary>
/// Get the relative path from the assets directory. /// Get the relative path from the assets directory.
/// </summary> /// </summary>
private static Result<string> GetRelativePath(string fullPath) private Result<string> GetRelativePath(string fullPath)
{ {
if (AssetsDirectory == null) if (AssetsDirectory == null)
{ {
@@ -26,7 +26,7 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Get the full path from a relative path. /// Get the full path from a relative path.
/// </summary> /// </summary>
private static Result<string> GetFullPath(string relativePath) private Result<string> GetFullPath(string relativePath)
{ {
if (AssetsDirectory == null) if (AssetsDirectory == null)
{ {
@@ -41,7 +41,7 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="assetPath">Full or relative path to the asset.</param> /// <param name="assetPath">Full or relative path to the asset.</param>
/// <returns>The GUID of the asset if found.</returns> /// <returns>The GUID of the asset if found.</returns>
public static Result<Guid> PathToGuid(string assetPath) public Result<Guid> PathToGuid(string assetPath)
{ {
var relativePath = assetPath; var relativePath = assetPath;
@@ -59,9 +59,9 @@ public static partial class AssetService
// Normalize path separators // Normalize path separators
relativePath = relativePath.Replace('\\', '/'); 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; return guid;
} }
@@ -75,11 +75,11 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="guid">GUID of the asset.</param> /// <param name="guid">GUID of the asset.</param>
/// <returns>The relative path to the asset if found.</returns> /// <returns>The relative path to the asset if found.</returns>
public static Result<string> GuidToPath(Guid guid) public Result<string> 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; return path;
} }
@@ -94,7 +94,7 @@ public static partial class AssetService
/// <typeparam name="T">Type of asset to load.</typeparam> /// <typeparam name="T">Type of asset to load.</typeparam>
/// <param name="guid">GUID of the asset.</param> /// <param name="guid">GUID of the asset.</param>
/// <returns>The loaded asset.</returns> /// <returns>The loaded asset.</returns>
public static Result<T> LoadAsset<T>(Guid guid) where T : Asset public Result<T> LoadAsset<T>(Guid guid) where T : Asset
{ {
// Implemented in AssetService.Loader.cs // Implemented in AssetService.Loader.cs
return LoadAssetInternal<T>(guid); return LoadAssetInternal<T>(guid);
@@ -105,7 +105,7 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="guid">GUID of the asset.</param> /// <param name="guid">GUID of the asset.</param>
/// <returns>List of tags associated with the asset.</returns> /// <returns>List of tags associated with the asset.</returns>
public static async ValueTask<Result<List<string>>> GetAssetTagsAsync(Guid guid, CancellationToken token = default) public async ValueTask<Result<List<string>>> GetAssetTagsAsync(Guid guid, CancellationToken token = default)
{ {
var pathResult = GuidToPath(guid); var pathResult = GuidToPath(guid);
if (pathResult.IsFailure) if (pathResult.IsFailure)
@@ -134,7 +134,7 @@ public static partial class AssetService
/// <param name="guid">GUID of the asset.</param> /// <param name="guid">GUID of the asset.</param>
/// <param name="tags">New tags for the asset.</param> /// <param name="tags">New tags for the asset.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> SetAssetTagsAsync(Guid guid, List<string> tags, CancellationToken token = default) public async ValueTask<Result> SetAssetTagsAsync(Guid guid, List<string> tags, CancellationToken token = default)
{ {
var pathResult = GuidToPath(guid); var pathResult = GuidToPath(guid);
if (pathResult.IsFailure) if (pathResult.IsFailure)
@@ -174,7 +174,7 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="namePattern">Search pattern (e.g., "*.txt", "player?", "test*").</param> /// <param name="namePattern">Search pattern (e.g., "*.txt", "player?", "test*").</param>
/// <returns>List of matching asset GUIDs.</returns> /// <returns>List of matching asset GUIDs.</returns>
public static async Task<List<Guid>> FindAssetsByNameAsync(string namePattern, CancellationToken token = default) public async Task<List<Guid>> FindAssetsByNameAsync(string namePattern, CancellationToken token = default)
{ {
return await GetAssetsByNameAsync(namePattern, token); return await GetAssetsByNameAsync(namePattern, token);
} }
@@ -184,7 +184,7 @@ public static partial class AssetService
/// </summary> /// </summary>
/// <param name="tag">Tag to search for.</param> /// <param name="tag">Tag to search for.</param>
/// <returns>List of asset GUIDs with the specified tag.</returns> /// <returns>List of asset GUIDs with the specified tag.</returns>
public static async Task<List<Guid>> FindAssetsByTagAsync(string tag, CancellationToken token = default) public async Task<List<Guid>> FindAssetsByTagAsync(string tag, CancellationToken token = default)
{ {
return await GetAssetsByTagAsync(tag, token); return await GetAssetsByTagAsync(tag, token);
} }
@@ -193,11 +193,11 @@ public static partial class AssetService
/// Get all assets in the database. /// Get all assets in the database.
/// </summary> /// </summary>
/// <returns>Dictionary mapping GUIDs to relative paths.</returns> /// <returns>Dictionary mapping GUIDs to relative paths.</returns>
public static IReadOnlyDictionary<Guid, string> GetAllAssets() public IReadOnlyDictionary<Guid, string> GetAllAssets()
{ {
lock (s_dbLock) lock (_dbLock)
{ {
return s_assetPathLookup.AsReadOnly(); return _assetPathLookup.AsReadOnly();
} }
} }
} }

View File

@@ -7,13 +7,13 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetService public partial class AssetService
{ {
private static readonly Dictionary<string, Type> s_importerTypeLookup = new(); private readonly Dictionary<string, Type> _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."); 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<AssetImporterAttribute>()!; var attribute = type.GetCustomAttribute<AssetImporterAttribute>()!;
foreach (var extension in attribute.SupportedExtensions) foreach (var extension in attribute.SupportedExtensions)
{ {
s_importerTypeLookup[extension] = type; _importerTypeLookup[extension] = type;
} }
} }
s_watcher.Created += OnFSEvent; _watcher.Created += OnFSEvent;
s_watcher.Deleted += OnFSEvent; _watcher.Deleted += OnFSEvent;
s_watcher.Changed += OnFSEvent; _watcher.Changed += OnFSEvent;
s_watcher.Renamed += OnAssetRenamed; _watcher.Renamed += OnAssetRenamed;
} }
private static Result<string> GetMetaFilePath(string assetPath) private Result<string> GetMetaFilePath(string assetPath)
{ {
if (Directory.Exists(assetPath)) if (Directory.Exists(assetPath))
{ {
@@ -49,11 +49,11 @@ public static partial class AssetService
return assetPath + FileExtensions.META_FILE_EXTENSION; return assetPath + FileExtensions.META_FILE_EXTENSION;
} }
private static ImporterSettings? GetDefaultSettingsForAsset(string assetPath) private ImporterSettings? GetDefaultSettingsForAsset(string assetPath)
{ {
var extension = Path.GetExtension(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]; var settingsType = importerType.BaseType?.GetGenericArguments()[0];
if (settingsType == null || !typeof(ImporterSettings).IsAssignableFrom(settingsType)) if (settingsType == null || !typeof(ImporterSettings).IsAssignableFrom(settingsType))
@@ -70,7 +70,7 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Calculate SHA256 hash of a file for change detection. /// Calculate SHA256 hash of a file for change detection.
/// </summary> /// </summary>
private static async Task<string> CalculateFileHashAsync(string filePath, CancellationToken token = default) private async Task<string> CalculateFileHashAsync(string filePath, CancellationToken token = default)
{ {
try try
{ {
@@ -84,12 +84,12 @@ public static partial class AssetService
} }
} }
private static async Task<Result> WriteMetaFileAsync(string metaFilePath, AssetMeta metaData, CancellationToken token = default) private async Task<Result> WriteMetaFileAsync(string metaFilePath, AssetMeta metaData, CancellationToken token = default)
{ {
try try
{ {
await using var fileStream = File.Create(metaFilePath); 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(); return Result.Success();
} }
catch (Exception ex) catch (Exception ex)
@@ -101,7 +101,7 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Read metadata from a .gmeta file. /// Read metadata from a .gmeta file.
/// </summary> /// </summary>
private static async ValueTask<Result<AssetMeta>> ReadMetaFileAsync(string assetPath, CancellationToken token = default) private async ValueTask<Result<AssetMeta>> ReadMetaFileAsync(string assetPath, CancellationToken token = default)
{ {
var metaFileResult = GetMetaFilePath(assetPath); var metaFileResult = GetMetaFilePath(assetPath);
if (metaFileResult.IsFailure) if (metaFileResult.IsFailure)
@@ -117,7 +117,7 @@ public static partial class AssetService
try try
{ {
await using var fileStream = File.OpenRead(metaFileResult.Value); await using var fileStream = File.OpenRead(metaFileResult.Value);
var meta = await JsonSerializer.DeserializeAsync<AssetMeta>(fileStream, s_defaultJsonOptions, token); var meta = await JsonSerializer.DeserializeAsync<AssetMeta>(fileStream, _defaultJsonOptions, token);
if (meta == null) if (meta == null)
{ {
return Result<AssetMeta>.Failure("Failed to deserialize metadata"); return Result<AssetMeta>.Failure("Failed to deserialize metadata");
@@ -131,7 +131,7 @@ public static partial class AssetService
} }
} }
internal static async ValueTask<Result> GenerateMetaFileAsync(string assetPath, CancellationToken token = default) internal async ValueTask<Result> GenerateMetaFileAsync(string assetPath, CancellationToken token = default)
{ {
Result r; Result r;
@@ -147,7 +147,7 @@ public static partial class AssetService
if (existingMetaResult.IsSuccess) if (existingMetaResult.IsSuccess)
{ {
var existingMeta = existingMetaResult.Value; 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); var relResult = GetRelativePath(assetPath);
if (relResult.IsSuccess && assetPath != path) if (relResult.IsSuccess && assetPath != path)
@@ -196,12 +196,12 @@ public static partial class AssetService
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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); 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)) if (IsMetaFile(e.FullPath))
{ {
@@ -219,7 +219,7 @@ public static partial class AssetService
await PostCommandAsync(new AssetCommand(type, e.FullPath, Timestamp: DateTime.UtcNow)); 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)) if (IsMetaFile(e.FullPath))
{ {
@@ -232,7 +232,7 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Mark all assets that depend on the specified asset as dirty. /// Mark all assets that depend on the specified asset as dirty.
/// </summary> /// </summary>
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. // TODO: We should have a reverse dependency lookup in the database to avoid scanning all assets.

View File

@@ -5,11 +5,11 @@ using System.Reflection;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetService public partial class AssetService
{ {
private static readonly Dictionary<string, Action<string>> s_assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, Action<string>> _assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase);
private static void InitializeAssetHandle() private void InitializeAssetHandle()
{ {
var methods = TypeCache.GetTypes() var methods = TypeCache.GetTypes()
.SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
@@ -23,20 +23,20 @@ public static partial class AssetService
var del = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), method); var del = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), method);
foreach (var ext in attr.Extensions) 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."); 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); var extension = Path.GetExtension(path);
if (s_assetOpenHandlers.TryGetValue(extension, out var handler)) if (_assetOpenHandlers.TryGetValue(extension, out var handler))
{ {
handler(path); handler(path);
} }

View File

@@ -4,14 +4,14 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetService public partial class AssetService
{ {
private static SqliteConnection? s_dbConnection; private SqliteConnection? _dbConnection;
/// <summary> /// <summary>
/// Initialize the SQLite database for asset caching. /// Init the SQLite database for asset caching.
/// </summary> /// </summary>
private static async Task InitializeDatabaseAsync(CancellationToken token = default) private async Task InitializeDatabaseAsync(CancellationToken token = default)
{ {
if (AssetsDirectory == null) if (AssetsDirectory == null)
{ {
@@ -32,11 +32,11 @@ public static partial class AssetService
Cache = SqliteCacheMode.Shared Cache = SqliteCacheMode.Shared
}.ToString(); }.ToString();
s_dbConnection = new SqliteConnection(connectionString); _dbConnection = new SqliteConnection(connectionString);
await s_dbConnection.OpenAsync(token); await _dbConnection.OpenAsync(token);
// Create tables // Create tables
await using var cmd = s_dbConnection.CreateCommand(); await using var cmd = _dbConnection.CreateCommand();
cmd.CommandText = @" cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Assets ( CREATE TABLE IF NOT EXISTS Assets (
Guid TEXT PRIMARY KEY, Guid TEXT PRIMARY KEY,
@@ -60,9 +60,9 @@ public static partial class AssetService
/// <param name="meta">Asset metadata from .gmeta file.</param> /// <param name="meta">Asset metadata from .gmeta file.</param>
/// <param name="fileHash">SHA256 hash of the asset file content.</param> /// <param name="fileHash">SHA256 hash of the asset file content.</param>
/// <param name="dependencies">List of GUIDs this asset depends on (extracted during import).</param> /// <param name="dependencies">List of GUIDs this asset depends on (extracted during import).</param>
private static async ValueTask<Result> UpsertAssetAsync(string assetPath, AssetMeta meta, string fileHash, List<Guid>? dependencies = null, CancellationToken token = default) private async ValueTask<Result> UpsertAssetAsync(string assetPath, AssetMeta meta, string fileHash, List<Guid>? dependencies = null, CancellationToken token = default)
{ {
if (s_dbConnection == null) if (_dbConnection == null)
{ {
return Result.Failure("Database not initialized"); return Result.Failure("Database not initialized");
} }
@@ -75,21 +75,21 @@ public static partial class AssetService
try try
{ {
lock (s_dbLock) lock (_dbLock)
{ {
// If this GUID already exists with a different path, remove the old path mapping // 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) // Update lookups with new path (normalize path separators for consistency)
var normalizedPath = relativePath.Value.Replace('\\', '/'); var normalizedPath = relativePath.Value.Replace('\\', '/');
s_assetPathLookup[meta.Guid] = normalizedPath; _assetPathLookup[meta.Guid] = normalizedPath;
s_pathAssetLookup[normalizedPath] = meta.Guid; _pathAssetLookup[normalizedPath] = meta.Guid;
} }
await using var cmd = s_dbConnection.CreateCommand(); await using var cmd = _dbConnection.CreateCommand();
cmd.CommandText = @" cmd.CommandText = @"
INSERT OR REPLACE INTO Assets (Guid, Path, Version, Tags, FileHash, DependencyGuids, LastModified) INSERT OR REPLACE INTO Assets (Guid, Path, Version, Tags, FileHash, DependencyGuids, LastModified)
VALUES (@guid, @path, @version, @tags, @fileHash, @deps, @modified) VALUES (@guid, @path, @version, @tags, @fileHash, @deps, @modified)
@@ -114,25 +114,25 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Remove an asset from the database. /// Remove an asset from the database.
/// </summary> /// </summary>
private static async Task<Result> RemoveAssetFromDatabaseAsync(Guid guid, CancellationToken token = default) private async Task<Result> RemoveAssetFromDatabaseAsync(Guid guid, CancellationToken token = default)
{ {
if (s_dbConnection == null) if (_dbConnection == null)
{ {
return Result.Failure("Database not initialized"); return Result.Failure("Database not initialized");
} }
try 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); _assetPathLookup.Remove(guid);
s_pathAssetLookup.Remove(path); _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.CommandText = "DELETE FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString()); cmd.Parameters.AddWithValue("@guid", guid.ToString());
@@ -150,16 +150,16 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Load all assets from the database into memory cache. /// Load all assets from the database into memory cache.
/// </summary> /// </summary>
private static async Task LoadAssetCacheFromDatabaseAsync(CancellationToken token = default) private async Task LoadAssetCacheFromDatabaseAsync(CancellationToken token = default)
{ {
if (s_dbConnection == null) if (_dbConnection == null)
{ {
return; return;
} }
try try
{ {
await using var cmd = s_dbConnection.CreateCommand(); await using var cmd = _dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Path FROM Assets"; cmd.CommandText = "SELECT Guid, Path FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token); await using var reader = await cmd.ExecuteReaderAsync(token);
@@ -170,10 +170,10 @@ public static partial class AssetService
if (Guid.TryParse(guidStr, out var guid)) if (Guid.TryParse(guidStr, out var guid))
{ {
lock (s_dbLock) lock (_dbLock)
{ {
s_assetPathLookup[guid] = path; _assetPathLookup[guid] = path;
s_pathAssetLookup[path] = guid; _pathAssetLookup[path] = guid;
} }
} }
} }
@@ -187,18 +187,18 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Get assets by tag. /// Get assets by tag.
/// </summary> /// </summary>
private static async Task<List<Guid>> GetAssetsByTagAsync(string tag, CancellationToken token = default) private async Task<List<Guid>> GetAssetsByTagAsync(string tag, CancellationToken token = default)
{ {
var result = new List<Guid>(); var result = new List<Guid>();
if (s_dbConnection == null) if (_dbConnection == null)
{ {
return result; return result;
} }
try try
{ {
await using var cmd = s_dbConnection.CreateCommand(); await using var cmd = _dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Tags FROM Assets"; cmd.CommandText = "SELECT Guid, Tags FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token); await using var reader = await cmd.ExecuteReaderAsync(token);
@@ -228,16 +228,16 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Get the file hash for an asset from the database. /// Get the file hash for an asset from the database.
/// </summary> /// </summary>
private static async Task<string?> GetFileHashAsync(Guid guid, CancellationToken token = default) private async Task<string?> GetFileHashAsync(Guid guid, CancellationToken token = default)
{ {
if (s_dbConnection == null) if (_dbConnection == null)
{ {
return null; return null;
} }
try try
{ {
await using var cmd = s_dbConnection.CreateCommand(); await using var cmd = _dbConnection.CreateCommand();
cmd.CommandText = "SELECT FileHash FROM Assets WHERE Guid = @guid"; cmd.CommandText = "SELECT FileHash FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString()); cmd.Parameters.AddWithValue("@guid", guid.ToString());
@@ -253,16 +253,16 @@ public static partial class AssetService
/// <summary> /// <summary>
/// Get the dependencies for an asset from the database. /// Get the dependencies for an asset from the database.
/// </summary> /// </summary>
private static async Task<List<Guid>> GetDependenciesAsync(Guid guid, CancellationToken token = default) private async Task<List<Guid>> GetDependenciesAsync(Guid guid, CancellationToken token = default)
{ {
if (s_dbConnection == null) if (_dbConnection == null)
{ {
return new List<Guid>(); return new List<Guid>();
} }
try try
{ {
await using var cmd = s_dbConnection.CreateCommand(); await using var cmd = _dbConnection.CreateCommand();
cmd.CommandText = "SELECT DependencyGuids FROM Assets WHERE Guid = @guid"; cmd.CommandText = "SELECT DependencyGuids FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString()); 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. /// Find assets by name pattern using database query with wildcards.
/// </summary> /// </summary>
/// <param name="namePattern">Pattern supporting * (any chars) and ? (single char).</param> /// <param name="namePattern">Pattern supporting * (any chars) and ? (single char).</param>
private static async Task<List<Guid>> GetAssetsByNameAsync(string namePattern, CancellationToken token = default) private async Task<List<Guid>> GetAssetsByNameAsync(string namePattern, CancellationToken token = default)
{ {
var results = new List<Guid>(); var results = new List<Guid>();
if (s_dbConnection == null) if (_dbConnection == null)
{ {
return results; return results;
} }
@@ -299,7 +299,7 @@ public static partial class AssetService
// Convert wildcard pattern to SQL LIKE pattern // Convert wildcard pattern to SQL LIKE pattern
var sqlPattern = namePattern.Replace('*', '%').Replace('?', '_'); 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 // 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 // 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
/// <summary> /// <summary>
/// Remove orphaned entries from database (assets that no longer exist on disk). /// Remove orphaned entries from database (assets that no longer exist on disk).
/// </summary> /// </summary>
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; return;
} }
@@ -355,7 +355,7 @@ public static partial class AssetService
{ {
var orphanedGuids = new List<Guid>(); var orphanedGuids = new List<Guid>();
await using var cmd = s_dbConnection.CreateCommand(); await using var cmd = _dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Path FROM Assets"; cmd.CommandText = "SELECT Guid, Path FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token); await using var reader = await cmd.ExecuteReaderAsync(token);

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.Contracts;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -33,31 +34,31 @@ internal readonly record struct AssetCommand(
/// Handles asset registration, lookup, importing, and dependency management. /// Handles asset registration, lookup, importing, and dependency management.
/// Uses SQLite for persistent storage and efficient querying. /// Uses SQLite for persistent storage and efficient querying.
/// </summary> /// </summary>
public static partial class AssetService public partial class AssetService : IAssetService
{ {
private static FileSystemWatcher? s_watcher; private FileSystemWatcher? _watcher;
private static readonly Lock s_dbLock = new(); private readonly Lock _dbLock = new();
private static readonly Dictionary<Guid, string> s_assetPathLookup = new(); private readonly Dictionary<Guid, string> _assetPathLookup = new();
private static readonly Dictionary<string, Guid> s_pathAssetLookup = new(); private readonly Dictionary<string, Guid> _pathAssetLookup = new();
// In-memory dirty asset tracking (for runtime modifications only) // In-memory dirty asset tracking (for runtime modifications only)
// TODO: We do not handle the reimporting of dirty assets yet // TODO: We do not handle the reimporting of dirty assets yet
private static readonly HashSet<Guid> s_dirtyAssets = new(); private readonly HashSet<Guid> _dirtyAssets = new();
// Command buffer pattern - Channel for file system event commands // Command buffer pattern - Channel for file system event commands
private static Channel<AssetCommand>? s_commandChannel; private Channel<AssetCommand>? _commandChannel;
private static Timer? s_commandProcessorTimer; private Timer? _commandProcessorTimer;
private static readonly ConcurrentQueue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh private readonly ConcurrentQueue<AssetCommand> _waitingCommands = new(); // Commands waiting for manual refresh
private static bool s_autoRefreshEnabled = true; private bool _autoRefreshEnabled = true;
// Initialization guard // Initialization guard
private static readonly Lock s_initializationLock = new(); private readonly Lock _initializationLock = new();
private static bool s_initialized = false; private bool _initialized = false;
private static readonly TimeSpan s_debounceDelay = TimeSpan.FromMilliseconds(100); private readonly TimeSpan _debounceDelay = TimeSpan.FromMilliseconds(100);
private static readonly ManualResetEventSlim s_resetEventSlim = new(false); private readonly ManualResetEventSlim _resetEventSlim = new(false);
private static readonly JsonSerializerOptions s_defaultJsonOptions = new() private readonly JsonSerializerOptions _defaultJsonOptions = new()
{ {
WriteIndented = true, WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@@ -67,43 +68,43 @@ public static partial class AssetService
} }
}; };
public static DirectoryInfo? AssetsDirectory public DirectoryInfo? AssetsDirectory
{ {
get; get;
private set; private set;
} }
/// <summary> /// <summary>
/// Initialize the asset database. /// Init the asset database.
/// Must be called after project is loaded. /// Must be called after project is loaded.
/// </summary> /// </summary>
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; return;
} }
s_initialized = true; _initialized = true;
} }
AssetsDirectory = new DirectoryInfo(Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME)); AssetsDirectory = new DirectoryInfo(Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME));
s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions _commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions
{ {
SingleReader = false, SingleReader = false,
SingleWriter = false SingleWriter = false
}); });
// Initialize command processor timer (starts disabled, triggered by events) // Init command processor timer (starts disabled, triggered by events)
s_commandProcessorTimer = new Timer(ProcessPendingCommands, null, Timeout.Infinite, Timeout.Infinite); _commandProcessorTimer = new Timer(ProcessPendingCommands, null, Timeout.Infinite, Timeout.Infinite);
await InitializeDatabaseAsync(token); await InitializeDatabaseAsync(token);
await LoadAssetCacheFromDatabaseAsync(token); await LoadAssetCacheFromDatabaseAsync(token);
s_watcher = new FileSystemWatcher _watcher = new FileSystemWatcher
{ {
Path = AssetsDirectory.FullName, Path = AssetsDirectory.FullName,
IncludeSubdirectories = true, IncludeSubdirectories = true,
@@ -122,7 +123,7 @@ public static partial class AssetService
/// Validate the asset database and fix any inconsistencies. /// Validate the asset database and fix any inconsistencies.
/// Checks for missing/corrupted assets and regenerates metadata as needed. /// Checks for missing/corrupted assets and regenerates metadata as needed.
/// </summary> /// </summary>
private static async Task<Result> ValidateAndFixDatabaseAsync(CancellationToken token = default) private async Task<Result> ValidateAndFixDatabaseAsync(CancellationToken token = default)
{ {
if (AssetsDirectory == null) if (AssetsDirectory == null)
{ {
@@ -175,19 +176,19 @@ public static partial class AssetService
/// Refresh the asset database manually. /// Refresh the asset database manually.
/// Scans the project directory for changes and processes any queued file system events. /// Scans the project directory for changes and processes any queued file system events.
/// </summary> /// </summary>
public static async Task<Result> RefreshAsync(CancellationToken token = default) public async Task<Result> RefreshAsync(CancellationToken token = default)
{ {
// Flush waiting commands to channel // 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(); _resetEventSlim.Reset();
s_commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty)); _commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty));
s_commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan); _commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
await Task.Run(s_resetEventSlim.Wait, token); await Task.Run(_resetEventSlim.Wait, token);
return Result.Success(); return Result.Success();
} }
@@ -195,55 +196,55 @@ public static partial class AssetService
/// Mark an asset as dirty (modified in memory but not yet saved). /// Mark an asset as dirty (modified in memory but not yet saved).
/// This state is NOT persisted and will be lost on application restart. /// This state is NOT persisted and will be lost on application restart.
/// </summary> /// </summary>
public static void MarkDirty(Guid assetGuid) public void MarkDirty(Guid assetGuid)
{ {
lock (s_dbLock) lock (_dbLock)
{ {
s_dirtyAssets.Add(assetGuid); _dirtyAssets.Add(assetGuid);
} }
} }
/// <summary> /// <summary>
/// Check if an asset is marked as dirty. /// Check if an asset is marked as dirty.
/// </summary> /// </summary>
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);
} }
} }
/// <summary> /// <summary>
/// Get all dirty assets. /// Get all dirty assets.
/// </summary> /// </summary>
public static Guid[] GetDirtyAssets() public Guid[] GetDirtyAssets()
{ {
lock (s_dbLock) lock (_dbLock)
{ {
return s_dirtyAssets.ToArray(); return _dirtyAssets.ToArray();
} }
} }
/// <summary> /// <summary>
/// Clear dirty flag for an asset (typically after saving). /// Clear dirty flag for an asset (typically after saving).
/// </summary> /// </summary>
public static void ClearDirty(Guid assetGuid) public void ClearDirty(Guid assetGuid)
{ {
lock (s_dbLock) lock (_dbLock)
{ {
s_dirtyAssets.Remove(assetGuid); _dirtyAssets.Remove(assetGuid);
} }
} }
/// <summary> /// <summary>
/// Clear all dirty flags. /// Clear all dirty flags.
/// </summary> /// </summary>
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. /// Enable or disable automatic asset database refresh.
/// When disabled, file system events are queued and processed only when RefreshAsync() is called. /// When disabled, file system events are queued and processed only when RefreshAsync() is called.
/// </summary> /// </summary>
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 // 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 // Give a tiny bit of time for any in-flight file watcher events to post to channel
Thread.Sleep(50); Thread.Sleep(50);
@@ -268,27 +269,27 @@ public static partial class AssetService
ProcessPendingCommands(null); 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; return;
} }
if (s_autoRefreshEnabled) if (_autoRefreshEnabled)
{ {
await s_commandChannel.Writer.WriteAsync(command, token); await _commandChannel.Writer.WriteAsync(command, token);
s_commandProcessorTimer?.Change(s_debounceDelay, Timeout.InfiniteTimeSpan); _commandProcessorTimer?.Change(_debounceDelay, Timeout.InfiniteTimeSpan);
} }
else 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; return;
} }
@@ -298,7 +299,7 @@ public static partial class AssetService
// // Collect all pending commands // // Collect all pending commands
// var commands = new List<AssetCommand>(); // var commands = new List<AssetCommand>();
// //
// while (s_commandChannel.Reader.TryRead(out var cmd)) // while (_commandChannel.Reader.TryRead(out var cmd))
// { // {
// commands.Add(cmd); // commands.Add(cmd);
// } // }
@@ -333,12 +334,12 @@ public static partial class AssetService
// Execute commands // Execute commands
// NOTE: We many don't need to collect all commands first, just process as we read. // 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. // 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); // await ExecuteCommandAsync(cmd);
//} //}
while (s_commandChannel.Reader.TryRead(out var cmd)) while (_commandChannel.Reader.TryRead(out var cmd))
{ {
await ExecuteCommandAsync(cmd); await ExecuteCommandAsync(cmd);
} }
@@ -351,11 +352,11 @@ public static partial class AssetService
} }
finally finally
{ {
s_resetEventSlim.Set(); _resetEventSlim.Set();
} }
} }
private static async ValueTask ExecuteCommandAsync(AssetCommand command) private async ValueTask ExecuteCommandAsync(AssetCommand command)
{ {
switch (command.Type) 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)) if (!File.Exists(path))
{ {
@@ -394,7 +395,7 @@ public static partial class AssetService
await GenerateMetaFileAsync(path, CancellationToken.None); await GenerateMetaFileAsync(path, CancellationToken.None);
} }
private static async ValueTask HandleFileModifiedAsync(string path) private async ValueTask HandleFileModifiedAsync(string path)
{ {
if (!File.Exists(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); var metaFileResult = GetMetaFilePath(path);
if (metaFileResult.IsSuccess && File.Exists(metaFileResult.Value)) 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 oldMetaPath = oldPath + Utilities.FileExtensions.META_FILE_EXTENSION;
var newMetaPath = newPath + 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; return;
} }
s_watcher?.Dispose(); _watcher?.Dispose();
s_watcher = null; _watcher = null;
s_commandProcessorTimer?.Dispose(); _commandProcessorTimer?.Dispose();
s_commandProcessorTimer = null; _commandProcessorTimer = null;
s_dbConnection?.Close(); _dbConnection?.Close();
s_dbConnection?.Dispose(); _dbConnection?.Dispose();
s_dbConnection = null; _dbConnection = null;
s_assetPathLookup.Clear(); _assetPathLookup.Clear();
s_pathAssetLookup.Clear(); _pathAssetLookup.Clear();
s_dirtyAssets.Clear(); _dirtyAssets.Clear();
s_waitingCommands.Clear(); _waitingCommands.Clear();
s_importerInstances.Clear(); _importerInstances.Clear();
s_importerTypeLookup.Clear(); _importerTypeLookup.Clear();
s_initialized = false; _initialized = false;
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
@@ -11,7 +12,7 @@ public abstract class AssetImporter
/// <param name="meta">Metadata for the asset.</param> /// <param name="meta">Metadata for the asset.</param>
/// <param name="token">Cancellation token.</param> /// <param name="token">Cancellation token.</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public abstract ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default); public abstract ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, IAssetService assetService, CancellationToken token = default);
/// <summary> /// <summary>
/// Export in-memory asset data to disk. /// 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. /// Dependencies are extracted from asset content during import and stored in the database.
/// </summary> /// </summary>
/// <param name="dependencies">List of dependency GUIDs extracted from the asset.</param> /// <param name="dependencies">List of dependency GUIDs extracted from the asset.</param>
/// <param name="assetService">The asset service instance.</param>
/// <returns>Result indicating if all dependencies are valid.</returns> /// <returns>Result indicating if all dependencies are valid.</returns>
protected virtual ValueTask<Result> ValidateDependenciesAsync(List<Guid> dependencies, CancellationToken token = default) protected virtual ValueTask<Result> ValidateDependenciesAsync(List<Guid> dependencies, IAssetService assetService, CancellationToken token = default)
{ {
foreach (var dependencyGuid in dependencies) foreach (var dependencyGuid in dependencies)
{ {
var path = AssetService.GuidToPath(dependencyGuid); var path = assetService.GuidToPath(dependencyGuid);
if (path.IsFailure) if (path.IsFailure)
{ {
return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}")); return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}"));

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.AssetHandle.Importers; namespace Ghost.Editor.Core.AssetHandle.Importers;
@@ -27,7 +28,7 @@ internal class TextImporterSettings : ImporterSettings
[AssetImporter(".txt", ".md")] [AssetImporter(".txt", ".md")]
internal class TextImporter : AssetImporter<TextImporterSettings> internal class TextImporter : AssetImporter<TextImporterSettings>
{ {
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default) public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, IAssetService assetService, CancellationToken token = default)
{ {
var settings = GetSettings(meta); var settings = GetSettings(meta);
@@ -36,7 +37,7 @@ internal class TextImporter : AssetImporter<TextImporterSettings>
var dependencies = new List<Guid>(); var dependencies = new List<Guid>();
// Validate dependencies // Validate dependencies
var depResult = await ValidateDependenciesAsync(dependencies); var depResult = await ValidateDependenciesAsync(dependencies, assetService, token);
if (depResult.IsFailure) if (depResult.IsFailure)
{ {
return depResult; return depResult;

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.Contracts;
using System.Text.Json; using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle.Importers; namespace Ghost.Editor.Core.AssetHandle.Importers;
@@ -73,20 +74,19 @@ internal class TextureImporterSettings : ImporterSettings
[AssetImporter(".png", ".jpg", ".jpeg", ".dds", ".tga", ".bmp")] [AssetImporter(".png", ".jpg", ".jpeg", ".dds", ".tga", ".bmp")]
internal class TextureImporter : AssetImporter<TextureImporterSettings> internal class TextureImporter : AssetImporter<TextureImporterSettings>
{ {
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default) public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, IAssetService assetService, CancellationToken token = default)
{ {
var settings = GetSettings(meta); var settings = GetSettings(meta);
// Textures typically don't reference other assets as dependencies // 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<Guid>();
var dependencies = new List<Guid>();
// Validate dependencies //// Validate dependencies
var depResult = await ValidateDependenciesAsync(dependencies, token); //var depResult = await ValidateDependenciesAsync(dependencies, assetService, token);
if (depResult.IsFailure) //if (depResult.IsFailure)
{ //{
return depResult; // return depResult;
} //}
try try
{ {
@@ -134,7 +134,7 @@ internal class TextureImporter : AssetImporter<TextureImporterSettings>
}; };
// Save the imported asset data // Save the imported asset data
var saveResult = AssetService.SaveImportedAsset(meta.Guid, textureAsset); var saveResult = assetService.SaveImportedAsset(meta.Guid, textureAsset);
if (saveResult.IsFailure) if (saveResult.IsFailure)
{ {
return Result.Failure($"Failed to save texture asset: {saveResult.Message}"); return Result.Failure($"Failed to save texture asset: {saveResult.Message}");

View File

@@ -1,5 +1,62 @@
using Ghost.Core;
using Ghost.Editor.Core.AssetHandle;
namespace Ghost.Editor.Core.Contracts; namespace Ghost.Editor.Core.Contracts;
public interface IAssetService public interface IAssetService
{ {
DirectoryInfo? AssetsDirectory { get; }
// Lifecycle
Task<Result> 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<Guid> PathToGuid(string assetPath);
Result<string> GuidToPath(Guid guid);
// Asset loading
Result<T> LoadAsset<T>(Guid guid) where T : Asset;
Result<T> LoadAssetAtPath<T>(string assetPath) where T : Asset;
void UnloadAsset(Guid guid);
void UnloadAllAssets();
bool IsAssetLoaded(Guid guid);
(int currentSize, int maxSize) GetCacheStats();
Result SaveImportedAsset<T>(Guid guid, T assetData) where T : Asset;
// Asset tags
ValueTask<Result<List<string>>> GetAssetTagsAsync(Guid guid, CancellationToken token = default);
ValueTask<Result> SetAssetTagsAsync(Guid guid, List<string> tags, CancellationToken token = default);
// Asset search
Task<List<Guid>> FindAssetsByNameAsync(string namePattern, CancellationToken token = default);
Task<List<Guid>> FindAssetsByTagAsync(string tag, CancellationToken token = default);
IReadOnlyDictionary<Guid, string> GetAllAssets();
// Asset file operations
ValueTask<Result> CreateAssetAsync(string assetPath, ReadOnlyMemory<byte> content, CancellationToken token = default);
ValueTask<Result> CreateAssetAsync(string assetPath, CancellationToken token = default);
ValueTask<Result> DeleteAssetAsync(Guid guid, CancellationToken token = default);
ValueTask<Result> DeleteAssetAsync(string assetPath, CancellationToken token = default);
ValueTask<Result> MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default);
ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default);
ValueTask<Result<Guid>> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default);
ValueTask<Result<Guid>> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default);
Result MarkDirtyAsync(Guid guid, CancellationToken token = default);
Task<Result> ImportDirtyAssetsAsync(CancellationToken token = default);
// Importer management
Type? GetImporterType(string extension);
Dictionary<string, Type> GetAllImporters();
ValueTask<Result<Guid>> ExportAssetAsync<T>(string assetPath, T assetData, CancellationToken token = default) where T : class;
// Asset opening
void OpenAsset(string path);
} }

View File

@@ -1,3 +1,4 @@
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -56,5 +57,9 @@ public static class EditorApplication
internal static void Shutdown() internal static void Shutdown()
{ {
if (s_serviceProvider?.GetService(typeof(IAssetService)) is AssetHandle.AssetService assetService)
{
assetService.Shutdown();
}
} }
} }

View File

@@ -1,4 +1,5 @@
using Ghost.Editor.Core; using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Utilities; using Ghost.Editor.Core.Utilities;
using Ghost.Editor.Models; using Ghost.Editor.Models;
using Ghost.Engine; using Ghost.Engine;
@@ -61,7 +62,9 @@ internal static class ActivationHandler
((EngineCore)App.GetService<IEngineContext>()).Init(); ((EngineCore)App.GetService<IEngineContext>()).Init();
}); });
// TODO: Initialize other subsystems here. await ((Core.AssetHandle.AssetService)App.GetService<IAssetService>()).Init();
// TODO: Init other subsystems here.
// await Task.Delay(10000); // Wait 10 seconds to simulate work. // await Task.Delay(10000); // Wait 10 seconds to simulate work.
} }
} }

View File

@@ -1,5 +1,6 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core; using Ghost.Editor.Core;
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services; using Ghost.Editor.Core.Services;
using Ghost.Editor.View.Pages.EngineEditor; using Ghost.Editor.View.Pages.EngineEditor;

View File

@@ -11,6 +11,7 @@ namespace Ghost.Editor.ViewModels.Controls;
internal partial class ProjectBrowserViewModel : ObservableObject internal partial class ProjectBrowserViewModel : ObservableObject
{ {
private readonly IInspectorService _inspectorService; private readonly IInspectorService _inspectorService;
private readonly IAssetService _assetService;
private readonly Dictionary<string, ExplorerItem> _pathToDirectoryItemMap = new(); private readonly Dictionary<string, ExplorerItem> _pathToDirectoryItemMap = new();
private ExplorerItem? _selectedItem; private ExplorerItem? _selectedItem;
@@ -40,9 +41,10 @@ internal partial class ProjectBrowserViewModel : ObservableObject
get; set; get; set;
} = string.Empty; } = string.Empty;
public ProjectBrowserViewModel(IInspectorService inspectorService) public ProjectBrowserViewModel(IInspectorService inspectorService, IAssetService assetService)
{ {
_inspectorService = inspectorService; _inspectorService = inspectorService;
_assetService = assetService;
var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
LoadSubFolderRecursive(assetsRootItem); LoadSubFolderRecursive(assetsRootItem);
@@ -108,7 +110,7 @@ internal partial class ProjectBrowserViewModel : ObservableObject
} }
else else
{ {
AssetService.OpenAsset(SelectedItem.FullName); _assetService.OpenAsset(SelectedItem.FullName);
return (null, 1); return (null, 1);
} }
} }

View File

@@ -1,6 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core; using Ghost.Editor.Core;
using Ghost.Editor.Core.AssetHandle; using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Models; using Ghost.Editor.Models;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@@ -8,6 +9,8 @@ namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
internal partial class ProjectViewModel : ObservableObject internal partial class ProjectViewModel : ObservableObject
{ {
private readonly IAssetService _assetService;
public ObservableCollection<ExplorerItem> SubDirectories public ObservableCollection<ExplorerItem> SubDirectories
{ {
get; get;
@@ -34,8 +37,10 @@ internal partial class ProjectViewModel : ObservableObject
set; set;
} }
public ProjectViewModel() public ProjectViewModel(IAssetService assetService)
{ {
_assetService = assetService;
var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
LoadSubFolderRecursive(ref assetsRootItem); LoadSubFolderRecursive(ref assetsRootItem);
@@ -124,7 +129,7 @@ internal partial class ProjectViewModel : ObservableObject
} }
else else
{ {
AssetService.OpenAsset(SelectedAsset.FullName); _assetService.OpenAsset(SelectedAsset.FullName);
} }
} }

View File

@@ -49,7 +49,7 @@ public class AssetDatabaseIntegrationTest
var projectMetadataInfo = new Data.Models.ProjectMetadataInfo(projectPath, metadata); var projectMetadataInfo = new Data.Models.ProjectMetadataInfo(projectPath, metadata);
ProjectService.CurrentProject = projectMetadataInfo; ProjectService.CurrentProject = projectMetadataInfo;
// Initialize AssetService // Init AssetService
await AssetService.Initialize(TestContext.CancellationToken); await AssetService.Initialize(TestContext.CancellationToken);
// Give the file system watcher time to start // Give the file system watcher time to start