tests/CitizenAnimationSetup/SmartPortGraphsTests.cs

Unit tests for SmartPortGraphs.Rebase behavior when rebasing animation graph subgraphs and renaming bones. Tests create fake KV3 graph files, supply rename mappings and a file loader, then assert copied subgraph contents, references, unreadable handling, and duplicate-name disambiguation.

File Access
using HumanoidRetargeter.Core.Target;
using Xunit;

namespace HumanoidRetargeter.Tests.Target;

/// <summary>Bone renaming that follows a graph into its subgraphs (the stock Citizen graph keeps its logic there).</summary>
public class SmartPortGraphsTests
{
    static string Graph(string cls, string body) => VmdlWriter.Kv3Header + "{ _class = \"" + cls + "\" " + body + " }";

    static readonly Dictionary<string, string> Renames = new()
    {
        ["pelvis"] = "ValveBiped_Bip01_Pelvis", ["hand_R"] = "ValveBiped_Bip01_R_Hand", ["hold_R"] = "hold_R",
    };

    static Dictionary<string, string> Files(params (string Path, string Text)[] files)
        => files.ToDictionary(f => f.Path, f => f.Text, StringComparer.OrdinalIgnoreCase);

    [Fact]
    public void SubgraphsNamingRenamedBonesAreCopiedAndReferenced()
    {
        var entry = Graph("CAnimationGraph", """
            m_nodes = [
              { m_subGraphFilename = "models/citizen/subgraphs/core.vsubgrph" },
              { m_subGraphFilename = "models\\citizen\\subgraphs\\face.vsubgrph" },
              { m_bone = "hand_R" }
            ]
            """);
        var files = Files(
            ("models/citizen/subgraphs/core.vsubgrph", Graph("CAnimationSubGraph", "m_hipBoneName = \"pelvis\" m_attachmentName = \"hold_R\"")),
            ("models/citizen/subgraphs/face.vsubgrph", Graph("CAnimationSubGraph", "m_sequenceName = \"Eyes_Blink\"")));
        var copy = SmartPortGraphs.Rebase(entry, Renames, p => files.GetValueOrDefault(p), "models/x_src/subgraphs");

        var core = Assert.Single(copy.Subgraphs);
        Assert.Equal("models/x_src/subgraphs/core.vsubgrph", core.Key);
        Assert.Contains("m_hipBoneName = \"ValveBiped_Bip01_Pelvis\"", core.Value);
        Assert.Contains("m_attachmentName = \"hold_R\"", core.Value);
        Assert.Contains("\"models/x_src/subgraphs/core.vsubgrph\"", copy.Graph);
        Assert.Contains("face.vsubgrph", copy.Graph); // unchanged: still the shipped one
        Assert.DoesNotContain("x_src/subgraphs/face", copy.Graph);
        Assert.Contains("\"ValveBiped_Bip01_R_Hand\"", copy.Graph);
        Assert.Empty(copy.Unread);
    }

    [Fact]
    public void AChangeDeepInsideCopiesEveryGraphAboveIt()
    {
        var entry = Graph("CAnimationGraph", "m_subGraphFilename = \"a.vsubgrph\"");
        var files = Files(
            ("a.vsubgrph", Graph("CAnimationSubGraph", "m_subGraphFilename = \"b.vsubgrph\"")),
            ("b.vsubgrph", Graph("CAnimationSubGraph", "m_hipBoneName = \"pelvis\" m_subGraphFilename = \"a.vsubgrph\"")));
        var copy = SmartPortGraphs.Rebase(entry, Renames, p => files.GetValueOrDefault(p), "out");
        Assert.Equal(new[] { "out/a.vsubgrph", "out/b.vsubgrph" }, copy.Subgraphs.Keys.OrderBy(k => k, StringComparer.Ordinal));
        Assert.Contains("\"out/b.vsubgrph\"", copy.Subgraphs["out/a.vsubgrph"]);
        Assert.Contains("\"out/a.vsubgrph\"", copy.Graph);
        Assert.Contains("\"a.vsubgrph\"", copy.Subgraphs["out/b.vsubgrph"]); // a reference back up the chain is not followed again
    }

    [Fact]
    public void UnreadableSubgraphsAreListedAndIdentityNamesChangeNothing()
    {
        var entry = Graph("CAnimationGraph", "m_subGraphFilename = \"missing.vsubgrph\" m_bone = \"hold_R\"");
        var copy = SmartPortGraphs.Rebase(entry, Renames, _ => null, "out");
        Assert.Empty(copy.Subgraphs);
        Assert.Equal(new[] { "missing.vsubgrph" }, copy.Unread);
        Assert.Contains("\"missing.vsubgrph\"", copy.Graph);
    }

    [Fact]
    public void SameNamedSubgraphsFromDifferentFoldersGetDistinctCopies()
    {
        var entry = Graph("CAnimationGraph", "m_nodes = [ { m_subGraphFilename = \"one/core.vsubgrph\" }, { m_subGraphFilename = \"two/core.vsubgrph\" } ]");
        var files = Files(
            ("one/core.vsubgrph", Graph("CAnimationSubGraph", "m_bone = \"pelvis\"")),
            ("two/core.vsubgrph", Graph("CAnimationSubGraph", "m_bone = \"hand_R\"")));
        var copy = SmartPortGraphs.Rebase(entry, Renames, p => files.GetValueOrDefault(p), "out");
        Assert.Equal(new[] { "out/core.vsubgrph", "out/core_2.vsubgrph" }, copy.Subgraphs.Keys.OrderBy(k => k, StringComparer.Ordinal));
    }
}