Editor/HumanoidRetargeter/UserPresets.cs

Editor utility for storing and loading user-created humanoid retargeter profiles. It reads/writes JSON Profile files under assets/humanoid_retargeter/profiles/user keyed by a skeleton signature, applies loaded profiles to a Skeleton, and creates profile files from confirmed MappingResult.

File Access
// 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>/humanoid_retargeter/profiles/user/<SkeletonSignature>.json
// 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.Mapping;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Editor;

/// <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 = "humanoid_retargeter/profiles/user";

	static string FolderAbsolute( string assetsPath )
		=> Path.Combine( assetsPath, FolderRelative.Replace( '/', Path.DirectorySeparatorChar ) );

	static string PresetPath( string assetsPath, string signature )
		=> Path.Combine( FolderAbsolute( assetsPath ), signature + ".json" );

	/// <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 = PresetPath( assetsPath, signature );
			if ( !File.Exists( path ) )
				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 )
		=> File.Exists( PresetPath( assetsPath, signature ) );
}