tests/SmartPort/Engine/SmartPortGripEngineTest.cs
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
using Editor;
using Sandbox;

// Read-only playback of a fresh Smart Port output in an explicitly opted-in scratch project.
public static class SmartPortGripEngineTest
{
    static bool started;
    [EditorEvent.Frame]
    public static void Tick()
    {
        var root = Environment.GetEnvironmentVariable("HR_SMART_PORT_GRIP_TEST_PROJECT");
        if (started || Project.Current is null || string.IsNullOrEmpty(root)
            || !string.Equals(Path.GetFullPath(Project.Current.GetRootPath()).TrimEnd('/', '\\'),
                Path.GetFullPath(root).TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase)
            || !AssetSystem.All.Any()) return;
        started = true;
        _ = Run(root);
    }

    static async Task Run(string root)
    {
        var data = new Dictionary<string, object>();
        void Save() => File.WriteAllText(Path.Combine(root, "grip-test-result.json"),
            JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
        try
        {
            data["started"] = DateTime.UtcNow; Save();
            var target = Environment.GetEnvironmentVariable("HR_SMART_PORT_GRIP_MODEL");
            if (string.IsNullOrWhiteSpace(target)) throw new Exception("Set HR_SMART_PORT_GRIP_MODEL to a freshly generated port's asset path");
            data["model"] = target;
            var session = SceneEditorSession.CreateDefault(); session.MakeActive();
            using (session.Scene.Push())
                foreach (var path in new[] { "models/player/human/frank_mp.vmdl", target })
                {
                    var go = session.Scene.CreateObject(); go.Name = path;
                    var actor = go.Components.Create<SkinnedModelRenderer>();
                    actor.Model = Model.Load(path); actor.UseAnimGraph = true;
                    if (actor.Model.IsError) throw new Exception("Missing model " + path);
                }
            EditorScene.Play(false, session); await Task.Delay(1500);
            if (!Game.IsPlaying || Game.ActiveScene is null || Game.ActiveScene.IsEditor) throw new Exception("Did not enter play mode");
            var actors = Game.ActiveScene.GetAllComponents<SkinnedModelRenderer>().ToArray();
            var hands = actors.ToDictionary(a => a, a => a.Model.Bones.AllBones
                .Where(b => string.Equals(b.Name, "hand_L", StringComparison.OrdinalIgnoreCase)
                    || string.Equals(b.Name, "hand_R", StringComparison.OrdinalIgnoreCase))
                .OrderBy(b => b.Name, StringComparer.OrdinalIgnoreCase).Select(b => b.Name).ToArray());
            if (hands.Values.Any(names => names.Length != 2)) throw new Exception("Missing hand bones");
            var observations = new List<object>(); data["observations"] = observations;
            var reference = new Dictionary<SkinnedModelRenderer, Vector3[]>();
            foreach (var (style, x, y) in new[] { (0, 0f, 0f), (4, 120f, 0f), (5, 240f, 0f),
                (4, 0f, 120f), (4, 0f, -120f), (4, -120f, 0f), (4, 85f, 85f),
                (4, 85f, -85f), (4, -85f, 85f), (4, -85f, -85f) })
            {
                var samples = actors.ToDictionary(a => a, a => new List<Vector3[]>());
                var feet = actors.ToDictionary(a => a, a => new List<Vector3>());
                for (var frame = 0; frame < 180; frame++)
                {
                    foreach (var actor in actors)
                    {
                        actor.Set("b_grounded", true); actor.Set("holdtype", 6); actor.Set("weapon_pose", 0); actor.Set("move_style", style);
                        actor.Set("aim_body", Vector3.Forward); actor.Set("aim_body_weight", 1f);
                        actor.Set("aim_head", Vector3.Forward); actor.Set("aim_head_weight", 1f);
                        var speed = MathF.Sqrt(x * x + y * y);
                        actor.Set("move_x", x); actor.Set("move_y", y); actor.Set("move_speed", speed); actor.Set("move_groundspeed", speed);
                        actor.Set("wish_x", x); actor.Set("wish_y", y); actor.Set("wish_groundspeed", speed);
                    }
                    await Task.Delay(20);
                    if (frame < 80) continue;
                    foreach (var actor in actors)
                    {
                        var grip = actor.SceneModel.GetAttachment("hold_R", true) ?? throw new Exception("Missing weapon attachment");
                        samples[actor].Add(hands[actor].Select(b => grip.Rotation.Inverse *
                            (actor.SceneModel.GetBoneWorldTransform(b).Position - grip.Position)).ToArray());
                        var ankle = actor.Model.Bones.AllBones.First(b => string.Equals(b.Name, "ankle_L", StringComparison.OrdinalIgnoreCase));
                        feet[actor].Add(actor.SceneModel.GetBoneWorldTransform(ankle.Name).Position - actor.WorldPosition);
                    }
                }
                foreach (var actor in actors)
                {
                    if (style == 0) reference[actor] = Enumerable.Range(0, 2).Select(side =>
                        samples[actor].Aggregate(Vector3.Zero, (sum, v) => sum + v[side]) / samples[actor].Count).ToArray();
                    var footMotion = feet[actor].Max(v => v.Distance(feet[actor][0]));
                    if (style != 0 && footMotion < 1) throw new Exception("Locomotion did not animate: " + actor.Model.Name);
                    foreach (var side in new[] { 0, 1 })
                    {
                        var drift = samples[actor].Max(v => v[side].Distance(reference[actor][side]));
                        observations.Add(new { model = actor.Model.Name, style, x, y, side, drift, footMotion }); Save();
                        if (!float.IsFinite(drift) || drift > .15f)
                            throw new Exception($"Hand slipped relative to weapon: {actor.Model.Name}, side {side}, style {style}, velocity {x},{y}, drift {drift}");
                    }
                }
            }
            data["passed"] = true;
        }
        catch (Exception e) { data["error"] = e.ToString(); data["passed"] = false; }
        finally
        {
            data["completed"] = true; Save();
            if (Game.IsPlaying) EditorScene.Stop();
            await Task.Delay(1000); EditorUtility.Quit(true);
        }
    }
}