Code/HumanoidRetargeter/Target/ModelGrounding.cs
#nullable enable annotations
using System;
using System.Linq;
using HumanoidRetargeter.Maths;
using NVector3 = System.Numerics.Vector3;

namespace HumanoidRetargeter.Target;

/// <summary>A final model-space translation, shared by the mesh, bind pose and every animation.</summary>
public static class ModelGrounding
{
    public const string ModifierName = "HumanoidRetargeter_Ground";

    public static string Apply(string vmdl, float minimumZ)
    {
        if (!float.IsFinite(minimumZ)) throw new ArgumentOutOfRangeException(nameof(minimumZ));
        // Do not move an already grounded model, or lower intentionally elevated geometry.
        if (minimumZ >= -0.05f) return vmdl;
        var doc = Kv3.Parse(vmdl);
        var children = (KvArray)((KvObject)((KvObject)doc.Root)["rootNode"])["children"];
        var list = children.Items.OfType<KvObject>().SingleOrDefault(n => n.GetString("_class") == "ModelModifierList");
        if (list is null)
        {
            list = new KvObject { ["_class"] = new KvString("ModelModifierList"), ["children"] = new KvArray() };
            children.Items.Add(list);
        }
        var modifiers = (KvArray)list["children"];
        var previous = modifiers.Items.OfType<KvObject>().SingleOrDefault(IsGrounding);
        var height = previous is null ? 0 : Height(previous);
        if (previous is not null) modifiers.Items.Remove(previous);
        var translation = new KvArray();
        translation.Items.Add(new KvDouble(0)); translation.Items.Add(new KvDouble(0));
        translation.Items.Add(new KvDouble(height - minimumZ));
        // After ScaleAndMirror: this offset is in final engine units, not source centimeters.
        modifiers.Items.Add(new KvObject { ["_class"] = new KvString("ModelModifier_Translate"),
            ["name"] = new KvString(ModifierName), ["translation"] = translation });
        return Kv3.Serialize(doc);
    }

    public static bool IsGrounding(KvObject node) => node.GetString("_class") == "ModelModifier_Translate"
        && node.GetString("name") == ModifierName;

    public static float Offset(string vmdl)
    {
        var children = (KvArray)((KvObject)((KvObject)Kv3.Parse(vmdl).Root)["rootNode"])["children"];
        var list = children.Items.OfType<KvObject>().SingleOrDefault(n => n.GetString("_class") == "ModelModifierList");
        var modifier = (list?.GetOrNull("children") as KvArray)?.Items.OfType<KvObject>().SingleOrDefault(IsGrounding);
        return modifier is null ? 0 : Height(modifier);
    }

    public static XForm SourceLocal(XForm compiled, bool isRoot, float offset)
        => isRoot ? new XForm(compiled.Pos - NVector3.UnitZ * offset, compiled.Rot) : compiled;

    private static float Height(KvObject node)
    {
        var vector = (KvArray)node["translation"];
        static double Number(KvValue v) => v is KvDouble d ? d.Value : ((KvLong)v).Value;
        if (vector.Items.Count != 3 || Number(vector.Items[0]) != 0 || Number(vector.Items[1]) != 0
            || !double.IsFinite(Number(vector.Items[2])))
            throw new InvalidOperationException("Invalid grounding translation.");
        var height = (float)Number(vector.Items[2]);
        if (!float.IsFinite(height)) throw new InvalidOperationException("Invalid grounding translation.");
        return height;
    }
}