Editor/McpExtras.ModelSkeleton.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Editor.Mcp;

public static partial class ExtrasTools
{
	/// <summary>
	/// Return read-only model-space bounds, bones, attachment ownership, material groups and animation
	/// names from an installed model resource. Unavailable complete fields are null with a reason;
	/// inspected empty collections remain empty. Skinning presence is unavailable in installed public APIs.
	/// </summary>
	/// <param name="model">Model asset path resolvable by AssetSystem.FindByPath.</param>
	[McpTool.ReadOnly( "x_model_skeleton" )]
	public static ModelSkeletonResult GetModelSkeleton( string model )
	{
		if ( string.IsNullOrWhiteSpace( model ) ) throw new Exception( "Give a model asset path." );
		var asset = AssetSystem.FindByPath( model.Replace( '\\', '/' ) )
			?? throw new Exception( $"Model asset '{Safe( model, 512 )}' was not found." );
		Model resource;
		try { resource = asset.LoadResource<Model>(); }
		catch ( Exception ex ) { throw new Exception( $"Model asset could not be loaded: {Safe( ex.Message, 512 )}" ); }
		if ( resource is null || !resource.IsValid || resource.IsError )
			throw new Exception( $"Model asset '{Safe( asset.Path, 512 )}' did not load as a valid non-error model resource." );

		var result = new ModelSkeletonResult { ModelAsset = asset.Path?.Replace( '\\', '/' ) };
		Capture( "Bounds", () =>
		{
			var bounds = resource.RenderBounds;
			result.Bounds = new ModelBoundsEvidence { Space = "model", Mins = bounds.Mins, Maxs = bounds.Maxs };
		}, result );
		Capture( "Bones", () => result.Bones = resource.Bones.AllBones.Select( x => new ModelBoneEvidence
		{
			Index = x.Index, Name = x.Name, ParentIndex = x.Parent?.Index ?? -1
		} ).ToList(), result );
		Capture( "Attachments", () => result.Attachments = resource.Attachments.All.Select( x => new ModelAttachmentEvidence
		{
			Name = x.Name, BoneIndex = x.Bone?.Index
		} ).ToList(), result );
		Capture( "MaterialGroups", () =>
		{
			var groups = new List<ModelMaterialGroupEvidence>();
			for ( var i = 0; i < resource.MaterialGroupCount; ++i )
			{
				groups.Add( new ModelMaterialGroupEvidence
				{
					Index = i,
					Name = resource.GetMaterialGroupName( i ),
					Materials = resource.GetMaterials( i ).Select( x => x?.ResourcePath?.Replace( '\\', '/' ) ).Where( x => x is not null ).ToList()
				} );
			}
			result.MaterialGroups = groups;
		}, result );
		Capture( "AnimationSequences", () =>
		{
			var animations = new List<string>( resource.AnimationCount );
			for ( var i = 0; i < resource.AnimationCount; ++i ) animations.Add( resource.GetAnimationName( i ) );
			result.AnimationSequences = animations;
		}, result );
		result.HasSkinningData = null;
		result.UnavailableFields["HasSkinningData"] = "Installed public model APIs do not expose skinning-data presence.";
		return result;
	}

	private static void Capture( string field, Action inspect, ModelSkeletonResult result )
	{
		try { inspect(); }
		catch ( Exception ex ) { result.UnavailableFields[field] = Safe( ex.Message, 256 ); }
	}
}

public sealed class ModelSkeletonResult
{
	public string ModelAsset { get; set; }
	public ModelBoundsEvidence Bounds { get; set; }
	public List<ModelBoneEvidence> Bones { get; set; }
	public List<ModelAttachmentEvidence> Attachments { get; set; }
	public List<ModelMaterialGroupEvidence> MaterialGroups { get; set; }
	public List<string> AnimationSequences { get; set; }
	public bool? HasSkinningData { get; set; }
	public Dictionary<string, string> UnavailableFields { get; set; } = new( StringComparer.Ordinal );
}

public sealed class ModelBoundsEvidence
{
	public string Space { get; set; }
	public Vector3 Mins { get; set; }
	public Vector3 Maxs { get; set; }
}

public sealed class ModelBoneEvidence
{
	public int Index { get; set; }
	public string Name { get; set; }
	public int ParentIndex { get; set; }
}

public sealed class ModelAttachmentEvidence
{
	public string Name { get; set; }
	public int? BoneIndex { get; set; }
}

public sealed class ModelMaterialGroupEvidence
{
	public int Index { get; set; }
	public string Name { get; set; }
	public List<string> Materials { get; set; } = [];
}