using NativeEngine; using Sandbox.Engine; using System.Reflection; using System.Runtime.InteropServices; using static Sandbox.ResourceLibrary; namespace Sandbox; public class ResourceSystem { private Dictionary ResourceIndex { get; } = new(); internal void Register( Resource resource ) { Log.Trace( $"Registering {resource.GetType()} ( {resource.ResourcePath} ) as {resource.ResourceId}" ); ResourceIndex[resource.ResourceId] = resource; if ( resource is GameResource gameResource && !gameResource.IsPromise ) { IToolsDll.Current?.RunEvent( i => i.OnRegister( gameResource ) ); } } internal void Unregister( Resource resource ) { // This isn't thread safe ThreadSafe.AssertIsMainThread(); // Make sure we're unregistering the currently indexed resource if ( ResourceIndex.TryGetValue( resource.ResourceId, out var existing ) && existing == resource ) { // native asset system doesn't support asset removal right now, // so just remove it from the index to ensure we don't retrieve it anymore ResourceIndex.Remove( resource.ResourceId ); } else { Log.Trace( $"Unregistering \"{resource.ResourcePath}\", but it wasn't registered" ); } if ( resource is GameResource gameResource && !gameResource.IsPromise ) { IToolsDll.Current?.RunEvent( i => i.OnUnregister( gameResource ) ); } } internal void OnHotload() { TypeCache.Clear(); } internal void Clear() { // TODO: remove from native too? var toDispose = ResourceIndex.Values.ToArray(); ResourceIndex.Clear(); foreach ( var resource in toDispose.OfType() ) { resource.DestroyInternal(); } TypeCache.Clear(); } internal Resource Get( System.Type t, int identifier ) { if ( !ResourceIndex.TryGetValue( identifier, out var resource ) ) return null; if ( resource.GetType().IsAssignableTo( t ) ) return resource; return null; } internal Resource Get( System.Type t, string filepath ) { filepath = Resource.FixPath( filepath ); return Get( t, filepath.FastHash() ); } /// /// Get a cached resource by its hash. /// /// Resource type to get. /// Resource hash to look up. public T Get( int identifier ) where T : Resource { if ( !ResourceIndex.TryGetValue( identifier, out var resource ) ) return default; return resource as T; } /// /// Get a cached resource by its file path. /// /// Resource type to get. /// File path to the resource. public T Get( string filepath ) where T : Resource { filepath = Resource.FixPath( filepath ); return Get( filepath.FastHash() ); } /// /// Try to get a cached resource by its file path. /// /// Resource type to get. /// File path to the resource. /// The retrieved resource, if any. /// True if resource was retrieved successfully. public bool TryGet( string filepath, out T resource ) where T : Resource { resource = Get( filepath ); return resource != null; } /// /// Get all cached resources of given type. /// /// Resource type to get. public IEnumerable GetAll() { return ResourceIndex.Values.OfType().Distinct(); } /// /// Get all cached resources of given type in a specific folder. /// /// Resource type to get. /// The path of the folder to check. /// Whether or not to check folders within the specified folder. public IEnumerable GetAll( string filepath, bool recursive = true ) where T : Resource { filepath = filepath.Replace( '\\', '/' ); if ( !filepath.EndsWith( "/" ) ) filepath += "/"; return ResourceIndex.Values.OfType().Distinct().Where( x => { if ( x.ResourcePath.StartsWith( filepath ) ) { if ( recursive ) return true; if ( !x.ResourcePath.Substring( filepath.Length ).Contains( "/" ) ) return true; } return false; } ); } /// /// Read compiled resource as JSON from the provided buffer. /// internal unsafe string ReadCompiledResourceJson( Span data ) { fixed ( byte* ptr = data ) { return EngineGlue.ReadCompiledResourceFileJson( (IntPtr)ptr ); } } /// /// Read compiled resource as JSON from the provided file path. /// internal unsafe string ReadCompiledResourceJson( BaseFileSystem fs, string fileName ) { if ( !fs.FileExists( fileName ) ) return string.Empty; var data = fs.ReadAllBytes( fileName ); fixed ( byte* ptr = data ) { return EngineGlue.ReadCompiledResourceFileJson( (IntPtr)ptr ); } } internal unsafe byte[] ReadCompiledResourceBlock( string blockName, Span data ) { fixed ( byte* ptr = data ) { IntPtr blockData = EngineGlue.ReadCompiledResourceFileBlock( blockName, (IntPtr)ptr, out var size ); if ( blockData == IntPtr.Zero || size <= 0 ) return null; var result = new byte[size]; Marshal.Copy( blockData, result, 0, size ); return result; } } private Dictionary TypeCache { get; } = new( StringComparer.OrdinalIgnoreCase ); /// /// Get the for a given extension. /// internal bool TryGetType( string extension, out AssetTypeAttribute resourceAttribute ) { if ( extension.StartsWith( '.' ) ) extension = extension[1..]; if ( extension.EndsWith( "_c", StringComparison.OrdinalIgnoreCase ) ) extension = extension[..^2]; if ( TypeCache.TryGetValue( extension, out resourceAttribute ) ) return true; resourceAttribute = Game.TypeLibrary.GetAttributes() .FirstOrDefault( x => string.Equals( x.Extension, extension, StringComparison.OrdinalIgnoreCase ) ); if ( resourceAttribute != null ) { TypeCache[extension] = resourceAttribute; return true; } return false; } /// /// garry: why the fuck does this exist /// garry: fuck me why the fuck does this exist /// internal GameResource LoadRawGameResource( string path ) { var extension = System.IO.Path.GetExtension( path ); if ( !TryGetType( extension, out var type ) ) { Log.Warning( $"Could not find GameResource for extension '{extension}'" ); return null; } var json = EngineGlue.ReadCompiledResourceFileJsonFromFilesystem( path ); if ( string.IsNullOrEmpty( json ) ) { Log.Warning( $"Failed to load {path}" ); return null; } try { var se = GameResource.GetPromise( type.TargetType, path ); if ( se is null ) return null; se.LoadFromJson( json ); // se.LoadFromResource( data ); Register( se ); se.PostLoadInternal(); return se; } catch ( System.Exception ex ) { Log.Warning( ex, $" Error when deserializing {path} ({ex.Message})" ); } return null; } internal T LoadGameResource( string file, BaseFileSystem fs, bool deferPostload = false ) where T : GameResource { var attr = typeof( T ).GetCustomAttribute(); if ( attr == null ) return default; // this is filled in automatically when accessed via TypeLibrary // but this ain't TypeLibrary kiddo attr.TargetType = typeof( T ); return LoadGameResource( attr, file, fs, deferPostload ) as T; } /// /// Loads a Gameresource from disk. Doesn't look at cache. Registers the resource if successful. /// internal GameResource LoadGameResource( AssetTypeAttribute type, string file, BaseFileSystem fs, bool deferPostload = false ) { Assert.NotNull( type ); Assert.NotNull( file ); if ( !file.EndsWith( "_c" ) ) file += "_c"; Span data = null; try { if ( fs.FileExists( file ) ) { data = fs.ReadAllBytes( file ); } if ( data.Length <= 3 ) { Log.Warning( $" Skipping {file} (is null)" ); return null; } var se = GameResource.GetPromise( type.TargetType, file ); if ( se is null ) return null; se.TryLoadFromData( data ); if ( Application.IsEditor ) { var sourceFilePath = file.Substring( 0, file.Length - 2 ); if ( fs.FileExists( sourceFilePath ) ) { var jsonBlob = fs.ReadAllText( sourceFilePath ); se.LastSavedSourceHash = jsonBlob.FastHash(); } } // // garry: wtf is this for? maps? // if ( Application.IsDedicatedServer ) { InstallReferences( se ); } Register( se ); if ( !deferPostload ) se.PostLoadInternal(); return se; } catch ( System.Exception ex ) { Log.Warning( ex, $" Error when deserializing {file} ({ex.Message})" ); } return null; } /// /// Installs all references for a GameResource /// private void InstallReferences( GameResource se ) { var references = se.GetReferencedPackages(); foreach ( var r in references ) { _ = PackageManager.InstallAsync( new PackageLoadOptions() { PackageIdent = r, ContextTag = "server" } ); } } } /// /// Keeps a library of all available . /// public static class ResourceLibrary { /// /// Get a cached resource by its hash. /// /// Resource type to get. /// Resource hash to look up. public static T Get( int identifier ) where T : Resource => Game.Resources.Get( identifier ); /// /// Get a cached resource by its file path. /// /// Resource type to get. /// File path to the resource. public static T Get( string filepath ) where T : Resource => Game.Resources.Get( filepath ); /// /// Try to get a cached resource by its file path. /// /// Resource type to get. /// File path to the resource. /// The retrieved resource, if any. /// True if resource was retrieved successfully. public static bool TryGet( string filepath, out T resource ) where T : Resource => Game.Resources.TryGet( filepath, out resource ); /// /// Get all cached resources of given type. /// /// Resource type to get. public static IEnumerable GetAll() => Game.Resources.GetAll(); /// /// Get all cached resources of given type in a specific folder. /// /// Resource type to get. /// The path of the folder to check. /// Whether or not to check folders within the specified folder. public static IEnumerable GetAll( string filepath, bool recursive = true ) where T : Resource => Game.Resources.GetAll( filepath, recursive ); /// /// Load a resource by its file path. /// public static async Task LoadAsync( string path ) where T : Resource { // try to load cached version first if ( TryGet( path, out var cached ) ) return cached; // Check if the type is a GameResource, and handle it accordingly var type = typeof( T ); if ( type.IsSubclassOf( typeof( GameResource ) ) ) { // Really should be loaded already I think? return Get( path ); } if ( type == typeof( Model ) ) { return (T)(object)(await Sandbox.Model.LoadAsync( path )); } if ( type == typeof( Material ) ) { return (T)(object)(await Sandbox.Material.LoadAsync( path )); } if ( type == typeof( Shader ) ) { return (T)(object)(Sandbox.Shader.Load( path )); } return default; } public interface IEventListener { /// /// Called when a new resource has been registered /// void OnRegister( GameResource resource ) { } /// /// Called when a previously known resource has been unregistered /// void OnUnregister( GameResource resource ) { } /// /// Called when a resource has been saved /// void OnSave( GameResource resource ) { } /// /// Called when the source file of a known resource has been externally modified on disk /// void OnExternalChanges( GameResource resource ) { } /// /// Called when the source file of a known resource has been externally modified on disk /// and after it has been fully loaded (after post load is called) /// void OnExternalChangesPostLoad( GameResource resource ) { } } }