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;
public static partial class AssetService
public partial class AssetService
{
/// <summary>
/// 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="content">Content to write to the asset file.</param>
/// <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)
{
@@ -57,7 +57,7 @@ public static partial class AssetService
/// </summary>
/// <param name="assetPath">Path to create the asset at.</param>
/// <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);
}
@@ -67,7 +67,7 @@ public static partial class AssetService
/// </summary>
/// <param name="guid">GUID of the asset to delete.</param>
/// <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);
if (pathResult.IsFailure)
@@ -114,7 +114,7 @@ public static partial class AssetService
/// </summary>
/// <param name="assetPath">Path to the asset to delete.</param>
/// <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);
if (guidResult.IsFailure)
@@ -131,7 +131,7 @@ public static partial class AssetService
/// <param name="guid">GUID of the asset to move.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <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);
if (oldPathResult.IsFailure)
@@ -211,7 +211,7 @@ public static partial class AssetService
/// <param name="oldPath">CurrentApplication path of the asset.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <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);
if (guidResult.IsFailure)
@@ -228,7 +228,7 @@ public static partial class AssetService
/// <param name="guid">GUID of the asset to copy.</param>
/// <param name="newPath">New path for the copied asset (relative or absolute).</param>
/// <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);
if (oldPathResult.IsFailure)
@@ -299,7 +299,7 @@ public static partial class AssetService
/// <param name="sourcePath">Path of the asset to copy.</param>
/// <param name="destPath">New path for the copied asset (relative or absolute).</param>
/// <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);
if (guidResult.IsFailure)
@@ -315,7 +315,7 @@ public static partial class AssetService
/// </summary>
/// <param name="guid">GUID of the asset to mark dirty.</param>
/// <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);
return Result.Success();
@@ -325,7 +325,7 @@ public static partial class AssetService
/// Import all dirty assets.
/// </summary>
/// <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();

View File

@@ -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<Type, AssetImporter> s_importerInstances = new();
private readonly Dictionary<Type, AssetImporter> _importerInstances = new();
/// <summary>
/// Import an asset at the specified path.
/// </summary>
/// <param name="assetPath">Full path to the asset file.</param>
/// <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);
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);
}
/// <summary>
@@ -49,9 +49,9 @@ public static partial class AssetService
/// </summary>
/// <param name="extension">File extension (e.g., ".png").</param>
/// <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;
}
@@ -59,9 +59,9 @@ public static partial class AssetService
/// Get all registered importer types and their supported extensions.
/// </summary>
/// <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>
@@ -72,17 +72,18 @@ public static partial class AssetService
/// <param name="assetPath">Full path where the asset should be saved.</param>
/// <param name="assetData">In-memory asset data to export.</param>
/// <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);
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}");
}
// 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<Guid>.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<Guid>.Failure($"ExportAsync method not found on importer {importerType.Name}. This importer does not support exporting.");
_importerInstances[importerType] = importerInstance;
}
// Generate metadata for the new asset

View File

@@ -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<Guid, Asset> s_assetCache = new();
private readonly ConcurrentDictionary<Guid, Asset> _assetCache = new();
// 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
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<string> GetImportedAssetsDirectory()
private Result<string> GetImportedAssetsDirectory()
{
if (AssetsDirectory == null)
{
@@ -34,7 +34,7 @@ public static partial class AssetService
return cacheDir;
}
private static Result<string> GetImportedAssetPath(Guid guid)
private Result<string> GetImportedAssetPath(Guid guid)
{
var importedDirResult = GetImportedAssetsDirectory();
if (importedDirResult.IsFailure)
@@ -47,13 +47,13 @@ public static partial class AssetService
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
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<T> LoadAssetAtPath<T>(string assetPath) where T : Asset
public Result<T> LoadAssetAtPath<T>(string assetPath) where T : Asset
{
var guidResult = PathToGuid(assetPath);
if (guidResult.IsFailure)
@@ -109,24 +109,24 @@ public static partial class AssetService
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
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.
/// </summary>
/// <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 _);
s_assetAccessTime.TryRemove(guid, out _);
_assetCache.TryRemove(guid, out _);
_assetAccessTime.TryRemove(guid, out _);
}
/// <summary>
/// Unload all assets from cache.
/// </summary>
public static void UnloadAllAssets()
public void UnloadAllAssets()
{
s_assetCache.Clear();
s_assetAccessTime.Clear();
_assetCache.Clear();
_assetAccessTime.Clear();
}
/// <summary>
@@ -163,18 +163,18 @@ public static partial class AssetService
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <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>
/// Get cache statistics.
/// </summary>
/// <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>
@@ -185,7 +185,7 @@ public static partial class AssetService
/// <param name="guid">GUID of the asset.</param>
/// <param name="assetData">Processed asset data to save.</param>
/// <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
{
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

View File

@@ -3,12 +3,12 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetService
public partial class AssetService
{
/// <summary>
/// Get the relative path from the assets directory.
/// </summary>
private static Result<string> GetRelativePath(string fullPath)
private Result<string> GetRelativePath(string fullPath)
{
if (AssetsDirectory == null)
{
@@ -26,7 +26,7 @@ public static partial class AssetService
/// <summary>
/// Get the full path from a relative path.
/// </summary>
private static Result<string> GetFullPath(string relativePath)
private Result<string> GetFullPath(string relativePath)
{
if (AssetsDirectory == null)
{
@@ -41,7 +41,7 @@ public static partial class AssetService
/// </summary>
/// <param name="assetPath">Full or relative path to the asset.</param>
/// <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;
@@ -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
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <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;
}
@@ -94,7 +94,7 @@ public static partial class AssetService
/// <typeparam name="T">Type of asset to load.</typeparam>
/// <param name="guid">GUID of the asset.</param>
/// <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
return LoadAssetInternal<T>(guid);
@@ -105,7 +105,7 @@ public static partial class AssetService
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <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);
if (pathResult.IsFailure)
@@ -134,7 +134,7 @@ public static partial class AssetService
/// <param name="guid">GUID of the asset.</param>
/// <param name="tags">New tags for the asset.</param>
/// <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);
if (pathResult.IsFailure)
@@ -174,7 +174,7 @@ public static partial class AssetService
/// </summary>
/// <param name="namePattern">Search pattern (e.g., "*.txt", "player?", "test*").</param>
/// <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);
}
@@ -184,7 +184,7 @@ public static partial class AssetService
/// </summary>
/// <param name="tag">Tag to search for.</param>
/// <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);
}
@@ -193,11 +193,11 @@ public static partial class AssetService
/// Get all assets in the database.
/// </summary>
/// <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;
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.");
}
@@ -24,17 +24,17 @@ public static partial class AssetService
var attribute = type.GetCustomAttribute<AssetImporterAttribute>()!;
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<string> GetMetaFilePath(string assetPath)
private Result<string> 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
/// <summary>
/// Calculate SHA256 hash of a file for change detection.
/// </summary>
private static async Task<string> CalculateFileHashAsync(string filePath, CancellationToken token = default)
private async Task<string> CalculateFileHashAsync(string filePath, CancellationToken token = default)
{
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
{
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
/// <summary>
/// Read metadata from a .gmeta file.
/// </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);
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<AssetMeta>(fileStream, s_defaultJsonOptions, token);
var meta = await JsonSerializer.DeserializeAsync<AssetMeta>(fileStream, _defaultJsonOptions, token);
if (meta == null)
{
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;
@@ -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
/// <summary>
/// Mark all assets that depend on the specified asset as dirty.
/// </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.

View File

@@ -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<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()
.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);
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);
}

View File

@@ -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;
/// <summary>
/// Initialize the SQLite database for asset caching.
/// Init the SQLite database for asset caching.
/// </summary>
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
/// <param name="meta">Asset metadata from .gmeta file.</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>
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");
}
@@ -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
/// <summary>
/// Remove an asset from the database.
/// </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");
}
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
/// <summary>
/// Load all assets from the database into memory cache.
/// </summary>
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
/// <summary>
/// Get assets by tag.
/// </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>();
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
/// <summary>
/// Get the file hash for an asset from the database.
/// </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;
}
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
/// <summary>
/// Get the dependencies for an asset from the database.
/// </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>();
}
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.
/// </summary>
/// <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>();
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
/// <summary>
/// Remove orphaned entries from database (assets that no longer exist on disk).
/// </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;
}
@@ -355,7 +355,7 @@ public static partial class AssetService
{
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";
await using var reader = await cmd.ExecuteReaderAsync(token);

View File

@@ -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.
/// </summary>
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<Guid, string> s_assetPathLookup = new();
private static readonly Dictionary<string, Guid> s_pathAssetLookup = new();
private FileSystemWatcher? _watcher;
private readonly Lock _dbLock = new();
private readonly Dictionary<Guid, string> _assetPathLookup = new();
private readonly Dictionary<string, Guid> _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<Guid> s_dirtyAssets = new();
private readonly HashSet<Guid> _dirtyAssets = new();
// Command buffer pattern - Channel for file system event commands
private static Channel<AssetCommand>? s_commandChannel;
private static Timer? s_commandProcessorTimer;
private static readonly ConcurrentQueue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh
private static bool s_autoRefreshEnabled = true;
private Channel<AssetCommand>? _commandChannel;
private Timer? _commandProcessorTimer;
private readonly ConcurrentQueue<AssetCommand> _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;
}
/// <summary>
/// Initialize the asset database.
/// Init the asset database.
/// Must be called after project is loaded.
/// </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;
}
s_initialized = true;
_initialized = true;
}
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,
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.
/// </summary>
private static async Task<Result> ValidateAndFixDatabaseAsync(CancellationToken token = default)
private async Task<Result> 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.
/// </summary>
public static async Task<Result> RefreshAsync(CancellationToken token = default)
public async Task<Result> 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.
/// </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>
/// Check if an asset is marked as dirty.
/// </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>
/// Get all dirty assets.
/// </summary>
public static Guid[] GetDirtyAssets()
public Guid[] GetDirtyAssets()
{
lock (s_dbLock)
lock (_dbLock)
{
return s_dirtyAssets.ToArray();
return _dirtyAssets.ToArray();
}
}
/// <summary>
/// Clear dirty flag for an asset (typically after saving).
/// </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>
/// Clear all dirty flags.
/// </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.
/// When disabled, file system events are queued and processed only when RefreshAsync() is called.
/// </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
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<AssetCommand>();
//
// 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;
}
}
}

View File

@@ -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
/// <param name="meta">Metadata for the asset.</param>
/// <param name="token">Cancellation token.</param>
/// <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>
/// 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.
/// </summary>
/// <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>
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)
{
var path = AssetService.GuidToPath(dependencyGuid);
var path = assetService.GuidToPath(dependencyGuid);
if (path.IsFailure)
{
return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}"));

View File

@@ -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<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);
@@ -36,7 +37,7 @@ internal class TextImporter : AssetImporter<TextImporterSettings>
var dependencies = new List<Guid>();
// Validate dependencies
var depResult = await ValidateDependenciesAsync(dependencies);
var depResult = await ValidateDependenciesAsync(dependencies, assetService, token);
if (depResult.IsFailure)
{
return depResult;

View File

@@ -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<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);
// 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
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<TextureImporterSettings>
};
// 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}");

View File

@@ -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<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.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();
}
}
}

View File

@@ -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<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.
}
}

View File

@@ -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;

View File

@@ -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<string, ExplorerItem> _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);
}
}

View File

@@ -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<ExplorerItem> 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);
}
}

View File

@@ -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