using NativeEngine; using System; using System.Collections.Concurrent; using System.ComponentModel; namespace Editor; /// /// The asset system, provides access to all the assets. /// public static partial class AssetSystem { static Logger log = new Logger( "AssetSystem" ); [SkipHotload] static Dictionary allAssets = new(); [SkipHotload] static ConcurrentDictionary assetsByPath = new( StringComparer.OrdinalIgnoreCase ); /// /// All the assets that are being tracked by the asset system. Does not include deleted assets. /// public static IEnumerable All => allAssets.Values.Where( x => !x.IsDeleted ); static bool HasChanges; static bool IsInitialized = false; static HashSet UpdateQueue = new(); /// /// Called after the asset types have been loaded from /// internal static void PreInitialize() { log.Trace( "PreInitialize" ); foreach ( var type in AssetType.AssetTypeCache ) { type.Value.Init(); } } internal static void InitializeFromProject( Project project ) { string path = System.IO.Path.Combine( project.GetRootPath(), ".sbox", "cloud.db" ); CloudDirectory = new CloudAssetDirectory( path ); HasChanges = true; IsInitialized = true; Tick(); } internal static void Shutdown() { CloudDirectory?.Dispose(); CloudDirectory = null; } internal static void AssetAdded( IAsset native ) { // log.Trace( $"Asset Added: {native.GetAbsolutePath_Transient()}" ); var a = new NativeAsset( native ); allAssets[a.AssetId] = a; UpdateQueue.Add( a ); HasChanges = true; } static void UpdateAsset( Asset asset, bool compileImmediately = true ) { asset.UpdateInternals( compileImmediately ); if ( string.IsNullOrWhiteSpace( asset.Path ) ) return; assetsByPath[asset.Path] = asset; assetsByPath[asset.AbsolutePath] = asset; assetsByPath[asset.RelativePath] = asset; assetsByPath[asset.AbsoluteCompiledPath] = asset; } /// /// This is only called on startup. The cache is loaded, so a bunch of assets are known, /// then it does a bit of research and sees that the asset is removed, and it can remove it now. /// This is the only point where an Asset is actually destroyed, during the mainloop the asset /// is just marked as deleted but never destroyed. /// internal static void AssetRemoved( uint index ) { var a = allAssets[index]; Assert.NotNull( a ); log.Trace( $"Removed: {a}" ); if ( a.Path is not null ) assetsByPath.TryRemove( a.Path, out _ ); if ( a.AbsolutePath is not null ) assetsByPath.TryRemove( a.AbsolutePath, out _ ); if ( a.RelativePath is not null ) assetsByPath.TryRemove( a.RelativePath, out _ ); if ( a.AbsoluteCompiledPath is not null ) assetsByPath.TryRemove( a.AbsoluteCompiledPath, out _ ); // Can it ever come back?? UpdateQueue.Remove( a ); allAssets.Remove( index ); a.OnRemoved(); } internal static void RecordAssetOpen( uint index ) { var a = allAssets[index]; Assert.NotNull( a ); a.LastOpened = DateTime.Now; } internal static void AssetChanged( uint index ) { var asset = allAssets[index]; UpdateAsset( asset ); if ( UpdateQueue.Add( asset ) ) { NativeAssetProcessor.OnAssetChanged( asset ); HasChanges = true; } } internal static void UpdateAssetAutoTags( uint index ) { allAssets[index].UpdateAutoTags(); } internal static void AssetScanComplete() { foreach ( var type in AssetType.AssetTypeCache ) { type.Value.Init(); } HasChanges = true; Tick(); DeleteOrphans(); } [EditorEvent.Frame] internal static void Tick() { if ( !HasChanges || !IsInitialized ) return; HasChanges = false; // Everything below is safe to do in parallel as long as our dependency info is up to date // Which needs to be done on the main thread foreach ( var asset in UpdateQueue ) { if ( asset is NativeAsset nativeAsset ) { nativeAsset.native.RequireDependencyInfo_Virtual(); } } Parallel.ForEach( UpdateQueue, ( Asset asset ) => UpdateAsset( asset, false ) ); // Thumbnails have their own queue, so do that all in main foreach ( var asset in UpdateQueue ) { EditorEvent.RunInterface( x => x.OnAssetChanged( asset ) ); if ( asset.HasCachedThumbnail ) { asset.RebuildThumbnail( true ); } if ( !asset.IsCompiled && asset.AssetType.IsGameResource ) { asset.Compile( false ); } } UpdateQueue.Clear(); EditorEvent.RunInterface( x => x.OnAssetSystemChanges() ); } /// /// Find an asset by path. /// /// The file path to an asset. Can be absolute or relative. public static Asset FindByPath( string path ) { if ( string.IsNullOrWhiteSpace( path ) ) return null; path = path.Replace( '\\', '/' ); path = path.TrimStart( '/' ); if ( assetsByPath.TryGetValue( path, out var asset ) && !asset.IsDeleted ) { return asset; } return null; } internal static Asset Get( IAsset asset ) { return allAssets[asset.GetAssetIndexInt()]; } internal static Asset Get( uint index ) { return allAssets[index]; } internal static void RegisterAssetType( int id, IAssetType assetType ) { var color = Color.Parse( assetType.GetColor() ); var at = new AssetType { FriendlyName = assetType.GetFriendlyName(), FileExtension = assetType.GetPrimaryExtension(), HiddenByDefault = assetType.HideTypeByDefault(), PrefersIconThumbnail = assetType.PrefersIconForThumbnail(), IconPathSmall = assetType.GetIconSm(), IconPathLarge = assetType.GetIconLg(), IsSimpleAsset = assetType.IsSimpleAsset(), HasDependencies = assetType.HasDependencies(), Category = assetType.GetCategory(), Color = color ?? Color.Gray }; at.AllFileExtensions.Add( at.FileExtension ); var additionalExtensions = CUtlVectorString.Create( 8, 8 ); assetType.GetAdditionalExtensions( additionalExtensions ); for ( var i = 0; i < additionalExtensions.Count(); ++i ) { at.AllFileExtensions.Add( additionalExtensions.Element( i ) ); } additionalExtensions.DeleteThis(); AssetType.AssetTypeCache[id] = at; at.Init(); } /// /// If you just created an asset, you probably want to immediately register it /// public static Asset RegisterFile( string absoluteFilePath ) { ThreadSafe.AssertIsMainThread(); var asset = IAssetSystem.RegisterAssetFile( absoluteFilePath ); if ( !asset.IsValid ) return null; var ret = Get( asset.GetAssetIndexInt() ); // Make sure the ResoureLibrary has proper loaded version, so that properties that target // GameResources do not break child assets if child asset was not loaded with Asset.CreateUI beforehand. ret.TryLoadGameResource( typeof( GameResource ), out _, true ); return ret; } /// /// Delete orphaned trivial children. These are things that are generated for /// usage by an asset, but aren't referenced by anything, so are useless. /// public static void DeleteOrphans() { var orphans = All .Where( x => x.IsTrivialChild ) .Where( x => !x.IsDeleted ) .Where( x => x.AssetType == AssetType.Texture ) // Note - gib models can be trivial children too, but I don't want to push my luck .Where( x => x.GetDependants( false ).Count == 0 ) .ToArray(); foreach ( var o in orphans ) { log.Trace( $"Deleting Orphan \"{o}\"" ); o.Delete(); UpdateQueue.Add( o ); HasChanges = true; } } internal static void InitializeCompilerForFilename( IResourceCompilerContext context, string filename ) { var ext = System.IO.Path.GetExtension( filename )[1..].ToLowerInvariant(); var assetType = AssetType.FromExtension( ext ); if ( assetType is null ) return; if ( assetType.IsGameResource == false ) return; context.SetCompiler( $"ManagedResourceCompiler" ); context.SetExtension( ext ); } /// /// Create an empty . /// /// Asset type extension for our new instance. /// Where to save the new instance. For example from . /// The new asset, or null if creation failed. public static Asset CreateResource( string type, string absoluteFilename ) { var gameResourceType = EditorTypeLibrary.GetAttributes().Where( x => x.Extension == type ).FirstOrDefault(); if ( gameResourceType is null ) { Log.Warning( $"Couldn't find matching resource type for extension {type}" ); return null; } var extension = gameResourceType.Extension; absoluteFilename = System.IO.Path.ChangeExtension( absoluteFilename, extension ); // try to find it first. If we find it, return it. var found = AssetSystem.FindByPath( absoluteFilename ); if ( found is not null ) return found; // create an empty file System.IO.File.WriteAllText( absoluteFilename, "{}" ); // convert it to an .asset var asset = RegisterFile( absoluteFilename ); if ( asset == null ) { log.Warning( $"Something went wrong when registering {absoluteFilename}" ); return null; } // // Load and save it, to create an empty version of it // if ( asset.TryLoadGameResource( typeof( GameResource ), out var obj, true ) ) { asset.SaveToDisk( obj ); } return asset; } internal static void OnSoundReload( string filename ) { if ( SoundFile.Loaded.TryGetValue( filename, out var soundFile ) ) { soundFile.OnReloadInternal(); } } internal static void OnSoundReloaded( string filename ) { if ( SoundFile.Loaded.TryGetValue( filename, out var soundFile ) ) { soundFile.OnReloadedInternal(); } } internal static void OnDemandRecompile( uint index, string reason ) { // matt: disabling this for now because it's causing re-entrants in CResourceSystem::ForceSynchronizationAndBlockUntilManifestLoaded() // which was making the entire resource system shit the bed // specifically I was seeing this because Qt events run CHammerEditorSession::RenderView but I imagine it can happen // in a shit load of places... layla said they'd got this in-game too but I'm not sure if it's the same thing // IToolsDll.Current?.Spin(); } /// /// Passed parameters for the AssetPicker going from engine to addon code /// [EditorBrowsable( EditorBrowsableState.Never )] public struct AssetPickerParameters { public Widget ParentWidget { get; init; } public List FilterAssetTypes { get; init; } public Action AssetSelectedCallback { get; init; } public int ViewMode { get; init; } public Asset InitialSelectedAsset { get; init; } public string Title { get; init; } public bool ShowCloudAssets { get; init; } public string InitialSearchText { get; init; } } internal static void PopulateAssetMenu( Native.QMenu qMenu, IAsset asset ) { var menu = new Menu( qMenu ); EditorEvent.Run( "asset.nativecontextmenu", menu, AssetSystem.Get( asset ) ); } /// /// Called from native to open our managed AssetPicker /// internal static void OpenPicker( Native.QWidget parentWidget, CUtlVectorAssetType assetTypes, IAssetPickerListener listener, int viewmode, IAsset selectedAsset, string titleAndSettingsName, bool cloudAllowed, string initialSearchText ) { List filterAssetTypes = new(); if ( assetTypes.IsValid ) { for ( int i = 0; i < assetTypes.Count(); i++ ) { filterAssetTypes.Add( AssetType.Find( assetTypes.Element( i ).GetPrimaryExtension() ) ); } } Action assetsPicked = ( Asset[] assets ) => { if ( !listener.IsValid ) return; var picked = AssetPickedWrapper.Create(); foreach ( var asset in assets ) { if ( asset is NativeAsset nativeAsset ) { picked.AddAsset( nativeAsset.native ); } } listener.NotifyAssetPicked( picked ); picked.DeleteThis(); }; AssetPickerParameters parameters = new() { ParentWidget = parentWidget.IsValid ? new Widget( parentWidget ) : null, InitialSelectedAsset = selectedAsset.IsValid ? AssetSystem.Get( selectedAsset ) : null, FilterAssetTypes = filterAssetTypes, AssetSelectedCallback = assetsPicked, ViewMode = viewmode, Title = titleAndSettingsName, ShowCloudAssets = true, InitialSearchText = initialSearchText }; EditorEvent.Run( "assetsystem.openpicker", parameters ); } static ulong _freeIndex = uint.MaxValue; internal static void AddAssetsFromMount( Sandbox.Mounting.BaseGameMount source ) { foreach ( var file in source.Resources ) { var index = _freeIndex++; var assetType = AssetType.ResolveFromPath( file.Path ); if ( assetType == null ) { //Log.Warning( $"assetType was null for {file.Path}" ); continue; } var asset = new MountAsset( index, file, source ); allAssets[asset.AssetId] = asset; UpdateQueue.Add( asset ); HasChanges = true; } Tick(); } /// /// Callbacks for the asset system. Add this interface to your Widget to get events. /// public interface IEventListener { /// /// An asset has been modified /// void OnAssetChanged( Asset asset ) { } /// /// The thumbnail for an asset has been updated /// void OnAssetThumbGenerated( Asset asset ) { } /// /// Changes have been detected in the asset system. We won't tell you what, but /// you probably need to update the asset list or something. /// void OnAssetSystemChanges() { } /// /// Called when a new tag has been added to the asset system. /// void OnAssetTagsChanged() { } } /// /// Create an Asset from a serialized property. This is expected to be an embedded asset property. /// public static Asset CreateEmbeddedAsset( SerializedProperty target ) { return new EmbeddedAsset( target ); } }