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 _ );
}
}
}
}