Files
sbox-public/engine/Sandbox.Tools/GameData/ModelDocParser.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

187 lines
5.9 KiB
C#

using System;
using System.Reflection;
using System.Text.Json.Serialization;
namespace Editor;
internal static partial class ModelDocParser
{
internal static MapClass ParseType( Type type )
{
if ( type.GetInterface( "Sandbox.IModelBreakCommand" ) == null && !type.HasAttribute( typeof( Sandbox.ModelEditor.GameDataAttribute ) ) )
{
Log.Warning( $"Unsupported ModelDoc type {type}" );
return null;
}
string nodeName;
string description;
var gdAttr = type.GetCustomAttribute<Sandbox.ModelEditor.GameDataAttribute>( false );
if ( gdAttr == null )
{
var libAttr = type.GetCustomAttribute<LibraryAttribute>( false );
if ( libAttr == null ) return null;
nodeName = libAttr.Name;
description = libAttr.Description;
}
else
{
nodeName = gdAttr.Name;
description = gdAttr.Description;
}
var descAtt = type.GetCustomAttributes<DescriptionAttribute>().FirstOrDefault();
if ( string.IsNullOrEmpty( description ) && descAtt != null && descAtt.Value != null ) description = descAtt.Value;
var classType = GameDataClassType.ModelGameData;
if ( type.GetInterface( "Sandbox.IModelBreakCommand" ) != null && gdAttr == null )
classType = GameDataClassType.ModelBreakCommand;
// TODO: @ModelAnimEvent
var mapClass = new MapClass( nodeName );
mapClass.Description = description;
mapClass.ClassType = classType;
if ( gdAttr != null && gdAttr.AllowMultiple )
{
mapClass.EditorHelpers.Add( new Tuple<string, string[]>( "game_data_list", new string[] { gdAttr.ListName ?? $"{gdAttr.Name}_list" } ) );
}
// TODO: Have ModelDoc display some kind of nice name for these nodes?
//var displayInfo = DisplayInfo.ForType( type, false );
//mapClass.DisplayName = displayInfo.Name ?? libAttr.Name;
//mapClass.Icon = displayInfo.Icon;
ParseProperties( type, mapClass );
EntityParser.ParseAttributes( type, mapClass );
return mapClass;
}
// I hate duplicating code
internal static void ParseProperties( Type type, MapClass mapClass )
{
var properties = type.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy );
foreach ( var prop in properties )
{
// Skip read only properties, and properties that point to themselves.
if ( !prop.CanWrite ) continue;
if ( prop.PropertyType == type )
{
Log.Warning( $"FGDWriter: Ignoring property {type.Name}.{prop.Name} that has same type {prop.PropertyType} as its parent." );
continue;
}
var skipAttr = prop.GetCustomAttribute<HideAttribute>( true );
if ( skipAttr != null ) continue;
// Basic property info
var displayInfo = DisplayInfo.ForMember( prop );
string propTitle = displayInfo.Name ?? prop.Name.ToTitleCase();
string propDesc = displayInfo.Description ?? "";
string propName = prop.Name.ToLower();
if ( prop.GetCustomAttribute<System.ObsoleteAttribute>() is ObsoleteAttribute obsolete )
{
propDesc = $"<font color=\"#f80\"><b>Marked as obsolete: {obsolete.Message ?? "No reason given."}</b></font><br><br>" + propDesc;
}
var jsonName = prop.GetCustomAttribute<JsonPropertyNameAttribute>( true );
if ( jsonName != null ) propName = jsonName.Name;
// Skip unwanted stuff from base classes.
var shouldSkip = false;
foreach ( var attrib in type.GetCustomAttributes<HidePropertyAttribute>() )
{
if ( attrib.PropertyName == propName )
{
shouldSkip = true;
break;
}
}
if ( shouldSkip ) continue;
// Default value
object defaultValue = null;
var dv = prop.GetCustomAttribute<DefaultValueAttribute>();
if ( dv != null && dv.Value != null )
{
defaultValue = dv.Value;
}
var variable = new MapClassVariable();
variable.Name = propName;
variable.LongName = propTitle;
variable.Description = propDesc;
variable.DefaultValue = defaultValue;
variable.PropertyType = prop.PropertyType;
variable.GroupName = displayInfo.Group;
// TODO: Structs
/*var isStruct = prop.PropertyType.IsValueType && !prop.PropertyType.IsPrimitive && !prop.PropertyType.IsEnum;
if ( structOut != null && (prop.PropertyType.IsClass || isStruct) )
{
AssetStructToString( t, structOut );
}*/
//
// Not a fan of this stuff, but just keeping it so I don't fuck everything up now.
//
{
var fgdTypeAttr = prop.GetCustomAttribute<FGDTypeAttribute>();
if ( fgdTypeAttr != null )
{
variable.PropertyTypeOverride = fgdTypeAttr.Type;
if ( prop.PropertyType.IsArray || prop.PropertyType.IsAssignableTo( typeof( IList ) ) ) variable.PropertyTypeOverride = $"array:{fgdTypeAttr.Type}";
if ( !string.IsNullOrEmpty( fgdTypeAttr.Editor ) ) variable.Metadata["editor"] = fgdTypeAttr.Editor;
}
if ( prop.PropertyType.IsAssignableTo( typeof( IAsset ) ) )
{
var assetname = prop.PropertyType.GetCustomAttribute<LibraryAttribute>()?.Name ?? prop.PropertyType.Name;
variable.Metadata["editor"] = $"AssetBrowse({assetname.ToLower()})";
}
}
//
// Metadata
//
var metaDataAttrs = prop.GetCustomAttributes<FieldMetaDataAttribute>();
foreach ( var mdAttr in metaDataAttrs )
{
mdAttr.AddMetaData( variable.Metadata );
}
foreach ( var meta in prop.GetCustomAttributes<MinMaxAttribute>() )
{
variable.Metadata["min"] = meta.MinValue.ToString();
variable.Metadata["max"] = meta.MaxValue.ToString();
}
//
// Add mapClass helpers from properties
// TODO: Maybe make this less hardcoded if more such attributes are added
//
var pointLineAttr = prop.GetCustomAttributes<PointLineAttribute>();
foreach ( var plAttr in pointLineAttr )
{
// TODO: Maybe also also do color with variables 2,3,4?
mapClass.EditorHelpers.Add( new Tuple<string, string[]>( plAttr.Local ? "vecline_local" : "vecline", new string[] { variable.Name } ) );
if ( !plAttr.Local && variable.DefaultValue == null ) variable.DefaultValue = ""; // Override the "0 0 0" default value for vectors
}
mapClass.Variables.Add( variable );
}
}
}