Files
sbox-public/engine/Sandbox.Engine/Resources/GameResource.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

382 lines
8.9 KiB
C#

using Facepunch.ActionGraphs;
using Sandbox.ActionGraphs;
using Sandbox.Engine;
using Sandbox.Internal;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace Sandbox;
/// <summary>
/// Assets defined in C# and created through tools.
/// You can define your own <a href="https://sbox.game/dev/doc/assetsresources/custom-assets/">Custom Asset Types</a>.
/// </summary>
[Library]
public abstract partial class GameResource : Resource, ISourceLineProvider
{
/// <summary>
/// Allows tools to post process the serialized json object
/// </summary>
internal static Action<object, JsonObject> ProcessSerializedObject { get; set; }
[JsonInclude]
[JsonPropertyName( "__references" )]
internal string[] referencedPackages { get; set; }
/// <summary>
/// The last saved compiled hash for this file.
/// </summary>
private int _jsonHash;
/// <summary>
/// The last saved uncompiled hash for this file. We use this to detect external changes in the editor, it's never serialized.
/// </summary>
[Hide, JsonIgnore]
internal int LastSavedSourceHash { get; set; }
/// <summary>
/// Re-use ActionGraph instances when deserializing this resource.
/// </summary>
[JsonIgnore]
internal ActionGraphCache ActionGraphCache { get; } = new();
bool _unsavedChanges;
/// <summary>
/// True if this resource has changed but the changes aren't written to disk
/// </summary>
[Hide, JsonIgnore]
public sealed override bool HasUnsavedChanges => _unsavedChanges;
/// <summary>
/// Should be called after the resource has been edited by the inspector
/// </summary>
public sealed override void StateHasChanged()
{
_unsavedChanges = true;
}
/// <summary>
/// True if we're waiting for our load to complete
/// </summary>
bool _awaitingLoad;
/// <summary>
/// True if we're a promise, waiting to finalize the load
/// </summary>
internal bool IsPromise => _awaitingLoad;
/// <summary>
/// Get a list of packages that are needed to load this asset
/// </summary>
public IEnumerable<string> GetReferencedPackages()
{
return referencedPackages ?? Array.Empty<string>();
}
/// <summary>
/// Called when the asset is first loaded from disk.
/// </summary>
protected virtual void PostLoad()
{
}
internal bool PostLoadInternal()
{
_awaitingLoad = false;
try
{
PostLoad();
return true;
}
catch ( System.Exception e )
{
Log.Error( e );
return false;
}
}
/// <summary>
/// Called when the asset is recompiled/reloaded from disk.
/// </summary>
protected virtual void PostReload()
{
}
internal bool PostReloadInternal()
{
_awaitingLoad = false;
try
{
PostReload();
return true;
}
catch ( System.Exception e )
{
Log.Error( e );
return false;
}
}
/// <summary>
/// Creates an instance of this type that will get loaded into later. This allows us to
/// have resources that reference other resources that aren't loaded yet (or are missing).
/// </summary>
internal static GameResource GetPromise( System.Type type, string filename )
{
var path = FixPath( filename );
var hash = path.FastHash();
var obj = Game.Resources.Get( type, hash ) as GameResource;
if ( obj != null ) return obj;
obj = System.Activator.CreateInstance( type ) as GameResource;
if ( obj is null )
{
Log.Warning( $"Failed to create '{type.FullName}'" );
return default;
}
obj.InternalInitialize( filename );
Game.Resources.Register( obj );
return obj;
}
private void InternalInitialize( string filename )
{
ResourcePath = FixPath( filename );
ResourceName = System.IO.Path.GetFileNameWithoutExtension( ResourcePath );
ResourceId = ResourcePath.FastHash();
Manifest = AsyncResourceLoader.Load( ResourcePath );
_awaitingLoad = true;
}
/// <summary>
/// Makes sure all properties are derived properly from filename, and then registered to ResourceLibrary
/// </summary>
internal void Register( string filename )
{
if ( string.IsNullOrEmpty( filename ) ) return;
ResourcePath = FixPath( filename );
ResourceName = System.IO.Path.GetFileNameWithoutExtension( ResourcePath );
ResourceId = ResourcePath.FastHash();
Game.Resources.Register( this );
}
/// <summary>
/// Loads a game resource from given file.
/// </summary>
internal static T Load<T>( string filename ) where T : GameResource
{
if ( ResourceLibrary.TryGet<T>( filename, out var resource ) )
{
return resource;
}
return null;
}
private SerializationOptions _serializationOptions;
[JsonIgnore, Hide]
internal SerializationOptions SerializationOptions => _serializationOptions ??= CreateSerializationOptions();
/// <summary>
/// Target type used for any action graphs contained in this resource.
/// Defaults to this resource's type.
/// </summary>
[JsonIgnore, Hide]
protected virtual Type ActionGraphTargetType => ActionGraphTarget?.GetType() ?? GetType();
/// <summary>
/// Target instance used for any action graphs contained in this resource.
/// Defaults to this resource itself.
/// </summary>
[JsonIgnore, Hide]
protected virtual object ActionGraphTarget => this;
private SerializationOptions CreateSerializationOptions()
{
return new(
Cache: ActionGraphCache,
WriteCacheReferences: false,
ForceUpdateCached: true,
SourceLocation: new GameResourceSourceLocation( this ),
ImpliedTarget: ActionGraphTargetType is { } targetType
? InputDefinition.Target( targetType, ActionGraphTarget )
: null );
}
/// <summary>
/// Pushes a context in which action graphs belonging to this resource can be serialized or deserialized.
/// </summary>
protected internal IDisposable PushSerializationScope()
{
return ActionGraph.PushSerializationOptions( SerializationOptions );
}
/// <summary>
/// Serialize the current state to a JsonObject
/// </summary>
public JsonObject Serialize()
{
JsonObject jsobj;
using ( PushSerializationScope() )
{
jsobj = Json.SerializeAsObject( this );
OnJsonSerialize( jsobj );
}
jsobj["__version"] = ResourceVersion;
ProcessSerializedObject?.Invoke( this, jsobj );
return jsobj;
}
/// <summary>
/// called to upgrade a bunch of json to the latest version
/// </summary>
/// <param name="node"></param>
void JsonUpgrade( JsonObject node )
{
if ( ResourceVersion == 0 )
return;
var serializedVersion = (int)(node["__version"] ?? 0);
if ( serializedVersion >= ResourceVersion ) return;
JsonUpgrader.Upgrade( serializedVersion, node, GetType() );
}
string ISourcePathProvider.Path => ResourcePath;
int ISourceLineProvider.Line => 0;
/// <summary>
/// The version of the component. Used by <see cref="JsonUpgrader"/>.
/// </summary>
[JsonIgnore, Hide] public virtual int ResourceVersion => 0;
public void LoadFromJson( string json )
{
Assert.NotNull( json, "json should not be null" );
_jsonHash = json.FastHash();
var docOptions = new JsonDocumentOptions();
docOptions.MaxDepth = 512;
var nodeOptions = new JsonNodeOptions();
nodeOptions.PropertyNameCaseInsensitive = true;
var node = JsonNode.Parse( json, nodeOptions, docOptions );
if ( node is not JsonObject jso )
{
throw new ArgumentException( "Couldn't load json" );
}
JsonUpgrade( jso );
jso.Remove( "__version" );
Deserialize( jso );
_awaitingLoad = false;
}
/// <summary>
/// Deserialize values from a JsonObject
/// </summary>
public void Deserialize( JsonObject jso )
{
using ( PushSerializationScope() )
{
Json.DeserializeToObject( this, jso );
}
}
/// <summary>
/// Called after we serialize, allowing you to store any extra or modify the output.
/// </summary>
protected virtual void OnJsonSerialize( JsonObject node )
{
}
internal bool TryLoadFromData( Span<byte> data )
{
var json = Game.Resources.ReadCompiledResourceJson( data );
if ( json is null )
return false;
// Prevent loading the same data twice
// todo - this should be a CRC on data
var newHash = json.FastHash();
if ( newHash == _jsonHash )
return false;
LoadFromJson( json );
LoadFromResource( data );
return true;
}
internal virtual bool LoadFromResource( Span<byte> data )
{
return false;
}
internal virtual void SaveToDisk( string filename, string jsonString )
{
_unsavedChanges = false;
LastSavedSourceHash = jsonString.FastHash();
System.IO.File.WriteAllText( filename, jsonString );
IToolsDll.Current?.RunEvent<ResourceLibrary.IEventListener>( i => i.OnSave( this ) );
}
internal virtual bool CanLoadFromJson()
{
return true;
}
private bool _destroyed;
[Hide, JsonIgnore] public sealed override bool IsValid => !_destroyed;
/// <summary>
/// Called when this resource is being unloaded.
/// Clean up any resources owned by this instance here.
/// </summary>
protected virtual void OnDestroy()
{
}
internal void DestroyInternal()
{
if ( _destroyed ) return;
_destroyed = true;
Game.Resources.Unregister( this );
try
{
OnDestroy();
}
catch ( Exception ex )
{
Log.Warning( ex, $"{ex.GetType().Name} when destroying {ResourcePath}" );
}
}
}