tests/SmartPort/Engine/SmartPortExtendEngineTest.cs
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using Editor;
using Sandbox;
// Copy into the Editor folder of an isolated test project with this library installed.
// Never runs unless the exact project root is explicitly opted in.
public static class SmartPortExtendEngineTest
{
static bool started;
static string output;
static readonly Dictionary<string, object> results = new();
[EditorEvent.Frame]
public static void Tick()
{
var expected = Environment.GetEnvironmentVariable("HR_SMART_PORT_EXTEND_TEST_PROJECT");
if (started || Project.Current is null || string.IsNullOrEmpty(expected)
|| !string.Equals(Path.GetFullPath(Project.Current.GetRootPath()).TrimEnd('/', '\\'),
Path.GetFullPath(expected).TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase)
|| !AssetSystem.All.Any()) return;
started = true;
output = Path.Combine(expected, "smart-port-extend-result.json");
_ = Run();
}
static void Save() => File.WriteAllText(output, JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }));
static async Task Run()
{
try
{
results["started"] = DateTime.UtcNow; Save();
var source = AssetSystem.FindByPath("models/player/human/frank_mp.vmdl") ?? throw new Exception("Missing compiled fixture");
var target = AssetSystem.FindByPath("animations/retargeted/retargeted_bunny.vmdl") ?? throw new Exception("Missing target fixture");
if (Model.Load(target.Path).IsError) throw new Exception("Target failed to load");
var type = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetType("HumanoidRetargeter.Editor.SmartPortModels")).FirstOrDefault(t => t != null)
?? throw new Exception("SmartPortModels was not compiled into the editor library");
var method = type.GetMethod("CreateExtendedAsync", BindingFlags.Static | BindingFlags.NonPublic);
Action<string> progress = message => { results["stage"] = message; Save(); };
var name = "extend_" + DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
var task = (Task)method.Invoke(null, new object[] { source, target, "smart_port", name, progress, CancellationToken.None });
await task;
var write = task.GetType().GetProperty("Result").GetValue(task);
var compiled = (bool)write.GetType().GetProperty("Compiled").GetValue(write);
results["compiled"] = compiled;
results["errors"] = write.GetType().GetProperty("Errors").GetValue(write); Save();
if (!compiled) throw new Exception("Smart Port verification failed");
var path = "smart_port/" + name + ".vmdl";
var model = Model.Load(path);
results["sequences"] = model.AnimationCount;
results["graph"] = model.AnimGraph.Name; Save();
var skin = Model.Load(target.Path);
var skinBones = skin.Bones.AllBones.Where(b => b.Parent != null &&
(b.Name.StartsWith("finger_") || b.Name.StartsWith("arm_lower_") || b.Name.StartsWith("hand_")
|| b.Name.StartsWith("leg_lower_") || b.Name.StartsWith("ankle_")) && !b.Name.Contains("twist")
&& !b.Name.Contains("IK") && !b.Name.Contains("ikrule")).ToArray();
// Actual editor play mode: component animation updates are driven by the game,
// not SceneModel.Update calls or the retargeter's preview widget.
var session = SceneEditorSession.CreateDefault();
session.MakeActive();
using (session.Scene.Push())
{
var go = session.Scene.CreateObject(); go.Name = "Smart Port Character";
var renderer = go.Components.Create<SkinnedModelRenderer>();
renderer.Model = model; renderer.UseAnimGraph = true;
var original = session.Scene.CreateObject(); original.Name = "Original";
var originalRenderer = original.Components.Create<SkinnedModelRenderer>();
originalRenderer.Model = skin; originalRenderer.UseAnimGraph = true;
var cameraObject = session.Scene.CreateObject(); cameraObject.Name = "Capture Camera";
cameraObject.Components.Create<CameraComponent>().FieldOfView = 38;
foreach (var yaw in new[] { 25f, 145f, 265f })
{
var light = session.Scene.CreateObject(); light.WorldRotation = Rotation.From(35, yaw, 0);
light.Components.Create<DirectionalLight>().LightColor = Color.White * .8f;
}
var floor = session.Scene.CreateObject(); floor.Name = "Floor";
var box = floor.Components.Create<BoxCollider>();
box.Scale = new Vector3(4000, 4000, 4); box.Center = new Vector3(0, 0, -2);
}
EditorScene.Play(false, session);
await Task.Delay(1500);
if (!Game.IsPlaying || Game.ActiveScene is null || Game.ActiveScene.IsEditor) throw new Exception("Did not enter game play mode");
results["inGame"] = true; Save();
var actor = Game.ActiveScene.GetAllComponents<SkinnedModelRenderer>().Single(r => r.GameObject.Name == "Smart Port Character");
var baseline = Game.ActiveScene.GetAllComponents<SkinnedModelRenderer>().Single(r => r.GameObject.Name == "Original");
var footsteps = 0;
actor.OnFootstepEvent += _ => footsteps++;
var movement = new List<object>();
foreach (var (style, speed) in new[] { (0, 100f), (1, 120f), (2, 240f) })
{
foreach (var driven in new[] { actor, baseline })
{
driven.Set("b_grounded", true); driven.Set("move_style", style); driven.Set("weapon_pose", 0);
driven.Set("move_x", speed); driven.Set("move_speed", speed); driven.Set("move_groundspeed", speed);
driven.Set("wish_x", speed); driven.Set("wish_groundspeed", speed);
}
await Task.Delay(1000);
var boneNames = model.Bones.AllBones.Where(b => b.Name.Contains("ankle") || b.Name.Contains("leg_lower") || b.Name.Contains("hand")).Select(b => b.Name).ToArray();
if (boneNames.Length < 4) throw new Exception("Insufficient limb bones for locomotion assertions");
var initial = boneNames.Select(n => actor.SceneModel.GetBoneWorldTransform(n).Position - actor.WorldPosition).ToArray();
float maxMotion = 0;
float maxStretch = 0;
for (var frame = 0; frame < 100; frame++)
{
await Task.Delay(20);
actor.WorldPosition += Vector3.Forward * speed * .02f;
baseline.WorldPosition = actor.WorldPosition;
foreach (var bone in skinBones)
{
var bindLength = bone.LocalTransform.Position.Distance(bone.Parent.LocalTransform.Position);
var length = actor.SceneModel.GetBoneWorldTransform(bone.Name).Position.Distance(
actor.SceneModel.GetBoneWorldTransform(bone.Parent.Name).Position);
// Preserve procedural graph scaling too, rather than forcing fixed bind lengths.
var originalLength = baseline.SceneModel.GetBoneWorldTransform(bone.Name).Position.Distance(
baseline.SceneModel.GetBoneWorldTransform(bone.Parent.Name).Position);
maxStretch = MathF.Max(maxStretch, MathF.Abs(length - originalLength));
if (!float.IsFinite(length) || MathF.Abs(length - originalLength) > .15f)
throw new Exception($"Changed skin bone {bone.Name}: bind={bindLength}, original={originalLength}, animated={length}");
}
for (var b = 0; b < boneNames.Length; b++)
{
var pos = actor.SceneModel.GetBoneWorldTransform(boneNames[b]).Position - actor.WorldPosition;
var distance = pos.Distance(initial[b]);
if (!float.IsFinite(distance) || pos.Length > 200) throw new Exception($"Non-finite or stretched limb {boneNames[b]}: {pos} (initial {initial[b]}, style {style})");
maxMotion = MathF.Max(maxMotion, distance);
}
}
movement.Add(new { style, speed, maxMotion, maxStretch });
if (maxMotion < 1) throw new Exception("Limbs did not animate during locomotion");
}
results["locomotion"] = movement;
results["footsteps"] = footsteps;
if (footsteps == 0) throw new Exception("No footstep events during walking/running");
// The graph must actually switch into the imported branch and back.
actor.Set("move_x", 0f); actor.Set("move_speed", 0f); actor.Set("move_groundspeed", 0f);
actor.Set("wish_x", 0f); actor.Set("wish_groundspeed", 0f); actor.Set("move_style", 0);
await Task.Delay(700);
var originalHand = actor.SceneModel.GetBoneWorldTransform("hand_L").Position - actor.WorldPosition;
baseline.SceneModel.RenderingEnabled = false;
void Capture(string label)
{
var camera = Game.ActiveScene.GetAllComponents<CameraComponent>().Single(c => c.GameObject.Name == "Capture Camera");
camera.WorldPosition = actor.WorldPosition + new Vector3(130, 65, 65);
camera.WorldRotation = Rotation.LookAt(actor.WorldPosition + new Vector3(0, 0, 37) - camera.WorldPosition);
var pixmap = new Pixmap(768, 768);
if (!camera.RenderToPixmap(pixmap)) throw new Exception("Capture failed: " + label);
File.WriteAllBytes(Path.Combine(Path.GetDirectoryName(output), "extend-" + label + ".png"), pixmap.GetPng());
}
Capture("original");
var guide = File.ReadAllLines(Path.Combine(Project.Current.GetAssetsPath(), "smart_port", name + "_smart_port", "clips.txt"));
int ClipId(string clip) => int.Parse(guide.Single(l => l.Contains(": " + clip + " (")).Split(':')[0]);
actor.Set("hr_smartport_clip", ClipId("hr_port_bindPose"));
await Task.Delay(700);
var importedHand = actor.SceneModel.GetBoneWorldTransform("hand_L").Position - actor.WorldPosition;
results["importedBranchMotion"] = importedHand.Distance(originalHand);
if (importedHand.Distance(originalHand) < 4) throw new Exception("Imported clip selector did not change the pose");
Capture("imported");
actor.Set("hr_smartport_clip", 0);
await Task.Delay(700);
var resumedHand = actor.SceneModel.GetBoneWorldTransform("hand_L").Position - actor.WorldPosition;
results["resumedHandError"] = resumedHand.Distance(originalHand);
if (resumedHand.Distance(originalHand) > 3) throw new Exception("Original graph did not resume");
Capture("resumed");
actor.Set("hr_smartport_clip", ClipId("hr_port_bindPose_delta"));
await Task.Delay(700);
var additiveHand = actor.SceneModel.GetBoneWorldTransform("hand_L").Position - actor.WorldPosition;
results["zeroAdditiveError"] = additiveHand.Distance(resumedHand);
if (additiveHand.Distance(resumedHand) > 3) throw new Exception("Zero additive replaced or distorted the original pose");
Capture("additive");
results["passed"] = true;
}
catch (Exception e) { results["passed"] = false; results["error"] = e.ToString(); }
finally
{
results["completed"] = true; Save();
if (Game.IsPlaying) EditorScene.Stop();
await Task.Delay(1000);
EditorUtility.Quit(true);
}
}
}