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

192 lines
5.4 KiB
C#

using NativeEngine;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Sandbox
{
public partial class Model
{
Dictionary<string, object> DataCache = new();
readonly JsonSerializerOptions jsonOptions = new()
{
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNameCaseInsensitive = true,
IncludeFields = true,
Converters =
{
new JsonStringEnumConverter()
}
};
internal string GetJson( string keyname )
{
if ( string.IsNullOrEmpty( Name ) ) Log.Warning( $"Warning: Trying to access Model Key Value '{keyname}' on an empty model!" );
var key = native.FindModelSubKey( keyname );
if ( key.IsNull ) return null;
return EngineGlue.KeyValues3ToJson( key );
}
internal string DeduceKeyName( Type type )
{
var outputType = type;
var dataType = type;
if ( dataType.IsArray )
{
dataType = dataType.GetElementType();
}
// Try to warn if the data types are mismatched
var gdAttr = dataType.GetCustomAttribute<ModelEditor.GameDataAttribute>();
if ( gdAttr == null ) throw new ArgumentException( $"{type.Name} does not have ModelDoc.GameDataAttribute" );
if ( !gdAttr.AllowMultiple && outputType.IsArray ) throw new ArgumentException( $"{type.Name} is not a list, but an array generic parameter was given" );
if ( gdAttr.AllowMultiple && !outputType.IsArray ) throw new ArgumentException( $"{type.Name} is a list, but non array generic parameter was given" );
var keyname = gdAttr.Name;
if ( gdAttr.AllowMultiple ) keyname = gdAttr.ListName ?? $"{keyname}_list";
return keyname;
}
/// <summary>
/// Tries to extract data from model based on the given type's <see cref="ModelEditor.GameDataAttribute">ModelDoc.GameDataAttribute</see>.
/// </summary>
/// <param name="data">The extracted data, or default on failure.</param>
/// <returns>true if data was extracted successfully, false otherwise.</returns>
public bool TryGetData<T>( out T data )
{
bool ret = TryGetData( typeof( T ), out object dat );
data = (T)dat;
return ret;
}
/// <summary>
/// Tries to extract data from model based on the given type's <see cref="ModelEditor.GameDataAttribute">ModelDoc.GameDataAttribute</see>.
/// </summary>
/// <param name="data">The extracted data, or default on failure.</param>
/// <param name="t">The class with <see cref="ModelEditor.GameDataAttribute">ModelDoc.GameDataAttribute</see>.</param>
/// <returns>true if data was extracted successfully, false otherwise.</returns>
public bool TryGetData( Type t, out object data )
{
string keyname = DeduceKeyName( t );
string json;
// Try to get data from cache
var cacheKey = $"{keyname}~{t.Name}";
if ( DataCache != null && DataCache.TryGetValue( cacheKey, out object cacheValue ) )
{
json = (string)cacheValue;
}
else
{
json = GetJson( keyname );
if ( string.IsNullOrWhiteSpace( json ) )
{
data = default;
return false;
}
// Special case of nodes that have no properties
if ( json == "null" ) json = "{}";
// Add to cache
if ( DataCache == null ) DataCache = new Dictionary<string, object>();
DataCache.Add( cacheKey, json );
}
try
{
var obj = JsonSerializer.Deserialize( json, t, jsonOptions );
if ( obj == null )
{
data = default;
return false;
}
data = obj;
return true;
}
catch ( Exception e )
{
Log.Warning( e, $"Failed to deserialize '{keyname}' to {t.Name}" );
}
data = default;
return false;
}
/// <summary>
/// Tests if this model has generic data based on given type's <see cref="ModelEditor.GameDataAttribute">ModelDoc.GameDataAttribute</see>.
/// This will be faster than testing this via GetData<![CDATA[<>]]>()
/// </summary>
public bool HasData<T>()
{
var keyname = DeduceKeyName( typeof( T ) );
return native.FindModelSubKey( keyname ).IsValid;
}
/// <summary>
/// Extracts data from model based on the given type's <see cref="ModelEditor.GameDataAttribute">ModelDoc.GameDataAttribute</see>.
/// </summary>
public T GetData<T>()
{
if ( TryGetData( out T data ) )
{
return data;
}
return default;
}
#region BreakCommands
/// <summary>
/// Internal function used to get a list of break commands the model has.
/// </summary>
public Dictionary<string, string[]> GetBreakCommands()
{
var cacheKey = $"break_command_list~{Name}";
if ( DataCache != null && DataCache.TryGetValue( cacheKey, out object cacheValue ) )
{
return (Dictionary<string, string[]>)cacheValue;
}
Dictionary<string, string[]> output = new();
var jsonString = GetJson( "break_command_list" );
if ( jsonString == null ) return output;
using ( JsonDocument document = JsonDocument.Parse( jsonString ) )
{
JsonElement root = document.RootElement;
if ( root.ValueKind != JsonValueKind.Array ) return output;
foreach ( JsonElement commandData in root.EnumerateArray() )
{
if ( commandData.TryGetProperty( "break_command", out JsonElement command ) )
{
var cmd = command.GetString();
List<string> arr = output.ContainsKey( cmd ) ? output[cmd].Cast<string>().ToList() : new();
arr.Add( commandData.GetRawText() );
output[cmd] = arr.ToArray();
}
}
}
if ( DataCache == null ) DataCache = new Dictionary<string, object>();
DataCache.Add( cacheKey, output );
return output;
}
#endregion
}
}