Editor-side utility that loads, saves and queries user preset profiles for humanoid retargeting. It reads/writes JSON Profile files under project assets, applies loaded profiles to a Skeleton via ProfileDetector, and exposes Exists/TryLoad/Save helpers with a legacy-folder fallback.
#nullable enable annotations
// User preset profiles - the "preset learning" half of the preview flow (design §6).
//
// The Code/ facade can do no file IO, so user presets live entirely Editor-side:
// when the user confirms a preview of a mapping that came from manual edits or the
// blind auto-mapper, the confirmed mapping is saved as a Profile JSON under
// <project assets>/data/humanoid_retargeter/profiles/user/<SkeletonSignature>.json
// (presets saved before 2026-10-06 under <project assets>/humanoid_retargeter/profiles/user
// are still read)
// and on every later file add the window looks the signature up FIRST and passes the
// loaded mapping to the facade as RetargetRequest.MappingOverride with
// MappingSource.UserPreset - so the same rig is recognized instantly and never asks
// again.
using System;
using System.IO;
using Editor;
using HumanoidRetargeter.Core;
using HumanoidRetargeter.Core.Mapping;
using SkeletonModel = HumanoidRetargeter.Core.Skeleton.Skeleton;
namespace HumanoidRetargeter.EditorTools;
/// <summary>
/// Editor-side persistence of user preset profiles, keyed by
/// <see cref="SkeletonSignature"/>. Files use the standard schema-versioned
/// <see cref="Profile"/> JSON, so user presets and shipped presets are the same format.
/// </summary>
public static class UserPresets
{
/// <summary>Assets-relative folder the presets are stored in.</summary>
public const string FolderRelative = "data/humanoid_retargeter/profiles/user";
/// <summary>Assets-relative folder presets were stored in before 2026-10-06; still read, never written.</summary>
public const string LegacyFolderRelative = "humanoid_retargeter/profiles/user";
static string FolderAbsolute( string assetsPath, string folderRelative = FolderRelative )
=> Path.Combine( assetsPath, folderRelative.Replace( '/', Path.DirectorySeparatorChar ) );
static string PresetPath( string assetsPath, string signature, string folderRelative = FolderRelative )
=> Path.Combine( FolderAbsolute( assetsPath, folderRelative ), signature + ".json" );
// The current location wins; a preset saved by an older version is found in the legacy folder.
static string ExistingPresetPath( string assetsPath, string signature )
{
var path = PresetPath( assetsPath, signature );
if ( File.Exists( path ) )
return path;
var legacy = PresetPath( assetsPath, signature, LegacyFolderRelative );
return File.Exists( legacy ) ? legacy : null;
}
/// <summary>
/// Loads the user preset for the given skeleton signature and applies it to the
/// skeleton. Returns null when no preset exists or the stored file is unreadable.
/// The returned mapping carries <see cref="MappingSource.UserPreset"/>.
/// </summary>
public static MappingResult TryLoad( string assetsPath, string signature, SkeletonModel skeleton )
{
try
{
var path = ExistingPresetPath( assetsPath, signature );
if ( path is null )
return null;
var profile = Profile.FromJson( File.ReadAllText( path ) );
var applied = ProfileDetector.Apply( profile, skeleton );
// Re-wrap with the UserPreset source (ProfileDetector.Apply always says Preset).
var result = new MappingResult( profile.Name, MappingSource.UserPreset )
{
Confidence = applied.Confidence,
};
foreach ( var kv in applied.RoleToBone )
result.RoleToBone[kv.Key] = kv.Value;
result.Notes.AddRange( applied.Notes );
return result;
}
catch ( Exception e )
{
Log.Warning( $"[humanoid-retargeter] failed to load user preset for {signature}: {e.Message}" );
return null;
}
}
/// <summary>
/// Saves a confirmed mapping as a user preset profile: one alias per role - the exact
/// source bone name. Returns the absolute file path written.
/// </summary>
/// <param name="namePrefix">Profile-name prefix, default <c>user</c>; the DL flow passes
/// <c>user_dl</c> so derived presets are recognizable in the profile chip.</param>
public static string Save( string assetsPath, string signature, SkeletonModel skeleton,
MappingResult mapping, string namePrefix = "user" )
{
var aliases = new System.Collections.Generic.Dictionary<BoneRole, string[]>();
foreach ( var kv in mapping.RoleToBone )
aliases[kv.Key] = new[] { skeleton[kv.Value].Name };
var shortSig = signature.Length >= 8 ? signature.Substring( 0, 8 ) : signature;
var profile = new Profile( $"{namePrefix}_{shortSig}", Array.Empty<string>(), aliases );
Directory.CreateDirectory( FolderAbsolute( assetsPath ) );
var path = PresetPath( assetsPath, signature );
File.WriteAllText( path, profile.ToJson() );
Log.Info( $"[humanoid-retargeter] saved user preset profile -> {path}" );
return path;
}
/// <summary>Whether a preset exists for the signature.</summary>
public static bool Exists( string assetsPath, string signature )
=> ExistingPresetPath( assetsPath, signature ) is not null;
}