Editor/WeaponImporter/Tool/Engine/CharacterLibrary.cs
using Sandbox;
using WeaponImporter.Core.Grip;
using WeaponImporter.Core.Ik;
using WeaponImporter.Core.Maths;
using WeaponImporter.Core.Rig;

namespace WeaponImporter.Tool;

/// <summary>The characters grips are fitted to, and how to read their skeletons and poses.</summary>
public static class CharacterLibrary
{
    public const string Human = "models/citizen_human/citizen_human_male.vmdl";
    public const string HumanFemale = "models/citizen_human/citizen_human_female.vmdl";
    public const string Citizen = "models/citizen/citizen.vmdl";

    public static string ModelPath( string character ) => character switch
    {
        "human" or "" or null => Human,
        "human_female" => HumanFemale,
        "citizen" => Citizen,
        _ => character,
    };

    /// <summary>A short label for a character id or custom model path.</summary>
    public static string Label( string character ) => character switch
    {
        "human" or "" or null => "Human",
        "human_female" => "Human (female)",
        "citizen" => "Citizen",
        _ => System.IO.Path.GetFileNameWithoutExtension( character ),
    };

    /// <summary>
    /// Why a model can't be used as the third-person character, or null when it can: grips are
    /// fitted to the character's own hold animations, so it needs an animation graph (the
    /// citizen/human one or a compatible graph with the holdtype parameters) and arms with hands.
    /// </summary>
    public static string Problem( string character )
    {
        var path = ModelPath( character );
        var model = Model.Load( path );
        if ( model is null || model.IsError )
            return $"'{path}' could not be loaded.";
        if ( model.AnimGraph is null && model.AnimationCount == 0 )
            return $"{Label( character )} has no animations (a static body). Third-person grips are fitted to the character's hold animations: use a model that includes the citizen or human animations.";
        if ( GraphFor( model ) is null )
            return $"{Label( character )} has no animation graph and its skeleton doesn't match the citizen or human one. Third-person grips are fitted to the character's hold animations.";
        var rig = CharacterRig.From( SkeletonOf( model ) );
        if ( rig is null )
            return $"{Label( character )} has no recognisable right arm and hand.";
        if ( rig.Left is null )
            return $"{Label( character )} has no recognisable left arm and hand.";
        return null;
    }

    /// <summary>
    /// True when the model copies the ring finger onto the pinky with a constraint (the human
    /// models do: "Citizen animations don't have a fifth finger"), so a pinky override never shows.
    /// Read from the model's source; false when it has none.
    /// </summary>
    public static bool PinkyFollowsRing( string modelPath )
    {
        try
        {
            var source = Editor.AssetSystem.FindByPath( modelPath )?.GetSourceFile( true );
            if ( string.IsNullOrEmpty( source ) || !System.IO.File.Exists( source ) )
                return false;
            return System.Text.RegularExpressions.Regex.IsMatch( System.IO.File.ReadAllText( source ),
                @"parent_bone\s*=\s*""finger_ring_0_R""[^{}]*\}\s*,\s*\{[^{}]*AnimConstraintSlave[^{}]*parent_bone\s*=\s*""finger_pinky_0_R""" );
        }
        catch ( Exception e )
        {
            Log.Warning( $"Weapon importer: couldn't read the constraints of '{modelPath}': {e.Message}" );
            return false;
        }
    }

    /// <summary>
    /// Bones the citizen and human animgraphs drive by name. A custom body (for example the
    /// citizen mannequin) often has only these: no IK, twist, hold or face helpers.
    /// </summary>
    private static readonly string[] CoreBones =
    {
        "pelvis", "spine_0", "spine_1", "spine_2", "neck_0", "head",
        "clavicle_L", "arm_upper_L", "arm_lower_L", "hand_L", "clavicle_R", "arm_upper_R", "arm_lower_R", "hand_R",
        "leg_upper_L", "leg_lower_L", "ankle_L", "leg_upper_R", "leg_lower_R", "ankle_R",
    };

    /// <summary>
    /// The animgraph a character is posed with: its own, else (custom bodies on the citizen or
    /// human skeleton usually leave it to the player controller) the graph of the built-in
    /// character whose skeleton it shares most. Null when there is neither.
    /// </summary>
    public static AnimationGraph GraphFor( Model model ) => GraphFor( model, out _ );

    public static AnimationGraph GraphFor( Model model, out string borrowedFrom )
    {
        borrowedFrom = null;
        if ( model is null || model.IsError )
            return null;
        if ( model.AnimGraph is { } own )
            return own;
        // A graph plays the model's own sequences: without any there is nothing to borrow for.
        if ( model.AnimationCount == 0 )
            return null;
        var bones = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
        for ( var i = 0; i < model.BoneCount; i++ )
            bones.Add( model.GetBoneName( i ) );
        if ( !CoreBones.All( bones.Contains ) )
            return null;
        AnimationGraph best = null;
        var bestShare = -1f;
        foreach ( var path in new[] { Human, Citizen } )
        {
            var builtIn = Model.Load( path );
            if ( builtIn is null || builtIn.IsError || builtIn.AnimGraph is null || builtIn.BoneCount == 0 )
                continue;
            var shared = 0;
            for ( var i = 0; i < builtIn.BoneCount; i++ )
                if ( bones.Contains( builtIn.GetBoneName( i ) ) )
                    shared++;
            var share = shared / (float)builtIn.BoneCount;
            if ( share > bestShare )
            {
                bestShare = share;
                best = builtIn.AnimGraph;
                borrowedFrom = path;
            }
        }
        return best;
    }

    /// <summary>Sets up a character renderer: its model, and the graph it is posed with.</summary>
    public static void Setup( SkinnedModelRenderer body, Model model )
    {
        body.Model = model;
        if ( model?.AnimGraph is null && GraphFor( model ) is { } graph )
            body.AnimationGraph = graph;
        body.UseAnimGraph = true;
        body.PlayAnimationsInEditorScene = true;
    }

    /// <summary>Bind-pose skeleton of a model in model space.</summary>
    public static Skeleton SkeletonOf( Model model )
    {
        var defs = new List<BoneDefinition>( model.BoneCount );
        var names = new string[model.BoneCount];
        for ( var i = 0; i < model.BoneCount; i++ )
            names[i] = model.GetBoneName( i );
        for ( var i = 0; i < model.BoneCount; i++ )
        {
            var parent = model.GetBoneParent( i );
            var world = model.GetBoneTransform( i ).ToCore();
            var local = parent >= 0 ? XForm.ToLocal( model.GetBoneTransform( parent ).ToCore(), world ) : world;
            defs.Add( new BoneDefinition( names[i], parent >= 0 ? names[parent] : null, local ) );
        }
        return Skeleton.Create( defs );
    }

    /// <summary>
    /// The animated pose of a character renderer in model space (animation only, before any
    /// bone overrides), mapped onto <paramref name="skeleton"/> by bone name.
    /// </summary>
    public static CharacterPose Sample( SkinnedModelRenderer body, Skeleton skeleton )
    {
        var locals = skeleton.Bones.Select( b => b.RestLocal ).ToArray();
        var world = new XForm[skeleton.Count];
        var has = new bool[skeleton.Count];
        var root = body.WorldTransform;
        foreach ( var bone in body.Model.Bones.AllBones )
        {
            var i = skeleton.IndexOf( bone.Name );
            if ( i < 0 )
                continue;
            if ( body.TryGetBoneTransformAnimation( bone, out var tx ) )
            {
                world[i] = root.ToLocal( tx ).ToCore();
                has[i] = true;
            }
        }
        for ( var i = 0; i < skeleton.Count; i++ )
        {
            if ( !has[i] )
                continue;
            var p = skeleton[i].ParentIndex;
            locals[i] = p < 0 ? world[i] : has[p] ? XForm.ToLocal( world[p], world[i] ) : locals[i];
        }
        return new CharacterPose( skeleton, locals );
    }
}