Files
sbox-public/engine/Sandbox.Engine/Resources/Compiling/ResourceGenerator.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

270 lines
7.0 KiB
C#

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;
/// <summary>
/// Creates a resource from a json definition
/// </summary>
[Expose]
public abstract class ResourceGenerator
{
public struct Options
{
/// <summary>
/// True if we're compiling this resource to write to disk
/// </summary>
public bool ForDisk { get; set; }
/// <summary>
/// Will be set to the compiler that is currently compiling this resource. Or null, if we're generating in another method.
/// </summary>
public ResourceCompiler Compiler { get; set; }
public static Options Default => new Options { };
}
/// <summary>
/// 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.
/// </summary>
[JsonIgnore, Hide]
public virtual bool CacheToDisk => false;
/// <summary>
/// Create a ResourceGenerator by name
/// </summary>
public static ResourceGenerator<T> Create<T>( string generatorName ) where T : Resource
{
var typeLibrary = TypeLibrary.Editor ?? Game.TypeLibrary;
if ( typeLibrary is null ) return default;
var t = typeLibrary.GetType<ResourceGenerator<T>>( generatorName );
if ( t == null ) return default;
return t.Create<ResourceGenerator<T>>();
}
/// <summary>
/// Create a ResourceGenerator by name and deserialize it
/// </summary>
public static ResourceGenerator<T> Create<T>( EmbeddedResource serialized ) where T : Resource
{
// find the generator name
if ( string.IsNullOrEmpty( serialized.ResourceGenerator ) ) return default;
// create the generator type
var generator = Create<T>( serialized.ResourceGenerator );
if ( generator is null ) return default;
// fill it with json
generator.Deserialize( serialized.Data );
return generator;
}
public static T CreateResource<T>( EmbeddedResource obj, Options options ) where T : Resource
{
// create the generator type
var generator = Create<T>( obj );
if ( generator is null ) return default;
// create the resource
return generator.FindOrCreate( options );
}
/// <summary>
/// Create a resource from an embedded resource with a given <see cref="Type"/>
/// </summary>
/// <param name="obj"></param>
/// <param name="options"></param>
/// <param name="type"></param>
/// <returns></returns>
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<ResourceGenerator>( obj.ResourceGenerator );
if ( t == null ) return default;
var generator = t.Create<ResourceGenerator>();
// fill it with json
generator.Deserialize( obj.Data );
// create the resource
return generator.FindOrCreateObject( options ) as Resource;
}
/// <summary>
/// Copy properties from obj to us
/// </summary>
public virtual void Deserialize( JsonObject obj )
{
if ( obj is null ) return;
Json.DeserializeToObject( this, obj );
}
/// <summary>
/// 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.
/// </summary>
public virtual ulong GetHash()
{
var jsonString = Json.Serialize( this );
return Crc64.FromString( jsonString );
}
/// <summary>
/// If we generated this before, then find the current cache'd value.
/// If not, then generate a new one.
/// </summary>
public abstract ValueTask<Resource> FindOrCreateObjectAsync( Options options, CancellationToken token );
/// <summary>
/// Find or create the resource (blocking)
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
public abstract Resource FindOrCreateObject( Options options );
}
/// <summary>
/// A resource generator targetting a specific type
/// </summary>
public abstract class ResourceGenerator<T> : ResourceGenerator where T : Resource
{
static WeakDictionary<ulong, T> cache = new();
/// <summary>
/// If true then the generation will avoid creating duplicate resources by checking
/// hash codes of previously generated resources and re-using them if possible.
/// </summary>
[Hide, JsonIgnore]
public virtual bool UseMemoryCache => true;
/// <summary>
/// Find a previously created of this resource
/// </summary>
public virtual T FindCached()
{
if ( !UseMemoryCache )
return default;
var hash = GetHash();
if ( cache.TryGetValue( hash, out var value ) )
{
return value;
}
return default;
}
/// <summary>
/// Add this resource to the cache for our current hash
/// </summary>
public void AddToCache( T val )
{
if ( !UseMemoryCache ) return;
if ( val == default ) return;
cache.Set( GetHash(), val );
}
/// <summary>
/// If we generated this before, then find the current cache'd value.
/// If not, then generate a new one.
/// </summary>
public virtual T FindOrCreate( Options options )
{
if ( FindCached() is { } cached )
return cached;
var x = Create( options );
AddToCache( x );
return x;
}
public override async ValueTask<Resource> FindOrCreateObjectAsync( Options options, CancellationToken token )
=> await FindOrCreateAsync( options, token );
public override Resource FindOrCreateObject( Options options ) => FindOrCreate( options );
/// <summary>
/// If we generated this before, then find the current cache'd value.
/// If not, then generate a new one.
/// </summary>
public virtual async ValueTask<T> 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;
}
/// <summary>
/// Create the resource blocking
/// </summary>
public abstract T Create( Options options );
/// <summary>
/// Create the resource asyncronously
/// </summary>
public abstract ValueTask<T> CreateAsync( Options options, CancellationToken token );
}
class WeakDictionary<TKey, TValue> where TValue : class
{
private readonly ConcurrentDictionary<TKey, WeakReference<TValue>> _table = new();
public void Set( TKey key, TValue value )
{
var weakValue = new WeakReference<TValue>( 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 _ );
}
}
}
}