tests/CitizenAnimationSetup/AdditiveReferenceTests.cs
using System.Text;
using HumanoidRetargeter.Target;
using Xunit;

namespace HumanoidRetargeter.Tests.Target;

public class AdditiveReferenceTests
{
    // Small self-contained articulated source: tests require no downloaded rig or engine.
    private static byte[] Source()
    {
        string Joint(string name, string offset, string children = "")
            => $"JOINT mixamorig:{name} {{ OFFSET {offset} CHANNELS 3 Zrotation Xrotation Yrotation {children} }} ";
        var spine = Joint("Spine", "0 20 0", Joint("Neck", "0 30 0", Joint("Head", "0 10 0"))
            + Joint("LeftArm", "20 20 0", Joint("LeftForeArm", "30 0 0", Joint("LeftHand", "20 0 0")))
            + Joint("RightArm", "-20 20 0", Joint("RightForeArm", "-30 0 0", Joint("RightHand", "-20 0 0"))));
        var legs = Joint("LeftUpLeg", "10 -10 0", Joint("LeftLeg", "0 -40 0", Joint("LeftFoot", "0 -40 0")))
            + Joint("RightUpLeg", "-10 -10 0", Joint("RightLeg", "0 -40 0", Joint("RightFoot", "0 -40 0")));
        var frames = string.Join("\n", Enumerable.Range(0, 6).Select(i => $"0 100 {i} " + string.Join(" ", Enumerable.Repeat("0", 48))));
        return Encoding.UTF8.GetBytes($"HIERARCHY ROOT mixamorig:Hips {{ OFFSET 0 100 0 CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation {spine} {legs} }} MOTION\nFrames: 6\nFrame Time: 0.0333333333\n{frames}\n");
    }

    [Theory]
    [InlineData(0)]
    [InlineData(3)]
    [InlineData(5)]
    public void ChosenReferenceFrameIsSerializedWithoutDeltaEvents(int reference)
    {
        var result = Convert(reference);
        var clip = Assert.Single(result.Clips);
        Assert.True(clip.Success, clip.Error);
        var root = (KvObject)((KvObject)Kv3.Parse(result.StandaloneVmdl).Root)["rootNode"];
        var list = ((KvArray)root["children"]).Items.OfType<KvObject>().Single(n => n.GetString("_class") == "AnimationList");
        var delta = ((KvArray)list["children"]).Items.OfType<KvObject>().Single(n => n.GetString("name") == clip.AdditiveVariantName);
        var subtract = Assert.IsType<KvObject>(Assert.Single(((KvArray)delta["children"]).Items));
        Assert.Equal("AnimSubtract", subtract.GetString("_class"));
        Assert.Equal(reference, ((KvLong)subtract["frame"]).Value);
        Assert.Equal(clip.ClipName, subtract.GetString("anim_name"));
    }

    [Theory]
    [InlineData(-1)]
    [InlineData(6)]
    public void InvalidReferenceFailsClearlyInsteadOfSilentlyClamping(int reference)
    {
        var clip = Assert.Single(Convert(reference).Clips);
        Assert.False(clip.Success);
        Assert.Contains("Additive reference frame", clip.Error);
    }

    private static RetargetResult Convert(int reference)
    {
        var data = Source();
        var scene = Retargeter.ImportSource(data, "test.bvh");
        var (map, _) = Retargeter.ResolveMapping(scene.Skeleton);
        return Retargeter.Convert(new RetargetRequest
        {
            SourceData = data, SourceFileName = "test.bvh", MappingOverride = map,
            CreateAdditiveVariant = true, AdditiveReferenceFrame = reference, GenerateFootstepEvents = true,
        }, new RetargetTargetSpec { Rig = TargetRig.FromSkeleton(scene.Skeleton, map), VmdlScale = 1 });
    }
}