using Sandbox.Internal; using Sandbox.Utility; using System.Collections.Concurrent; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; namespace Sandbox.Resources; /// /// Creates a resource from a json definition /// [Expose] public abstract class ResourceGenerator { public struct Options { /// /// True if we're compiling this resource to write to disk /// public bool ForDisk { get; set; } /// /// Will be set to the compiler that is currently compiling this resource. Or null, if we're generating in another method. /// public ResourceCompiler Compiler { get; set; } public static Options Default => new Options { }; } /// /// If true then the generation will create a real resource and store it on disk. /// Use this if creating the resource takes a while, or you won't be shipping the generator /// with the game, or if it relies on data that won't be available in the shipped game. /// [JsonIgnore, Hide] public virtual bool CacheToDisk => false; /// /// Create a ResourceGenerator by name /// public static ResourceGenerator Create( string generatorName ) where T : Resource { var typeLibrary = TypeLibrary.Editor ?? Game.TypeLibrary; if ( typeLibrary is null ) return default; var t = typeLibrary.GetType>( generatorName ); if ( t == null ) return default; return t.Create>(); } /// /// Create a ResourceGenerator by name and deserialize it /// public static ResourceGenerator Create( EmbeddedResource serialized ) where T : Resource { // find the generator name if ( string.IsNullOrEmpty( serialized.ResourceGenerator ) ) return default; // create the generator type var generator = Create( serialized.ResourceGenerator ); if ( generator is null ) return default; // fill it with json generator.Deserialize( serialized.Data ); return generator; } public static T CreateResource( EmbeddedResource obj, Options options ) where T : Resource { // create the generator type var generator = Create( obj ); if ( generator is null ) return default; // create the resource return generator.FindOrCreate( options ); } /// /// Create a resource from an embedded resource with a given /// /// /// /// /// public static Resource CreateResource( EmbeddedResource obj, Options options, Type type ) { var typeLibrary = TypeLibrary.Editor ?? Game.TypeLibrary; if ( typeLibrary is null ) return default; var t = typeLibrary.GetType( obj.ResourceGenerator ); if ( t == null ) return default; var generator = t.Create(); // fill it with json generator.Deserialize( obj.Data ); // create the resource return generator.FindOrCreateObject( options ) as Resource; } /// /// Copy properties from obj to us /// public virtual void Deserialize( JsonObject obj ) { if ( obj is null ) return; Json.DeserializeToObject( this, obj ); } /// /// Returns a hash to be used when loading/saving. We use this to determine if the resource has changed. /// By default we serialize the generator to a json string and return the CRC64 of that value. You can /// override this in your generator if you need to make it faster, or ignore some stuff. /// public virtual ulong GetHash() { var jsonString = Json.Serialize( this ); return Crc64.FromString( jsonString ); } /// /// If we generated this before, then find the current cache'd value. /// If not, then generate a new one. /// public abstract ValueTask FindOrCreateObjectAsync( Options options, CancellationToken token ); /// /// Find or create the resource (blocking) /// /// /// public abstract Resource FindOrCreateObject( Options options ); } /// /// A resource generator targetting a specific type /// public abstract class ResourceGenerator : ResourceGenerator where T : Resource { static WeakDictionary cache = new(); /// /// If true then the generation will avoid creating duplicate resources by checking /// hash codes of previously generated resources and re-using them if possible. /// [Hide, JsonIgnore] public virtual bool UseMemoryCache => true; /// /// Find a previously created of this resource /// public virtual T FindCached() { if ( !UseMemoryCache ) return default; var hash = GetHash(); if ( cache.TryGetValue( hash, out var value ) ) { return value; } return default; } /// /// Add this resource to the cache for our current hash /// public void AddToCache( T val ) { if ( !UseMemoryCache ) return; if ( val == default ) return; cache.Set( GetHash(), val ); } /// /// If we generated this before, then find the current cache'd value. /// If not, then generate a new one. /// public virtual T FindOrCreate( Options options ) { if ( FindCached() is { } cached ) return cached; var x = Create( options ); AddToCache( x ); return x; } public override async ValueTask FindOrCreateObjectAsync( Options options, CancellationToken token ) => await FindOrCreateAsync( options, token ); public override Resource FindOrCreateObject( Options options ) => FindOrCreate( options ); /// /// If we generated this before, then find the current cache'd value. /// If not, then generate a new one. /// public virtual async ValueTask FindOrCreateAsync( Options options, CancellationToken token ) { if ( FindCached() is { } cached ) return cached; var hash = GetHash(); var v = await CreateAsync( options, token ); // If the cache value changed while we were generating // then throw this value away! if ( hash == GetHash() ) { AddToCache( v ); } return v; } /// /// Create the resource blocking /// public abstract T Create( Options options ); /// /// Create the resource asyncronously /// public abstract ValueTask CreateAsync( Options options, CancellationToken token ); } class WeakDictionary where TValue : class { private readonly ConcurrentDictionary> _table = new(); public void Set( TKey key, TValue value ) { var weakValue = new WeakReference( value ); _table[key] = weakValue; Cleanup(); } public bool TryGetValue( TKey key, out TValue value ) { Cleanup(); value = default; if ( !_table.TryGetValue( key, out var weak ) ) return false; return weak.TryGetTarget( out value ); } private void Cleanup() { foreach ( var kvp in _table ) { if ( !kvp.Value.TryGetTarget( out _ ) ) { _table.TryRemove( kvp.Key, out _ ); } } } }