943 results

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Sandbox;

namespace SFXR;


[Title( "SFXR Component" )]
[Category( "SFXR" )]
[Icon( "volume_up" )]
public sealed class SFXRComponent : Component
{

	/// <summary>
	/// The base Waveform
	/// (Default: Square)
	/// </summary>
	[Property, Group( "Sound" )]
	public Waveform Waveform { get; set; } = Waveform.Square;

	/// <summary>
	/// The sample rate of the sound
	/// </summary>
	[Property, Group( "Sound" )]
	public SampleRate SampleRate { get; set; } = SampleRate.Hz44100;

	/// <summary>
	/// The bit depth of the sound
	/// </summary>
	// [Property, Group( "Sound" )]
	public BitDepth BitDepth { get; set; } = BitDepth.Bit16;

	/// <summary>
	/// The length of the sound in seconds
	/// </summary>
	[Property, Group( "Sound" ), Range( 0f, 20f, 0.01f )]
	public float Length { get; set; } = 0.5f;

	/// <summary>
	/// The volume of the sound
	/// (Default: 0.5)
	/// </summary>
	[Property, Group( "Sound" ), Range( 0f, 1f, 0.01f )]
	public float MasterVolume { get; set; } = 0.5f;

	[Property, Group( "Frequency" ), Range( 0, 3000f, 1f )]
	float StartFrequency
	{
		get => Frequency.Start;
		set => Frequency.Start = value;
	}

	[Property, Group( "Frequency" ), Range( -3000f, 3000f, 1f )]
	float Slide
	{
		get => Frequency.Slide;
		set => Frequency.Slide = value;
	}

	[Property, Group( "Frequency" ), Range( -3000f, 3000f, 1f )]
	float SlideDelta
	{
		get => Frequency.DeltaSlide;
		set => Frequency.DeltaSlide = value;
	}

	/// <summary>
	/// The random seed
	/// </summary>
	[Property, Group( "Controls" )]
	public long Seed { get; set; } = 0;

	[Property, Group( "Controls" )]
	public SFXRControls Controls { get; set; } = new SFXRControls();

	public SFXRFrequency Frequency { get; set; } = new SFXRFrequency();
	Random _random = new Random();

	List<SFXRNote> NotesPlaying = new();

	/// <summary>
	/// Plays the sound defined by the component
	/// </summary>
	/// <returns>The sound handle of the sound. This can be used to change position, pitch, ect</returns>
	public SoundHandle PlaySound()
	{
		var sfx = Generate( (int)(Length * (int)SampleRate) );
		var handle = sfx.Play();
		// DestroyStream(sfx, Length);
		return handle;
	}

	/// <summary>
	/// Plays the sound defined by the component (Via a frequency trigger. This will play indefinitely until released)
	/// </summary>
	/// <param name="frequency">The frequency of the sound</param>
	/// <param name="volume">The volume of the trigger </param>
	public void TriggerNotePress( float frequency, float volume = 1f )
	{
		foreach ( var note in NotesPlaying.Where( x => x.Frequency == frequency ) )
		{
			note.Release();
		}

		var newNote = new SFXRNote( this, frequency, volume );
		newNote.Trigger();
		NotesPlaying.Add( newNote );
	}

	/// <summary>
	/// Releases a note playing at the given frequency
	/// </summary>
	/// <param name="frequency">The frequency of the sound</param>
	public void TriggerNoteRelease( float frequency )
	{
		foreach ( var note in NotesPlaying.Where( x => x.Frequency == frequency ) )
		{
			note.Release();
		}
	}

	/// <summary>
	/// Releases all notes playing
	/// </summary>
	public void TriggerReleaseAll()
	{
		foreach ( var note in NotesPlaying )
		{
			note.Release();
		}
	}

	/// <summary>
	/// Generates a sound stream from the component
	/// </summary>
	/// <param name="sampleCount">How many samples the stream should be filled with</param>
	/// <returns></returns>
	public SoundStream Generate( int sampleCount )
	{
		List<SFXREffect> effects = new();

		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect || !effect.Enabled ) continue;
			effects.Add( effect );
		}

		return Generate( sampleCount, effects );
	}

	/// <summary>
	/// Generates a sound stream from the component with the given effects
	/// </summary>
	/// <param name="sampleCount">The number of samples</param>
	/// <param name="effects">A list of the effects to apply</param>
	/// <returns></returns>
	public SoundStream Generate( int sampleCount, List<SFXREffect> effects )
	{
		short[] samples = new short[sampleCount];

		float t = 0;
		for ( int i = 0; i < sampleCount; i++ )
		{
			t += 1f / (int)SampleRate;
			short sampleValue = SFXR.GetWaveformSample( Waveform, t, Frequency.GetFrequency( t ) );

			sampleValue = (short)((float)sampleValue * MasterVolume);

			samples[i] = sampleValue;
		}

		foreach ( var effect in effects )
		{
			if ( !effect.Enabled ) continue;
			samples = effect.Apply( samples, this );
		}

		var stream = new SoundStream( (int)SampleRate );
		stream.WriteData( samples );

		return stream;
	}

	/// <summary>
	/// Randomizes the component's parameters
	/// </summary>
	public void Randomize()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );

		var waveform = Waveform;
		ResetParameters();
		Waveform = waveform;

		Frequency.Start = _random.Next( 10, 3000 );
		if ( _random.Next( 2 ) == 0 ) Frequency.Slide = _random.Next( -3000, 3000 );
		if ( Frequency.Start > 2000 && Frequency.Slide > 200 ) Frequency.Slide = -Frequency.Slide;
		else if ( Frequency.Start < 400 && Frequency.Slide < -50 ) Frequency.Slide = -Frequency.Slide;
		if ( _random.Next( 2 ) == 0 ) Frequency.DeltaSlide = _random.Next( -3000, 3000 );

		SanitizeParameters();
	}

	/// <summary>
	/// Mutates the component's parameters slightly
	/// </summary>
	public void Mutate( float mutation = 0.05f )
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );

		Frequency.Start += _random.Float( -mutation, mutation ) * 1000;
		if ( Frequency.Start > 2000 && Frequency.Slide > 200 ) Frequency.Slide = -Frequency.Slide;
		else if ( Frequency.Start < 400 && Frequency.Slide < -50 ) Frequency.Slide = -Frequency.Slide;
		Frequency.Slide += _random.Float( -mutation, mutation ) * 1000;
		Frequency.DeltaSlide += _random.Float( -mutation, mutation ) * 1000;
		if ( Frequency.Slide < -3000 ) Frequency.Slide = -3000;
		if ( Frequency.Slide > 3000 ) Frequency.Slide = 3000;

		SanitizeParameters();
	}


	public void RandomizePickup()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();

		Waveform = (Waveform)_random.Int( 0, 2 );

		Frequency.Start = _random.Float( 0.4f, 0.9f ) * 3000;

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Decay = _random.Float( 0.1f, 0.3f );
		envelope.Sustain = _random.Float( 0f, 0.1f );
		envelope.Release = _random.Float( 0.1f, 0.3f );

		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;

	}

	public void RandomizeLaser()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();
		var highpass = Components.GetOrCreate<SFXRHighPass>();

		Waveform = (Waveform)_random.Int( 0, 2 );
		if ( Waveform == Waveform.Sine && _random.Next( 2 ) == 0 ) Waveform = (Waveform)_random.Int( 0, 1 );

		Frequency.Start = _random.Float( 0.6f, 0.75f ) * 3000;
		Frequency.Slide = _random.Float( -0.25f, -0.15f ) * 3000;
		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Decay = _random.Float( 0f, 0.4f );
		envelope.Sustain = _random.Float( 0.1f, 0.3f );
		envelope.Release = _random.Float( 0.25f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;

		if ( _random.Next( 2 ) == 0 )
		{
			highpass.Enabled = true;
			highpass.Cutoff = _random.Float( 0f, 0.3f );
		}
	}

	public void RandomizeExplosion()
	{

		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();
		var vibrato = Components.GetOrCreate<SFXRVibrato>();

		Waveform = Waveform.Noise;

		if ( _random.Next( 2 ) == 0 )
		{
			Frequency.Start = _random.Float( 0.025f, 0.15f ) * 3000;
			Frequency.Slide = _random.Float( -0.1f, -0.01f ) * 3000;
		}
		else
		{
			Frequency.Start = _random.Float( 0.1f, 0.2f ) * 3000;
			Frequency.Slide = _random.Float( -0.6f, 0.6f ) * 3000;
		}

		if ( _random.Next( 4 ) == 0 ) Frequency.Slide = 0;

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Sustain = _random.Float( 0.1f, 0.4f );
		envelope.Release = _random.Float( 0.1f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;


		if ( _random.Next( 2 ) == 0 )
		{
			vibrato.Enabled = true;
			vibrato.Depth = _random.Float( 0f, 0.7f );
			vibrato.Speed = _random.Float( 0f, 60f );
		}
		else
		{
			vibrato.Enabled = false;
		}

		if ( -Frequency.Slide > Frequency.Start )
		{
			Frequency.Slide = -Frequency.Start;
		}
	}

	public void RandomizePowerup()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();
		var vibrato = Components.GetOrCreate<SFXRVibrato>();

		if ( _random.Next( 2 ) == 0 )
		{
			Waveform = Waveform.Sawtooth;
		}

		if ( _random.Next( 2 ) == 0 )
		{
			Frequency.Start = _random.Float( 0.2f, 0.5f ) * 3000;
			Frequency.Slide = _random.Float( 0.1f, 0.5f ) * 3000;
		}
		else
		{
			Frequency.Start = _random.Float( 0.25f, 0.5f ) * 3000;
			Frequency.Slide = _random.Float( 0.05f, 0.25f ) * 3000;
			if ( _random.Next( 2 ) == 0 )
			{
				vibrato.Enabled = true;
				vibrato.Depth = _random.Float( 0, 0.7f );
				vibrato.Speed = _random.Float( 0, 60f );
			}
			else
			{
				vibrato.Enabled = false;
			}
		}

		if ( -Frequency.Slide > Frequency.Start )
		{
			Frequency.Slide = -Frequency.Start;
		}

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Sustain = _random.Float( 0f, 0.4f );
		envelope.Release = _random.Float( 0.1f, 0.5f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;
	}

	public void RandomizeHit()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();
		var highpass = Components.GetOrCreate<SFXRHighPass>();

		Waveform = (Waveform)_random.Int( 0, 3 );
		if ( Waveform == Waveform.Sine )
		{
			Waveform = Waveform.Noise;
		}

		Frequency.Start = _random.Float( 0.1f, 0.5f ) * 3000;
		Frequency.Slide = _random.Float( -0.7f, -0.3f ) * 3000;

		if ( -Frequency.Slide > Frequency.Start )
		{
			Frequency.Slide = -Frequency.Start;
		}

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Decay = 0;
		envelope.Sustain = _random.Float( 0.025f, 0.1f );
		envelope.Release = _random.Float( 0.1f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;

		if ( _random.Next( 2 ) == 0 )
		{
			highpass.Enabled = true;
			highpass.Cutoff = _random.Float( 0f, 0.3f );
		}
		else
		{
			highpass.Enabled = false;
		}
	}

	public void RandomizeJump()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();

		Waveform = Waveform.Square;

		Frequency.Start = _random.Float( 0.3f, 0.6f ) * 3000;
		Frequency.Slide = _random.Float( 0.1f, 0.3f ) * 3000;

		if ( -Frequency.Slide > Frequency.Start )
		{
			Frequency.Slide = -Frequency.Start;
		}

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Sustain = _random.Float( 0.1f, 0.4f );
		envelope.Release = _random.Float( 0.1f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;
	}

	public void RandomizeBlip()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();

		Waveform = Waveform.Square;

		Frequency.Start = _random.Float( 0.2f, 0.6f ) * 3000;

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Decay = _random.Float( 0.1f, 0.2f );
		envelope.Sustain = _random.Float( 0.025f, 0.1f );
		envelope.Release = _random.Float( 0.1f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;
	}


	public void ResetParameters()
	{
		Waveform = Waveform.Square;
		SampleRate = SampleRate.Hz44100;
		BitDepth = BitDepth.Bit16;
		Length = 0.5f;
		MasterVolume = 0.5f;
		Frequency = new SFXRFrequency();
		Controls = new SFXRControls();
	}

	void SanitizeParameters()
	{

	}

	protected override void OnUpdate()
	{
		foreach ( var note in NotesPlaying )
		{
			note.Update();

			// if (!note.IsPlaying)
			// {
			// 	note.DestroyStreams();
			// }
		}

		NotesPlaying.RemoveAll( x => !x.IsPlaying );
	}
}
using System;
using System.Collections.Generic;
using Sandbox;

namespace SFXR;

[Title( "ADSR Envelope" )]
[Category( "SFXR Effects" )]
[Icon( "mail_outline" )]
public class SFXREnvelope : SFXREffect
{
    /// <summary>
    /// Time the sound takes to reach its peak amplitude
    /// (Default: 0)
    /// </summary>
    [Property, Range( 0, 10 )]
    public float Attack { get; set; } = 0;

    /// <summary>
    /// The time taken for the sound to fade to the sustain level
    /// </summary>
    [Property, Range( 0, 10 )]
    public float Decay { get; set; } = 0;

    /// <summary>
    /// The level maintained until release is triggered
    /// (Default: 1)
    /// </summary>
    [Property, Range( 0, 1 )]
    public float Sustain { get; set; } = 1f;

    /// <summary>
    /// The time taken for the sound to fade to zero after the sustain
    /// (Default: 0.3)
    /// </summary>
    [Property, Range( 0, 10 )]
    public float SustainTime { get; set; } = 0.3f;

    /// <summary>
    /// The time taken for the sound to fade to zero after the release
    /// (Default: 0.4)
    /// </summary>
    [Property, Range( 0, 10 )]
    public float Release { get; set; } = 0.4f;

    /// <summary>
    /// Returns the amplitude of the envelope at a given time
    /// </summary>
    /// <param name="time">Time in seconds</param>
    /// <returns>Amplitude of the envelope at the given time</returns>
    public float GetAmplitude( float time )
    {
        return GetCurve().Evaluate( time / GetLength() );
    }

    public override short[] Apply( short[] samples, SFXRComponent sound )
    {
        // Calculate the envelope amplitude for each sample
        for ( int i = 0; i < samples.Length; i++ )
        {
            float t = i / (float)sound.SampleRate;
            float amplitude = GetAmplitude( t );
            samples[i] = (short)(samples[i] * amplitude);
        }

        return samples;
    }

    public float GetLength()
    {
        return Attack + Decay + SustainTime + Release;
    }

    public Curve GetCurve()
    {
        Curve curve = new();

        List<Vector2> points = new();

        // Add the attack curve
        points.Add( new Vector2( 0, 0 ) );
        points.Add( new Vector2( Attack, 1 ) );

        // Add the decay curve
        points.Add( new Vector2( Attack + Decay, Sustain ) );

        // Add the sustain curve
        points.Add( new Vector2( Attack + Decay + SustainTime, Sustain ) );

        // Add the release curve
        points.Add( new Vector2( Attack + Decay + SustainTime + Release, 0 ) );

        // Normalize the curve to 0-1 in the x
        for ( int i = 0; i < points.Count; i++ )
        {
            points[i] = new Vector2( points[i].x / (Attack + Decay + SustainTime + Release), points[i].y );
        }

        // Add the points to the curve
        foreach ( var point in points )
        {
            curve.AddPoint( point.x, point.y );
        }

        return curve;
    }
}
using Sandbox;

public sealed class SceneTrigger : Component, Component.ITriggerListener
{
	[Property] public SceneFile SceneFile { get; set; }
	protected override void OnUpdate()
	{

	}

	void ITriggerListener.OnTriggerEnter(Sandbox.Collider other)
	{
		if (other.GameObject.Parent.Tags.Has("player") || other.GameObject.Tags.Has("boat"))
		{
			Game.ActiveScene.Load(SceneFile);
		}
	}

	void ITriggerListener.OnTriggerExit(Sandbox.Collider other)
	{

	}
}
using System.Collections.Generic;

namespace Sandbox;

/// <summary>
/// How to use the system: 
/// <code>
/// public sealed class ExampleComponent : Component
/// {
///		// Reference to the system.
///		private FixedUpdateInputSystem _fixedInput;
///		
///		protected override void Start()
///		{
///			// Get the reference like this:
///			_fixedInput = Scene.GetSystem&lt;FixedUpdateInputSystem&gt;();
///			
///			base.OnStart();
///		}
///		
///		protected override void OnFixedUpdate()
///		{
///			// Query for input like usual.
///			if( _fixedInput.Pressed("jump") )
///			{
///				Log.Info("Jumped");
///			}
///			
///			base.OnFixedUpdate();
///		}
/// }
/// </code>
/// </summary>
public sealed class FixedUpdateInputSystem : GameObjectSystem
{
	private struct FixedUpdateInputBuffer
	{
		private class State
		{
			public bool Held;
			public bool Pressed;
			public bool Released;
		}

		private Dictionary<string, State> _actionStates;

		public FixedUpdateInputBuffer()
		{
			_actionStates = new Dictionary<string, State>();

			foreach ( var b in Input.GetActions() )
			{
				_actionStates[b.Name.ToLowerInvariant()] = new State();
			}
		}

		/// <summary>
		/// Call from a <see cref="Component.OnUpdate"/> method
		/// to update the states of the actions.
		/// </summary>
		public void OnUpdate()
		{
			foreach ( var (name, state) in _actionStates )
			{
				if ( Input.Down( name ) )
					_actionStates[name].Held = true;

				if ( Input.Pressed( name ) )
					_actionStates[name].Pressed = true;

				if ( Input.Released( name ) )
					_actionStates[name].Released = true;
			}
		}

		/// <summary>
		/// Call from a <see cref="Component.OnFixedUpdate"/>
		/// method to get the <see cref="State.Held"/> state of this action.
		/// </summary>
		/// <param name="action">The action name (case insensitive).</param>
		/// <returns></returns>
		/// 
		public bool Held( string action )
		{
			return _actionStates[action.ToLowerInvariant()].Held;
		}

		/// <summary>
		/// Call from a <see cref="Component.OnFixedUpdate"/>
		/// method to get the <see cref="State.Pressed"/> state of this action.
		/// </summary>
		/// <param name="action">The action name (case insensitive).</param>
		/// <returns></returns>
		public bool Pressed( string action )
		{
			return _actionStates[action.ToLowerInvariant()].Pressed;
		}

		/// <summary>
		/// Call from a <see cref="Component.OnFixedUpdate"/>
		/// method to get the <see cref="State.Pressed"/> state of this action.
		/// </summary>
		/// <param name="action">The action name (case insensitive).</param>
		/// <returns></returns>
		public bool Released( string action )
		{
			return _actionStates[action.ToLowerInvariant()].Released;
		}

		/// <summary>
		/// Call at the end of your <see cref="Component.OnFixedUpdate"/> method
		/// to clear the state of the struct and reset.
		/// </summary>
		public void Clear()
		{
			foreach ( var actionName in _actionStates.Keys )
			{
				_actionStates[actionName].Held = false;
				_actionStates[actionName].Pressed = false;
			}
		}
	}

	private FixedUpdateInputBuffer _buffer;

	public FixedUpdateInputSystem( Scene scene ) : base( scene )
	{
		_buffer = new();
		Listen( Stage.StartUpdate, int.MinValue, OnStartUpdate, "FUIB.OnStartUpdate" );
		Listen( Stage.FinishFixedUpdate, int.MaxValue, OnFinishFixedUpdate, "FUIB.OnFinishFixedUpdate" );
	}

	private void OnStartUpdate()
	{
		_buffer.OnUpdate();
	}

	private void OnFinishFixedUpdate()
	{
		_buffer.Clear();
	}

	/// <summary>
	/// Is the action currently held down?
	/// </summary>
	/// <param name="action">The action name (case insensitive).</param>
	/// <returns></returns>
	/// 
	public bool Held( string action ) => _buffer.Held( action );

	/// <summary>
	/// Was the action pressed?
	/// </summary>
	/// <param name="action">The action name (case insensitive).</param>
	/// <returns></returns>
	public bool Pressed( string action ) => _buffer.Pressed( action );

	/// <summary>
	/// Was the action released?
	/// </summary>
	/// <param name="action">The action name (case insensitive).</param>
	/// <returns></returns>
	public bool Released( string action ) => _buffer.Released( action );
}

using System;

namespace Sandbox.Events;

/// <summary>
/// Only valid on <see cref="IGameEventHandler{T}.OnGameEvent"/> implementations. Forces this
/// event handler to be invoked before any handlers not marked as early, except if more specific
/// constraints are given (i.e., <see cref="BeforeAttribute{T}"/>, <see cref="AfterAttribute{T}"/>).
/// </summary>
[AttributeUsage( AttributeTargets.Method )]
public sealed class EarlyAttribute : Attribute
{

}

/// <summary>
/// Only valid on <see cref="IGameEventHandler{T}.OnGameEvent"/> implementations. Forces this
/// event handler to be invoked after any handlers not marked as late, except if more specific
/// constraints are given (i.e., <see cref="BeforeAttribute{T}"/>, <see cref="AfterAttribute{T}"/>).
/// </summary>
[AttributeUsage( AttributeTargets.Method )]
public sealed class LateAttribute : Attribute
{

}

internal interface IBeforeAttribute
{
	Type Type { get; }
}

internal interface IAfterAttribute
{
	Type Type { get; }
}

/// <summary>
/// Only valid on <see cref="IGameEventHandler{T}.OnGameEvent"/> implementations. Forces this
/// event handler to be invoked before any handlers in the specified type.
/// </summary>
[AttributeUsage( AttributeTargets.Method, AllowMultiple = true )]
public sealed class BeforeAttribute<T> : Attribute, IBeforeAttribute
{
	Type IBeforeAttribute.Type => typeof(T);
}

/// <summary>
/// Only valid on <see cref="IGameEventHandler{T}.OnGameEvent"/> implementations. Forces this
/// event handler to be invoked after any handlers in the specified type.
/// </summary>
[AttributeUsage( AttributeTargets.Method, AllowMultiple = true )]
public sealed class AfterAttribute<T> : Attribute, IAfterAttribute
{
	Type IAfterAttribute.Type => typeof( T );
}
using System.Collections.Generic;
using System.Linq;

namespace Sandbox.Events;

/// <summary>
/// Generate an ordering based on a set of first-most and last-most items, and
/// individual constraints between pairs of items. All first-most items will be
/// ordered before all last-most items, and any other items will be put in the
/// middle unless forced to be elsewhere by a constraint.
/// </summary>
internal class SortingHelper
{
	public record struct SortConstraint( int EarlierIndex, int LaterIndex )
	{
		public SortConstraint Complement => new ( LaterIndex, EarlierIndex );
	}

	private readonly int _itemCount;

	private readonly HashSet<SortConstraint> _initialConstraints = new HashSet<SortConstraint>();

	private readonly HashSet<int> _first = new HashSet<int>();
	private readonly HashSet<int> _last = new HashSet<int>();

	public SortingHelper( int itemCount )
	{
		_itemCount = itemCount;
	}

	public void AddConstraint( int earlierIndex, int laterIndex )
	{
		_initialConstraints.Add( new SortConstraint( earlierIndex, laterIndex ) );
	}

	public void AddFirst( int earlierIndex )
	{
		_first.Add( earlierIndex );
	}

	public void AddLast( int laterIndex )
	{
		_last.Add( laterIndex );
	}

	public bool Sort( List<int> result, out SortConstraint invalidConstraint )
	{
		var middle = new HashSet<int>();

		for ( var index = 0; index < _itemCount; ++index )
		{
			if ( !_first.Contains( index ) && !_last.Contains( index ) )
				middle.Add( index );
		}

		var allConstraints = new HashSet<SortConstraint>();
		var newConstraints = new Queue<SortConstraint>();
		var beforeDict = new Dictionary<int, HashSet<int>>();
		var afterDict = new Dictionary<int, HashSet<int>>();

		bool AddWorkingConstraint( int earlierIndex, int laterIndex, out SortConstraint constraint )
		{
			constraint = new SortConstraint( earlierIndex, laterIndex );

			if ( allConstraints.Contains( constraint.Complement ) )
				return false;

			if ( !allConstraints.Add( constraint ) )
				return true;

			newConstraints.Enqueue( constraint );

			if ( !beforeDict.TryGetValue( earlierIndex, out var before ) )
				beforeDict.Add( earlierIndex, before = new HashSet<int>() );

			if ( !afterDict.TryGetValue( laterIndex, out var after ) )
				afterDict.Add( laterIndex, after = new HashSet<int>() );

			before.Add( laterIndex );
			after.Add( earlierIndex );

			return true;
		}

		// Add initial constraints

		foreach ( var initialConstraint in _initialConstraints )
		{
			if ( !AddWorkingConstraint( initialConstraint.EarlierIndex, initialConstraint.LaterIndex, out invalidConstraint ) )
				return false;
		}

		// Everything in _first should be before everything in _last

		foreach ( var earlierIndex in _first )
		{
			foreach ( var laterIndex in _last )
			{
				if ( !AddWorkingConstraint( earlierIndex, laterIndex, out invalidConstraint ) )
					return false;
			}
		}

		// Keep propagating constraints until nothing changes

		while ( newConstraints.TryDequeue( out var nextConstraint ) )
		{
			// if a < b, and b < c, then a < c etc

			if ( beforeDict.TryGetValue( nextConstraint.LaterIndex, out var before ) )
			{
				foreach ( var laterIndex in before )
				{
					if ( !AddWorkingConstraint( nextConstraint.EarlierIndex, laterIndex, out invalidConstraint ) )
						return false;
				}
			}

			if ( afterDict.TryGetValue( nextConstraint.EarlierIndex, out var after ) )
			{
				foreach ( var earlierIndex in after )
				{
					if ( !AddWorkingConstraint( earlierIndex, nextConstraint.LaterIndex, out invalidConstraint ) )
					{
						return false;
					}
				}
			}
		}

		// Now if we have any items that aren't using GroupOrder.First, and haven't
		// determined that they are ordered before another item with GroupOrder.First,
		// we can safely order them after all GroupOrder.First items. And vice versa.

		foreach ( var middleIndex in middle )
		{
			var isBeforeAnyFirst = beforeDict.TryGetValue( middleIndex, out var before )
				&& before.Any( x => _first.Contains( x ) );

			var isAfterAnyLast = afterDict.TryGetValue( middleIndex, out var after )
				&& after.Any( x => _last.Contains( x ) );

			if ( !isBeforeAnyFirst )
			{
				foreach ( var earlierIndex in _first )
					AddWorkingConstraint( earlierIndex, middleIndex, out invalidConstraint );
			}

			if ( !isAfterAnyLast )
			{
				foreach ( var laterIndex in _last )
					AddWorkingConstraint( middleIndex, laterIndex, out invalidConstraint );
			}
		}

		// Now lets add items to the final ordering if all items that should be sorted
		// before them are already added to that ordering. We'll implement this by choosing
		// items that have an empty list / don't appear in afterDict, and update that
		// dictionary as we go.

		var earliestRemaining = new Queue<int>();

		// First, seed the queue with everything that's already not ordered after anything

		for ( var index = 0; index < _itemCount; ++index )
		{
			if ( !afterDict.ContainsKey( index ) )
			{
				earliestRemaining.Enqueue( index );
			}
		}

		result.Clear();

		while ( earliestRemaining.TryDequeue( out var nextIndex ) )
		{
			result.Add( nextIndex );

			foreach ( var laterIndex in beforeDict.TryGetValue( nextIndex, out var laterIndices )
				? laterIndices : Enumerable.Empty<int>() )
			{
				var beforeLater = afterDict[laterIndex];
				beforeLater.Remove( nextIndex );

				if ( beforeLater.Count == 0 )
					earliestRemaining.Enqueue( laterIndex );
			}
		}

		invalidConstraint = default;
		return result.Count == _itemCount;
	}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace Sandbox.Events;

/// <summary>
/// Interface for event payloads that can be listened for by <see cref="IGameEventHandler{T}"/>s.
/// </summary>
public interface IGameEvent { }

/// <summary>
/// Interface for components that handle game events with a payload of type <see cref="T"/>.
/// </summary>
/// <typeparam name="T">Event payload type.</typeparam>
public interface IGameEventHandler<in T>
	where T : IGameEvent
{
	/// <summary>
	/// Called when an event with payload of type <see cref="T"/> is dispatched on a <see cref="GameObject"/>
	/// that contains this component, including on a descendant.
	/// </summary>
	/// <param name="eventArgs">Event payload.</param>
	void OnGameEvent( T eventArgs );
}

/// <summary>
/// Helper for dispatching game events in a scene.
/// </summary>
public static class GameEvent
{
	private static Dictionary<Type, IReadOnlyDictionary<Type, int>> HandlerOrderingCache { get; } = new();

	/// <summary>
	/// Notifies all <see cref="IGameEventHandler{T}"/> components that are within <paramref name="root"/>,
	/// with a payload of type <typeparamref name="T"/>.
	/// </summary>
	public static void Dispatch<T>( this GameObject root, T eventArgs )
		where T : IGameEvent
	{
		var handlers = (root is Scene scene
			? scene.GetAllComponents<IGameEventHandler<T>>() // I think this is more efficient?
			: root.Components.GetAll<IGameEventHandler<T>>())
			.ToArray();

		if ( !HandlerOrderingCache.TryGetValue( typeof(T), out var ordering ) || handlers.Any( x => !ordering.ContainsKey( x.GetType() ) ) )
		{
			ordering = HandlerOrderingCache[typeof(T)] = GetHandlerOrdering<T>();
		}

		List<Exception>? exceptions = null;

		foreach ( var handler in handlers.OrderBy( x => ordering[x.GetType()] ) )
		{
			try
			{
				handler.OnGameEvent( eventArgs );
			}
			catch ( Exception e )
			{
				exceptions ??= new();
				exceptions.Add( e );
			}
		}

		switch ( exceptions?.Count )
		{
			case 1:
				Log.Error( exceptions[0] );
				break;

			case > 1:
				Log.Error( new AggregateException( exceptions ) );
				break;
		}
	}

	private static bool IsImplementingMethodName( string methodName )
	{
		if ( methodName == nameof(IGameEventHandler<IGameEvent>.OnGameEvent) )
		{
			return true;
		}

		return methodName.StartsWith( "Sandbox.Events.IGameEventHandler<" ) && methodName.EndsWith( ">.OnGameEvent" );
	}

	private static MethodDescription? GetImplementation<T>( TypeDescription type )
	{
		foreach ( var method in type.Methods )
		{
			if ( method.IsStatic ) continue;
			if ( method.Parameters.Length != 1 ) continue;
			if ( method.Parameters[0].ParameterType != typeof( T ) ) continue;

			if ( !IsImplementingMethodName( method.Name ) ) continue;

			return method;
		}

		return null;
	}

	private static IReadOnlyDictionary<Type, int> GetHandlerOrdering<T>()
		where T : IGameEvent
	{
		var types = TypeLibrary.GetTypes<IGameEventHandler<T>>().ToArray();
		var helper = new SortingHelper( types.Length );

		for ( var i = 0; i < types.Length; ++i )
		{
			var type = types[i];
			var method = GetImplementation<T>( type );

			if ( method is null )
			{
				Log.Warning( $"Can't find {nameof( IGameEventHandler<T> )}<{typeof( T ).Name}> implementation in {type.Name}!" );
				continue;
			}

			foreach ( var attrib in method.Attributes )
			{
				switch ( attrib )
				{
					case EarlyAttribute:
						helper.AddFirst( i );
						break;

					case LateAttribute:
						helper.AddLast( i );
						break;

					case IBeforeAttribute before:
						for ( var j = 0; j < types.Length; ++j )
						{
							if ( i == j ) continue;

							var other = types[j];

							if ( before.Type.IsAssignableFrom( other.TargetType ) )
							{
								helper.AddConstraint( i, j );
							}
						}

						break;

					case IAfterAttribute after:
						for ( var j = 0; j < types.Length; ++j )
						{
							if ( i == j ) continue;

							var other = types[j];

							if ( after.Type.IsAssignableFrom( other.TargetType ) )
							{
								helper.AddConstraint( j, i );
							}
						}

						break;
				}
			}
		}

		var ordering = new List<int>();

		if ( !helper.Sort( ordering, out var invalid ) )
		{
			Log.Error( $"Invalid event ordering constraint between {types[invalid.EarlierIndex].Name} and {types[invalid.LaterIndex].Name}!" );
			return ImmutableDictionary<Type, int>.Empty;
		}

		return Enumerable.Range( 0, ordering.Count )
			.ToImmutableDictionary( i => types[ordering[i]].TargetType, i => i );
	}
}

public delegate void GameEventAction<in T>( T eventArgs )
	where T : IGameEvent;

/// <summary>
/// Base class for components that expose game events to Action Graph.
/// </summary>
public abstract class GameEventComponent<T> : Component, IGameEventHandler<T>
	where T : IGameEvent
{
	/// <summary>
	/// Action invoked when the <typeparamref name="T"/> event is dispatched.
	/// </summary>
	[Property]
	public GameEventAction<T>? OnEvent { get; set; }

	/// <summary>
	/// If this component is within a state machine, optional state to transition
	/// to when this event is dispatched.
	/// </summary>
	[Property]
	public StateComponent? NextState { get; set; }

	void IGameEventHandler<T>.OnGameEvent( T eventArgs )
	{
		OnEvent?.Invoke( eventArgs );

		if ( NextState is not null )
		{
			Components.GetInAncestorsOrSelf<StateMachineComponent>()?.Transition( NextState );
		}
	}
}
using System.Collections.Generic;
using System.Linq;

namespace Sandbox.Events;

/// <summary>
/// Generate an ordering based on a set of first-most and last-most items, and
/// individual constraints between pairs of items. All first-most items will be
/// ordered before all last-most items, and any other items will be put in the
/// middle unless forced to be elsewhere by a constraint.
/// </summary>
internal class SortingHelper
{
	public record struct SortConstraint( int EarlierIndex, int LaterIndex )
	{
		public SortConstraint Complement => new ( LaterIndex, EarlierIndex );
	}

	private readonly int _itemCount;

	private readonly HashSet<SortConstraint> _initialConstraints = new HashSet<SortConstraint>();

	private readonly HashSet<int> _first = new HashSet<int>();
	private readonly HashSet<int> _last = new HashSet<int>();

	public SortingHelper( int itemCount )
	{
		_itemCount = itemCount;
	}

	public void AddConstraint( int earlierIndex, int laterIndex )
	{
		_initialConstraints.Add( new SortConstraint( earlierIndex, laterIndex ) );
	}

	public void AddFirst( int earlierIndex )
	{
		_first.Add( earlierIndex );
	}

	public void AddLast( int laterIndex )
	{
		_last.Add( laterIndex );
	}

	public bool Sort( List<int> result, out SortConstraint invalidConstraint )
	{
		var middle = new HashSet<int>();

		for ( var index = 0; index < _itemCount; ++index )
		{
			if ( !_first.Contains( index ) && !_last.Contains( index ) )
				middle.Add( index );
		}

		var allConstraints = new HashSet<SortConstraint>();
		var newConstraints = new Queue<SortConstraint>();
		var beforeDict = new Dictionary<int, HashSet<int>>();
		var afterDict = new Dictionary<int, HashSet<int>>();

		bool AddWorkingConstraint( int earlierIndex, int laterIndex, out SortConstraint constraint )
		{
			constraint = new SortConstraint( earlierIndex, laterIndex );

			if ( allConstraints.Contains( constraint.Complement ) )
				return false;

			if ( !allConstraints.Add( constraint ) )
				return true;

			newConstraints.Enqueue( constraint );

			if ( !beforeDict.TryGetValue( earlierIndex, out var before ) )
				beforeDict.Add( earlierIndex, before = new HashSet<int>() );

			if ( !afterDict.TryGetValue( laterIndex, out var after ) )
				afterDict.Add( laterIndex, after = new HashSet<int>() );

			before.Add( laterIndex );
			after.Add( earlierIndex );

			return true;
		}

		// Add initial constraints

		foreach ( var initialConstraint in _initialConstraints )
		{
			if ( !AddWorkingConstraint( initialConstraint.EarlierIndex, initialConstraint.LaterIndex, out invalidConstraint ) )
				return false;
		}

		// Everything in _first should be before everything in _last

		foreach ( var earlierIndex in _first )
		{
			foreach ( var laterIndex in _last )
			{
				if ( !AddWorkingConstraint( earlierIndex, laterIndex, out invalidConstraint ) )
					return false;
			}
		}

		// Keep propagating constraints until nothing changes

		while ( newConstraints.TryDequeue( out var nextConstraint ) )
		{
			// if a < b, and b < c, then a < c etc

			if ( beforeDict.TryGetValue( nextConstraint.LaterIndex, out var before ) )
			{
				foreach ( var laterIndex in before )
				{
					if ( !AddWorkingConstraint( nextConstraint.EarlierIndex, laterIndex, out invalidConstraint ) )
						return false;
				}
			}

			if ( afterDict.TryGetValue( nextConstraint.EarlierIndex, out var after ) )
			{
				foreach ( var earlierIndex in after )
				{
					if ( !AddWorkingConstraint( earlierIndex, nextConstraint.LaterIndex, out invalidConstraint ) )
					{
						return false;
					}
				}
			}
		}

		// Now if we have any items that aren't using GroupOrder.First, and haven't
		// determined that they are ordered before another item with GroupOrder.First,
		// we can safely order them after all GroupOrder.First items. And vice versa.

		foreach ( var middleIndex in middle )
		{
			var isBeforeAnyFirst = beforeDict.TryGetValue( middleIndex, out var before )
				&& before.Any( x => _first.Contains( x ) );

			var isAfterAnyLast = afterDict.TryGetValue( middleIndex, out var after )
				&& after.Any( x => _last.Contains( x ) );

			if ( !isBeforeAnyFirst )
			{
				foreach ( var earlierIndex in _first )
					AddWorkingConstraint( earlierIndex, middleIndex, out invalidConstraint );
			}

			if ( !isAfterAnyLast )
			{
				foreach ( var laterIndex in _last )
					AddWorkingConstraint( middleIndex, laterIndex, out invalidConstraint );
			}
		}

		// Now lets add items to the final ordering if all items that should be sorted
		// before them are already added to that ordering. We'll implement this by choosing
		// items that have an empty list / don't appear in afterDict, and update that
		// dictionary as we go.

		var earliestRemaining = new Queue<int>();

		// First, seed the queue with everything that's already not ordered after anything

		for ( var index = 0; index < _itemCount; ++index )
		{
			if ( !afterDict.ContainsKey( index ) )
			{
				earliestRemaining.Enqueue( index );
			}
		}

		result.Clear();

		while ( earliestRemaining.TryDequeue( out var nextIndex ) )
		{
			result.Add( nextIndex );

			foreach ( var laterIndex in beforeDict.TryGetValue( nextIndex, out var laterIndices )
				? laterIndices : Enumerable.Empty<int>() )
			{
				var beforeLater = afterDict[laterIndex];
				beforeLater.Remove( nextIndex );

				if ( beforeLater.Count == 0 )
					earliestRemaining.Enqueue( laterIndex );
			}
		}

		invalidConstraint = default;
		return result.Count == _itemCount;
	}
}
using Sandbox;
using System.Collections.Generic;

namespace EZCameraShake
{
    public class CameraShaker : Component
    {
        /// <summary>
        /// The single instance of the CameraShaker in the current scene. Do not use if you have multiple instances.
        /// </summary>
        public static CameraShaker Instance;
        static Dictionary<string, CameraShaker> instanceList = new Dictionary<string, CameraShaker>();

        /// <summary>
        /// The default position influcence of all shakes created by this shaker.
        /// </summary>
        [Property] public Vector3 DefaultPosInfluence = new Vector3(0.15f, 0.15f, 0.15f);
		/// <summary>
		/// The default rotation influcence of all shakes created by this shaker.
		/// </summary>
		[Property]  public Vector3 DefaultRotInfluence = new Vector3(1, 1, 1);
		/// <summary>
		/// Offset that will be applied to the camera's default (0,0,0) rest position
		/// </summary>
		[Property] public Vector3 RestPositionOffset = new Vector3(0, 0, 0);
		/// <summary>
		/// Offset that will be applied to the camera's default (0,0,0) rest rotation
		/// </summary>
		[Property] public Vector3 RestRotationOffset = new Vector3(0, 0, 0);

        Vector3 posAddShake, rotAddShake;

        List<CameraShakeInstance> cameraShakeInstances = new List<CameraShakeInstance>();

        protected override void OnAwake()
        {
            Instance = this;
            instanceList.Add(GameObject.Name, this);
        }

		protected override void OnUpdate()
        {
            posAddShake = Vector3.Zero;
            rotAddShake = Vector3.Zero;

            for (int i = 0; i < cameraShakeInstances.Count; i++)
            {
                if (i >= cameraShakeInstances.Count)
                    break;

                CameraShakeInstance c = cameraShakeInstances[i];

                if (c.CurrentState == CameraShakeState.Inactive && c.DeleteOnInactive)
                {
                    cameraShakeInstances.RemoveAt(i);
                    i--;
                }
                else if (c.CurrentState != CameraShakeState.Inactive)
                {
                    posAddShake += CameraUtilities.MultiplyVectors(c.UpdateShake(), c.PositionInfluence);
                    rotAddShake += CameraUtilities.MultiplyVectors(c.UpdateShake(), c.RotationInfluence);
                }
            }

            Transform.LocalPosition = (posAddShake) + RestPositionOffset;
			Vector3 thing = (rotAddShake / 100) + RestRotationOffset;

			Transform.LocalRotation = new Angles(thing.x, thing.y, thing.z);
        }

        /// <summary>
        /// Gets the CameraShaker with the given name, if it exists.
        /// </summary>
        /// <param name="name">The name of the camera shaker instance.</param>
        /// <returns></returns>
        public static CameraShaker GetInstance(string name)
        {
            CameraShaker c;

            if (instanceList.TryGetValue(name, out c))
                return c;

            Log.Error("CameraShake " + name + " not found!");

            return null;
        }

        /// <summary>
        /// Starts a shake using the given preset.
        /// </summary>
        /// <param name="shake">The preset to use.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance Shake(CameraShakeInstance shake)
        {
            cameraShakeInstances.Add(shake);
            return shake;
        }

        /// <summary>
        /// Shake the camera once, fading in and out  over a specified durations.
        /// </summary>
        /// <param name="magnitude">The intensity of the shake.</param>
        /// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
        /// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
        /// <param name="fadeOutTime">How long to fade out the shake, in seconds.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance ShakeOnce(float magnitude, float roughness, float fadeInTime, float fadeOutTime)
        {
            CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness, fadeInTime, fadeOutTime);
            shake.PositionInfluence = DefaultPosInfluence;
            shake.RotationInfluence = DefaultRotInfluence;
            cameraShakeInstances.Add(shake);

            return shake;
        }

        /// <summary>
        /// Shake the camera once, fading in and out over a specified durations.
        /// </summary>
        /// <param name="magnitude">The intensity of the shake.</param>
        /// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
        /// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
        /// <param name="fadeOutTime">How long to fade out the shake, in seconds.</param>
        /// <param name="posInfluence">How much this shake influences position.</param>
        /// <param name="rotInfluence">How much this shake influences rotation.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance ShakeOnce(float magnitude, float roughness, float fadeInTime, float fadeOutTime, Vector3 posInfluence, Vector3 rotInfluence)
        {
            CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness, fadeInTime, fadeOutTime);
            shake.PositionInfluence = posInfluence;
            shake.RotationInfluence = rotInfluence;
            cameraShakeInstances.Add(shake);

            return shake;
        }

        /// <summary>
        /// Start shaking the camera.
        /// </summary>
        /// <param name="magnitude">The intensity of the shake.</param>
        /// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
        /// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance StartShake(float magnitude, float roughness, float fadeInTime)
        {
            CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness);
            shake.PositionInfluence = DefaultPosInfluence;
            shake.RotationInfluence = DefaultRotInfluence;
            shake.StartFadeIn(fadeInTime);
            cameraShakeInstances.Add(shake);
            return shake;
        }

        /// <summary>
        /// Start shaking the camera.
        /// </summary>
        /// <param name="magnitude">The intensity of the shake.</param>
        /// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
        /// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
        /// <param name="posInfluence">How much this shake influences position.</param>
        /// <param name="rotInfluence">How much this shake influences rotation.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance StartShake(float magnitude, float roughness, float fadeInTime, Vector3 posInfluence, Vector3 rotInfluence)
        {
            CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness);
            shake.PositionInfluence = posInfluence;
            shake.RotationInfluence = rotInfluence;
            shake.StartFadeIn(fadeInTime);
            cameraShakeInstances.Add(shake);
            return shake;
        }

        /// <summary>
        /// Gets a copy of the list of current camera shake instances.
        /// </summary>
        public List<CameraShakeInstance> ShakeInstances
        { get { return new List<CameraShakeInstance>(cameraShakeInstances); } }

		protected override void OnDestroy()
        {
            instanceList.Remove(GameObject.Name);
        }
    }
}

public sealed class PlayerPusher : Component
{
	[Property] public float Radius { get; set; } = 100;

	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		Gizmo.Draw.LineSphere( Vector3.Zero, Radius );
	}

	public static Vector3 GetPushVector( in Vector3 position, Scene scene, GameObject ignore )
	{
		Vector3 vec = default;

		foreach ( var pusher in scene.GetAllComponents<PlayerPusher>() )
		{
			if ( pusher.GameObject.IsAncestor( ignore ) )
				continue;

			pusher.Collect( position, ref vec );
		}

		return vec;
	}

	private void Collect( Vector3 position, ref Vector3 output )
	{
		var delta = (position - Transform.Position);
		if ( delta.Length > Radius ) return;

		delta.z = 0; // ignore z

		var distanceDelta = (delta.Length / Radius);

		output += delta.Normal * (1.0f - distanceDelta);
	}
}
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using Sandbox;
using System.Threading.Tasks;

public sealed class WebSocketUtility : Component
{
	[Property] public List<WebsocketTools> websocketToolsList { get; set; }
	protected override void OnAwake()
	{
		foreach ( var websocketTools in websocketToolsList )
		{
			if ( websocketTools.url is null )
			{
				Log.Error( "WebsocketTools URL is null" );
				return;
			}
			websocketTools.webSocket = new WebSocket();
			ConnectToSocket( websocketTools.webSocket, websocketTools.url );
			websocketTools.isConnected = true;
			websocketTools.webSocket.OnMessageReceived += websocketTools.OnMessageReceivedMethod;
			websocketTools.isSubscribed = true;
		}
	}

	protected override void OnUpdate()
	{
		SendMessageFromList( WebsocketTools.Fetch.OnUpdate );
	}
	protected override void OnFixedUpdate()
	{
		SendMessageFromList( WebsocketTools.Fetch.OnFixedUpdate );
	}
	protected override void OnStart()
	{
		SendMessageFromList( WebsocketTools.Fetch.OnStart );
	}

	private async void SendMessageFromList( WebsocketTools.Fetch fetch )
	{
		foreach ( var websocketTools in websocketToolsList )
		{
			if ( websocketTools.fetch == fetch )
			{
				if ( websocketTools.message.UseJsonTags )
				{
					var jsonStrings = websocketTools.message.jsonTags.Select( tag => Json.Serialize( tag.ToString() ) );

					var bigString = string.Join( "", jsonStrings );

					var finalJsonString = Json.Serialize( bigString );

					await websocketTools.webSocket.Send( finalJsonString );
				}
				else
				{
					var messageBytes = Encoding.UTF8.GetBytes( websocketTools.message.message );
					await websocketTools.webSocket.Send( messageBytes );
				}
			}
		}
	}

	[Description( "Sends a message over a websocket connection" )]
	public static async Task SendAsync( WebsocketTools websocketTools )
	{
		if ( websocketTools.webSocket is null )
		{
			websocketTools.webSocket = new WebSocket();
		}
		if ( !websocketTools.isConnected )
		{
			await websocketTools.webSocket.Connect( websocketTools.url );
			websocketTools.isConnected = true;
		}

        if ( websocketTools.message.UseJsonTags )
            await websocketTools.webSocket.Send( Json.Serialize( websocketTools.message.jsonTags ) );
        else
		    await websocketTools.webSocket.Send( websocketTools.message.message );

		if ( !websocketTools.isSubscribed )
		{
			websocketTools.webSocket.OnMessageReceived += websocketTools.OnMessageReceivedMethod;
			websocketTools.isSubscribed = true;
		}
	}

	public static async Task SendStringAsync( string url, string message )
	{
		var webSocket = new WebSocket();
		await webSocket.Connect( url );
		await webSocket.Send( message );
	}

	public static void ChangeJsonTagValue( WebsocketMessage message, string tag, string value )
	{
         if ( message is null )
            message = new WebsocketMessage();

		if ( message.jsonTags is null )
			message.jsonTags = new List<JsonTags>();

		var jsonTag = message.jsonTags.Find( x => x.tag == tag );
		if ( jsonTag is null )
		{
			Log.Warning( $"Tag {tag} not found in message" );
		}
		else
		{
			jsonTag.value = value;
		}
	}

	public static void AddJsonTag( WebsocketMessage message, string tag, string value )
	{
        if ( message is null )
            message = new WebsocketMessage();
		
		if ( message.jsonTags is null )
			message.jsonTags = new List<JsonTags>();

		var jsonTag = new JsonTags
		{
			tag = tag,
			value = value
		};
		message.jsonTags.Add( jsonTag );
	}

	private async void ConnectToSocket( WebSocket webSocket, string url )
	{
		await webSocket.Connect( url );
	}

	[ActionGraphNode( "new websocket tools" ), Pure]
	public static WebsocketTools NewWebsocketTools()
	{
		return new WebsocketTools();
	}
}

public class WebsocketTools
{
	public delegate void OnMessageReceived( string message );
	public OnMessageReceived onMessageReceived { get; set; }
	public WebSocket webSocket { get; set; }
	public string url { get; set; }
	public WebsocketMessage message { get; set; } = new();
	public bool isConnected { get; set; }
	public bool isSubscribed { get; set; }
	public string returnMessage { get; set; }
	public enum Fetch
	{
		OnUpdate,
		OnFixedUpdate,
		OnStart,
	}
	public Fetch fetch { get; set; }

	public void OnMessageReceivedMethod( string message )
	{
		onMessageReceived?.Invoke( message );
		returnMessage = message;
	}

	public WebsocketTools()
	{
		url = "ws://localhost:8080";
		fetch = Fetch.OnUpdate;
		onMessageReceived = null;
		message = null;
	}

	public WebsocketTools( string url, OnMessageReceived onMessageReceived, WebsocketMessage message, Fetch fetch = Fetch.OnUpdate )
	{
		this.url = url;
		this.fetch = fetch;
		this.onMessageReceived = onMessageReceived;
		this.message = message;
	}

}
[GameResource( "Message", "message", "A message to be sent over a websocket connection", Icon = "chat_bubble" )]
public class WebsocketMessage : GameResource
{
	public bool UseJsonTags { get; set; }
	[ShowIf( "UseJsonTags", false )] public string message { get; set; } = "";
	[ShowIf( "UseJsonTags", true )] public List<JsonTags> jsonTags { get; set; } = new();
}

public class JsonTags
{
	public string tag { get; set; }
	public string value { get; set; }

}
public sealed class JiggleBone : TransformProxyComponent
{
	JiggleBoneState state = new JiggleBoneState();

	[Property]
	public Vector3 StartPoint = new Vector3( 0, 0, 0 );

	[Property]
	public Vector3 EndPoint = new Vector3( 32, 0, 0 );

	[Property, Range( 0, 2 )]
	public float Speed { get; set; } = 1.0f;

	[Property, Range( 0, 2 )]
	public float Stiffness { get; set; } = 1.0f;

	[Property, Range( 0, 2 )]
	public float Damping { get; set; } = 1.0f;

	[Property, Range( 0, 100 )]
	public float Radius { get; set; } = 40.0f;

	[Property, Range( 0, 100 )]
	public float Mass { get; set; } = 1.0f;

	Transform LocalJigglePosition;

	protected override void OnEnabled()
	{
		LocalJigglePosition = Transform.Local;

		base.OnEnabled();

		state = new JiggleBoneState();
	}

	protected override void OnUpdate()
	{
		var oldPos = LocalJigglePosition;



		using ( Transform.DisableProxy() )
		{
			var worldTx = Transform.World;

			var startPoint = worldTx.PointToWorld( StartPoint );
			var endPoint = worldTx.PointToWorld( EndPoint );

			//Gizmo.Draw.LineSphere( startPoint, 1 );
			//Gizmo.Draw.LineSphere( endPoint, 1 );

			state.Extent = (endPoint - startPoint);
			state.Stiffness = Stiffness;
			state.Damping = Damping;
			state.Radius = Radius;
			state.Mass = Mass;

			state.Update( startPoint, Time.Delta * Speed * 16.0f );

			var tx = worldTx.RotateAround( startPoint, state.Rotation );
			LocalJigglePosition = GameObject.Parent.Transform.World.ToLocal( tx );
		}

		if ( oldPos != LocalJigglePosition )
		{
			MarkTransformChanged();
		}
	}

	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		if ( !Gizmo.IsSelected )
			return;

		using ( Transform.DisableProxy() )
		{
			Gizmo.Transform = Transform.World;
			Gizmo.Draw.IgnoreDepth = false;
			Gizmo.Draw.Color = Gizmo.Colors.Yaw.WithAlpha( 0.5f );
			Gizmo.Draw.Line( StartPoint, EndPoint );
			Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( StartPoint, 5 ) );
			Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( EndPoint, 5 ) );
			Gizmo.Draw.LineSphere( EndPoint, Radius * 2.0f, 4 );
		}
	}

	public override Transform GetLocalTransform()
	{
		return LocalJigglePosition;
	}
}

class JiggleBoneState
{
	public Vector3 Extent = new Vector3( 32, 0, 0 );

	public Vector3 Position { get; set; }
	public Rotation Rotation { get; set; }
	public float Stiffness { get; set; } = 1.0f;
	public float Damping { get; set; } = 1.0f;
	public float Radius { get; set; } = 10.0f;
	public float Gravity { get; set; } = 1.0f;
	public float Mass { get; set; } = 1.0f;


	Vector3 basePosition;
	Vector3 velocity;

	public JiggleBoneState()
	{

	}

	internal void Update( Vector3 position, float timeDelta )
	{
		basePosition = position + Extent;

		// initialization
		if ( Position == default )
		{
			Position = basePosition;
		}

		// Calculate spring force based on displacement from the cube
		Vector3 displacement = Position - basePosition;
		Vector3 springForce = -Stiffness * displacement;

		// Calculate acceleration (Newton's second law)
		Vector3 acceleration = springForce / Mass;

		// Update velocity (integrate acceleration)
		velocity += acceleration * timeDelta;

		// Apply exponential damping
		velocity *= (float)Math.Exp( -Damping * timeDelta );

		// Update position (integrate velocity)
		Position += velocity * timeDelta;

		{
			var diff = Position - basePosition;
			var diffLen = diff.Length;
			if ( diffLen > Radius )
			{
				Position = basePosition + diff.Normal * Radius;
				//velocity = velocity.AddClamped( -diff * 2.0f, diff.Length );
			}
		}

		// Store the rotation offset result
		Rotation = Rotation.FromToRotation( basePosition - position, Position - position );

		//Gizmo.Draw.IgnoreDepth = true;
		//Gizmo.Draw.Line( position, Position );
		//Gizmo.Draw.Line( basePosition, Position );
	}
}
using Sandbox;

/// <summary>
/// This is a component - in your library!
/// </summary>
[Title( "LibraryImporter - My Component" )]
public class MyLibraryComponent : Component
{

}
using Sandbox;

public sealed class CameraMovement : Component
{
	[Property] public CharacterController1 Player { get; set; }
	[Property] public GameObject Body { get; set; }
	[Property] public GameObject Head { get; set; }
	[Property] public float Distance { get; set; } = 0f;
	[Property] public float Sensitivity { get; set; } = 0.1f;
	public bool IsFirstPerson => Distance == 0f;
	private CameraComponent Camera;
	private ModelRenderer BodyRenderer;
	private Vector3 CurrentOffset = Vector3.Zero;
	protected override void OnAwake()
	{
		base.OnAwake();
		Camera = Components.Get<CameraComponent>();
		BodyRenderer = Body.Components.Get<ModelRenderer>();
	}
	protected override void OnUpdate()
	{
		var eyeAngles = Head.Transform.Rotation.Angles();
		eyeAngles.pitch += Input.MouseDelta.y * Sensitivity;
		eyeAngles.yaw -= Input.MouseDelta.x * Sensitivity;
		eyeAngles.roll = 0f;
		eyeAngles.pitch = eyeAngles.pitch.Clamp( -89.9f, 89.9f );
		Head.Transform.Rotation = eyeAngles.ToRotation();
		var targetOffset = Vector3.Zero;
		if ( Player.IsCrouching ) targetOffset += Vector3.Down * 35f;
		CurrentOffset = Vector3.Lerp( CurrentOffset, targetOffset, Time.Delta * 10f );
		if ( Camera is not null )
		{
			var camPos = Head.Transform.Position + CurrentOffset;
			if ( !IsFirstPerson )
			{
				var camForward = eyeAngles.ToRotation().Forward;
				var camTrace = Scene.Trace.Ray( camPos, camPos - (camForward * Distance) )
					.WithoutTags( "player", "trigger" )
					.Run();
				if ( camTrace.Hit )
				{
					camPos = camTrace.HitPosition + camTrace.Normal;
				}
				else
				{
					camPos = camTrace.EndPosition;
				}
				BodyRenderer.RenderType = ModelRenderer.ShadowRenderType.On;
			}
			else
			{
				BodyRenderer.RenderType = ModelRenderer.ShadowRenderType.ShadowsOnly;
			}


			Log.Info( CurrentOffset );
			Camera.Transform.Position = camPos;
			Camera.Transform.Rotation = eyeAngles.ToRotation();
		}
	}
}
global using Microsoft.AspNetCore.Components; 
global using Microsoft.AspNetCore.Components.Rendering;
using System.Collections.Generic;
using Sandbox.Diagnostics;

namespace NPBehave
{
    public class Parallel : Composite
    {
        public enum Policy
        {
            One,
            All,
        }

        // public enum Wait
        // {
        //     NEVER,
        //     ON_FAILURE,
        //     ON_SUCCESS,
        //     BOTH
        // }

        // private Wait waitForPendingChildrenRule;
        private Policy _failurePolicy;
        private Policy _successPolicy;
        private int _childrenCount = 0;
        private int _runningCount = 0;
        private int _succeededCount = 0;
        private int _failedCount = 0;
        private Dictionary<Node, bool> _childrenResults;
        private bool _successState;
        private bool _childrenAborted;

        public Parallel(Policy successPolicy, Policy failurePolicy, /*Wait waitForPendingChildrenRule,*/ params Node[] children) : base("Parallel", children)
        {
            _successPolicy = successPolicy;
            _failurePolicy = failurePolicy;
            // this.waitForPendingChildrenRule = waitForPendingChildrenRule;
            _childrenCount = children.Length;
            _childrenResults = new Dictionary<Node, bool>();
        }

        protected override void DoStart()
        {
            foreach (Node child in Children)
            {
                Assert.AreEqual(child.CurrentState, State.Inactive);
            }

            _childrenAborted = false;
            _runningCount = 0;
            _succeededCount = 0;
            _failedCount = 0;
            foreach (Node child in Children)
            {
                _runningCount++;
                child.Start();
            }
        }

        protected override void DoStop()
        {
            Assert.True(_runningCount + _succeededCount + _failedCount == _childrenCount);

            foreach (Node child in Children)
            {
                if (child.IsActive)
                {
                    child.Stop();
                }
            }
        }

        protected override void DoChildStopped(Node child, bool result)
        {
            _runningCount--;
            if (result)
            {
                _succeededCount++;
            }
            else
            {
                _failedCount++;
            }
            _childrenResults[child] = result;

            bool allChildrenStarted = _runningCount + _succeededCount + _failedCount == _childrenCount;
            if (allChildrenStarted)
            {
                if (_runningCount == 0)
                {
                    if (!_childrenAborted) // if children got aborted because rule was evaluated previously, we don't want to override the successState 
                    {
                        if (_failurePolicy == Policy.One && _failedCount > 0)
                        {
                            _successState = false;
                        }
                        else if (_successPolicy == Policy.One && _succeededCount > 0)
                        {
                            _successState = true;
                        }
                        else if (_successPolicy == Policy.All && _succeededCount == _childrenCount)
                        {
                            _successState = true;
                        }
                        else
                        {
                            _successState = false;
                        }
                    }
                    Stopped(_successState);
                }
                else if (!_childrenAborted)
                {
                    Assert.False(_succeededCount == _childrenCount);
                    Assert.False(_failedCount == _childrenCount);

                    if (_failurePolicy == Policy.One && _failedCount > 0/* && waitForPendingChildrenRule != Wait.ON_FAILURE && waitForPendingChildrenRule != Wait.BOTH*/)
                    {
                        _successState = false;
                        _childrenAborted = true;
                    }
                    else if (_successPolicy == Policy.One && _succeededCount > 0/* && waitForPendingChildrenRule != Wait.ON_SUCCESS && waitForPendingChildrenRule != Wait.BOTH*/)
                    {
                        _successState = true;
                        _childrenAborted = true;
                    }

                    if (_childrenAborted)
                    {
                        foreach (Node currentChild in Children)
                        {
                            if (currentChild.IsActive)
                            {
                                currentChild.Stop();
                            }
                        }
                    }
                }
            }
        }

        public override void StopLowerPriorityChildrenForChild(Node abortForChild, bool immediateRestart)
        {
            if (immediateRestart)
            {
                Assert.False(abortForChild.IsActive);
                if (_childrenResults[abortForChild])
                {
                    _succeededCount--;
                }
                else
                {
                    _failedCount--;
                }
                _runningCount++;
                abortForChild.Start();
            }
            else
            {
                throw new Exception("On Parallel Nodes all children have the same priority, thus the method does nothing if you pass false to 'immediateRestart'!");
            }
        }
    }
}
using System.Collections;
using Sandbox.Diagnostics;

namespace NPBehave
{
    public class RandomSequence : Composite
    {
        static System.Random _rng = new System.Random();


#if DEBUG
        static public void DebugSetSeed( int seed )
        {
            _rng = new System.Random( seed );
        }
#endif

        private int _currentIndex = -1;
        private int[] _randomizedOrder;

        public RandomSequence(params Node[] children) : base("Random Sequence", children)
        {
            _randomizedOrder = new int[children.Length];
            for (int i = 0; i < Children.Length; i++)
            {
                _randomizedOrder[i] = i;
            }
        }

        protected override void DoStart()
        {
            foreach (Node child in Children)
            {
                Assert.AreEqual(child.CurrentState, State.Inactive);
            }

            _currentIndex = -1;

            // Shuffling
            int n = _randomizedOrder.Length;
            while (n > 1)
            {
                int k = _rng.Next(n--);
                (_randomizedOrder[n], _randomizedOrder[k]) = (_randomizedOrder[k], _randomizedOrder[n]);
            }

            ProcessChildren();
        }

        protected override void DoStop()
        {
            Children[_randomizedOrder[_currentIndex]].Stop();
        }


        protected override void DoChildStopped(Node child, bool result)
        {
            if (result)
            {
                ProcessChildren();
            }
            else
            {
                Stopped(false);
            }
        }

        private void ProcessChildren()
        {
            if (++_currentIndex < Children.Length)
            {
                if (IsStopRequested)
                {
                    Stopped(false);
                }
                else
                {
                    Children[_randomizedOrder[_currentIndex]].Start();
                }
            }
            else
            {
                Stopped(true);
            }
        }

        public override void StopLowerPriorityChildrenForChild(Node abortForChild, bool immediateRestart)
        {
            int indexForChild = 0;
            bool found = false;
            foreach (Node currentChild in Children)
            {
                if (currentChild == abortForChild)
                {
                    found = true;
                }
                else if (!found)
                {
                    indexForChild++;
                }
                else if (found && currentChild.IsActive)
                {
                    if (immediateRestart)
                    {
                        _currentIndex = indexForChild - 1;
                    }
                    else
                    {
                        _currentIndex = Children.Length;
                    }
                    currentChild.Stop();
                    break;
                }
            }
        }

        public override string ToString()
        {
            return $"{base.ToString()}[{_currentIndex}]";
        }
    }
}
namespace NPBehave
{
    public class Succeeder : Decorator
    {
        public Succeeder(Node decoratee) : base("Succeeder", decoratee)
        {
        }

        protected override void DoStart()
        {
            Decoratee.Start();
        }

        protected override void DoStop()
        {
            Decoratee.Stop();
        }

        protected override void DoChildStopped(Node child, bool result)
        {
            Stopped(true);
        }
    }
}
using System;

namespace NPBehave
{
    public class Exception : System.Exception
    {
        public Exception(string message) : base(message)
        {
        }
    }
}
namespace NPBehave
{
    public class Repeater : Decorator
    {
        private int _loopCount = -1;
        private int _currentLoop;

        /// <param name="loopCount">number of times to execute the decoratee. Set to -1 to repeat forever, be careful with endless loops!</param>
        /// <param name="decoratee">Decorated Node</param>
        public Repeater(int loopCount, Node decoratee) : base("Repeater", decoratee)
        {
            _loopCount = loopCount;
        }

        /// <param name="decoratee">Decorated Node, repeated forever</param>
        public Repeater(Node decoratee) : base("Repeater", decoratee)
        {
        }

        protected override void DoStart()
        {
            if (_loopCount != 0)
            {
                _currentLoop = 0;
                Decoratee.Start();
            }
            else
            {
                Stopped(true);
            }
        }

        protected override void DoStop()
        {
            Clock.RemoveTimer(RestartDecoratee);
            
            if (Decoratee.IsActive)
            {
                Decoratee.Stop();
            }
            else
            {
                Stopped(false);
            }
        }

        protected override void DoChildStopped(Node child, bool result)
        {
            if (result)
            {
                if (IsStopRequested || (_loopCount > 0 && ++_currentLoop >= _loopCount))
                {
                    Stopped(true);
                }
                else
                {
                    Clock.AddTimer(0, 0, RestartDecoratee);
                }
            }
            else
            {
                Stopped(false);
            }
        }

        protected void RestartDecoratee()
        {
            Decoratee.Start();
        }
    }
}
global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
global using Microsoft.AspNetCore.Components; 
global using Microsoft.AspNetCore.Components.Rendering;
global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System;

namespace Duccsoft;

/// <summary>
/// Provides a handy asynchronous wrapper for loading a VideoPlayer and waiting
/// until its video and audio are both loaded.
/// </summary>
public class AsyncVideoLoader
{
	public AsyncVideoLoader() 
	{
		_videoPlayer = new VideoPlayer();
	}

	public AsyncVideoLoader( VideoPlayer player )
	{
		_videoPlayer = player ?? new VideoPlayer();
	}

	public bool IsLoading { get; private set; }

	private VideoPlayer _videoPlayer;
	private Action _onLoaded;
	private Action _onAudioReady;

	public async Task<VideoPlayer> LoadFromUrl( string url, CancellationToken cancelToken = default )
	{
		void Play( VideoPlayer player ) => player.Play( url );

		await Load( Play, cancelToken );
		return _videoPlayer;
	}

	public async Task<VideoPlayer> LoadFromFile( BaseFileSystem fileSystem, string path, CancellationToken cancelToken )
	{
		void Play( VideoPlayer player ) => player.Play( fileSystem, path );

		await Load( Play, cancelToken );
		return _videoPlayer;
	}

	private async Task Load( Action<VideoPlayer> playAction, CancellationToken cancelToken = default )
	{
		// Attempting to play a video from a thread would throw an exception.
		await GameTask.MainThread( cancelToken );

		if ( IsLoading )
		{
			throw new InvalidOperationException( "Another video was already being loaded. Check IsLoading or create a new instance of AsyncVideoLoader." );
		}

		IsLoading = true;

		bool videoLoaded = false;
		bool audioLoaded = false;

		// Assign private members instead of named methods to the invocation lists of the
		// VideoPlayer delegates to break reference equality between runs.
		_onLoaded = () => videoLoaded = true;
		_onAudioReady = () => audioLoaded = true;

		_videoPlayer.OnLoaded = _onLoaded;
		_videoPlayer.OnAudioReady = _onAudioReady;

		playAction?.Invoke( _videoPlayer );

		// Non-blocking spin until video and audio are loaded.
		while ( !videoLoaded || !audioLoaded )
		{
			// If OnLoaded or OnAudioReady are changed externally before we're finished
			// loading, the video will likely never load. Abort to avoid spinning forever.
			var callbacksChanged = _onLoaded != _videoPlayer.OnLoaded || _onAudioReady != _videoPlayer.OnAudioReady;
			if ( callbacksChanged || cancelToken.IsCancellationRequested )
			{
				IsLoading = false;
				return;
			}

			await GameTask.Yield();
		}

		IsLoading = false;
	}
}
global using Microsoft.AspNetCore.Components; 
global using Microsoft.AspNetCore.Components.Rendering;
using System.Text.Json;
using SboxMcp.Server;
using Xunit;

namespace SboxMcp.Tests;

public class ProtocolTests
{
	[Fact]
	public void Parse_request_with_id_and_params()
	{
		var req = JsonRpcRequest.Parse( """{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"x"}}""" );

		Assert.False( req.IsNotification );
		Assert.Equal( 7, req.Id.Value.GetInt32() );
		Assert.Equal( "tools/call", req.Method );
		Assert.Equal( "x", req.Params.Value.GetProperty( "name" ).GetString() );
	}

	[Fact]
	public void Parse_notification_has_no_id()
	{
		var req = JsonRpcRequest.Parse( """{"jsonrpc":"2.0","method":"notifications/initialized"}""" );

		Assert.True( req.IsNotification );
		Assert.Equal( "notifications/initialized", req.Method );
	}

	[Fact]
	public void Parse_invalid_json_throws()
	{
		Assert.Throws<JsonRpcParseException>( () => JsonRpcRequest.Parse( "{nope" ) );
	}

	[Fact]
	public void Parse_missing_method_throws()
	{
		Assert.Throws<JsonRpcParseException>( () => JsonRpcRequest.Parse( """{"jsonrpc":"2.0","id":1}""" ) );
	}

	[Fact]
	public void Writer_result_emits_envelope()
	{
		var id = JsonDocument.Parse( "3" ).RootElement;
		var json = JsonRpcWriter.Result( id, new { protocolVersion = "2025-06-18" } );
		var doc = JsonDocument.Parse( json ).RootElement;

		Assert.Equal( "2.0", doc.GetProperty( "jsonrpc" ).GetString() );
		Assert.Equal( 3, doc.GetProperty( "id" ).GetInt32() );
		Assert.Equal( "2025-06-18", doc.GetProperty( "result" ).GetProperty( "protocolVersion" ).GetString() );
	}

	[Fact]
	public void Writer_error_emits_code_and_message()
	{
		var json = JsonRpcWriter.Error( null, JsonRpcError.MethodNotFound, "no such method" );
		var doc = JsonDocument.Parse( json ).RootElement;

		Assert.Equal( JsonValueKind.Null, JsonKind( doc, "id" ) );
		Assert.Equal( -32601, doc.GetProperty( "error" ).GetProperty( "code" ).GetInt32() );
		Assert.Equal( "no such method", doc.GetProperty( "error" ).GetProperty( "message" ).GetString() );
	}

	[Fact]
	public void Records_serialize_camel_case()
	{
		var schema = JsonDocument.Parse( """{"type":"object"}""" ).RootElement;
		var json = JsonRpcWriter.Result( null,
			McpResults.ToolsList( new[] { new McpToolDescriptor( "a_tool", "does things", schema ) } ) );
		var doc = JsonDocument.Parse( json ).RootElement;
		var tool = doc.GetProperty( "result" ).GetProperty( "tools" )[0];

		Assert.Equal( "a_tool", tool.GetProperty( "name" ).GetString() );
		Assert.Equal( "does things", tool.GetProperty( "description" ).GetString() );
		Assert.Equal( "object", tool.GetProperty( "inputSchema" ).GetProperty( "type" ).GetString() );
	}

	[Fact]
	public void Version_negotiation()
	{
		// only 2025-06-18 is supported (older revisions require JSON-RPC batching)
		Assert.Equal( "2025-06-18", McpVersion.Negotiate( "2025-06-18" ) );
		Assert.Equal( "2025-06-18", McpVersion.Negotiate( "2025-03-26" ) );
		Assert.Equal( "2025-06-18", McpVersion.Negotiate( null ) );
	}

	[Fact]
	public void Null_id_is_rejected()
	{
		Assert.Throws<JsonRpcParseException>( () =>
			JsonRpcRequest.Parse( """{"jsonrpc":"2.0","id":null,"method":"ping"}""" ) );
	}

	[Fact]
	public void Text_content_shape()
	{
		var json = JsonRpcWriter.Result( null, McpResults.TextContent( "hello", isError: true ) );
		var result = JsonDocument.Parse( json ).RootElement.GetProperty( "result" );

		Assert.Equal( "text", result.GetProperty( "content" )[0].GetProperty( "type" ).GetString() );
		Assert.Equal( "hello", result.GetProperty( "content" )[0].GetProperty( "text" ).GetString() );
		Assert.True( result.GetProperty( "isError" ).GetBoolean() );
	}

	[Fact]
	public void Image_content_shape()
	{
		var json = JsonRpcWriter.Result( null, McpResults.ImageContent( "QUJD", "a screenshot" ) );
		var content = JsonDocument.Parse( json ).RootElement.GetProperty( "result" ).GetProperty( "content" );

		Assert.Equal( "image", content[0].GetProperty( "type" ).GetString() );
		Assert.Equal( "QUJD", content[0].GetProperty( "data" ).GetString() );
		Assert.Equal( "image/png", content[0].GetProperty( "mimeType" ).GetString() );
		Assert.Equal( "a screenshot", content[1].GetProperty( "text" ).GetString() );
	}

	static JsonValueKind JsonKind( JsonElement el, string prop ) =>
		el.TryGetProperty( prop, out var v ) ? v.ValueKind : JsonValueKind.Undefined;
}
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Maths;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Cleanup;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>Tunables for the grounded-foot stance recalibration pass.</summary>
public sealed class FootGroundAlignOptions
{
    /// <summary>
    /// Dead zone (degrees): measured stance offsets at or below this are genuine planted
    /// articulation (heel-roll bias, natural lean — measured 2–4° on well-rested rigs and
    /// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.
    /// Only offsets beyond it are clearly rest-pose artifacts (measured 12–25° on the
    /// repro rig) and get recalibrated.
    /// </summary>
    public float MinCorrectionDeg { get; set; } = 8f;

    /// <summary>
    /// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE
    /// for the offset measurement. Plants beyond this are not standing on the sole (crawls,
    /// kneels, prone contact — measured 60–90° there) and are excluded; genuine rest-pose
    /// stance artifacts measure well below it (largest seen: 27°).
    /// </summary>
    public float MaxStanceDeviationDeg { get; set; } = 35f;
}

/// <summary>Per-foot results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignFootReport
{
    /// <summary>Plants that contributed to the stance measurement.</summary>
    public int StancePlants { get; set; }

    /// <summary>Plants excluded as non-stance (mean sole deviation beyond
    /// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/>).</summary>
    public int SkippedPlants { get; set; }

    /// <summary>Measured planted sole offset from the ground plane, degrees (0 when no
    /// stance plants exist).</summary>
    public float MeasuredOffsetDeg { get; set; }

    /// <summary>Foot correction applied to every frame, degrees (0 = inside the dead zone,
    /// nothing changed).</summary>
    public float AppliedFootDeg { get; set; }

    /// <summary>Toe correction applied to every frame, degrees.</summary>
    public float AppliedToeDeg { get; set; }
}

/// <summary>Results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignReport
{
    /// <summary>Left-foot results.</summary>
    public required FootGroundAlignFootReport Left { get; init; }

    /// <summary>Right-foot results.</summary>
    public required FootGroundAlignFootReport Right { get; init; }
}

/// <summary>
/// Grounded-foot stance recalibration: measures how far the foot's SOLE sits from the ground
/// plane while planted, and — when that offset is clearly a rest-pose artifact — rotates it
/// out with one constant per foot, applied to every frame of the clip.
/// </summary>
/// <remarks>
/// <para><b>Why a cleanup pass.</b> The solver transfers feet as rest-relative deltas
/// (<see cref="Solve.RoleTransferMode.CharacterDeltaFromRest"/>), so the target keeps its own
/// ankle anatomy — correct whenever the source's rest pose is a flat-footed stance (the delta
/// is then "deviation from standing"). Some rigs ship a NON-stance rest (measured: an
/// Auto-Rig-Pro export whose rest foot sits 12–25° from its planted stance), and that constant
/// offset rides into every frame of the replay — planted feet hover toe-down/heel-up. What a
/// stance actually looks like is animation evidence (planted phases), which a per-frame
/// solver cannot see, so the recalibration lives here.</para>
/// <para><b>Measurement.</b> Per foot: over every planted frame, the sole normal = rest up
/// carried by the foot's world delta from the target bind rest (whose feet stand on the
/// ground by construction); plants whose own mean normal sits beyond
/// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/> are excluded (crawl/kneel/prone
/// contact is not a stance). The pooled mean normal's deviation from up is the stance
/// offset.</para>
/// <para><b>Correction.</b> Offsets inside <see cref="FootGroundAlignOptions.MinCorrectionDeg"/>
/// are genuine articulation — nothing is changed (well-rested rigs and same-rig round trips
/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the
/// pooled normal back to up (pitch+roll only — yaw/toe-out is pose and follows the source)
/// premultiplies the foot's world rotation on EVERY frame: a rest artifact is constant, so
/// the fix is too — within-plant heel-roll, swing styling and frame-to-frame continuity are
/// preserved exactly, and no blending is needed. The toe then receives its own residual
/// constant measured on top of the corrected foot (it neither double-rotates with the foot
/// fix nor inherits the source toe's own rest artifact). Corrections rotate bones about
/// their own joints: ankle positions are untouched, so the pass composes freely with the
/// <see cref="FootPlant"/> position pinning (which preserves foot world rotations).</para>
/// <para><b>Plant intervals come from the caller</b> (the pipeline detects them on the
/// SOURCE clip via <see cref="FootPlant.DetectPlantIntervals"/> — ground truth, immune to
/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned
/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass
/// only when the source's normalized rest is implausible as a flat stance (toe at/above
/// ankle level or asymmetric feet — see <c>Retargeter.GroundAlignFeet</c>); on plausible
/// stance rests the solver's rest-relative transfer is already faithful and planted-sole
/// deviations are genuine articulation (boxing stances, heel rolls) that must not be
/// flattened.</para>
/// </remarks>
public static class FootGroundAlign
{
    /// <summary>Measures planted stance offsets and recalibrates feet whose offset is a
    /// rest-pose artifact; returns what was measured and done.</summary>
    /// <param name="frames">Per-frame local transforms (skeleton bone order); modified in place.</param>
    /// <param name="skeleton">Bone hierarchy the frames are expressed against; its bind rest
    /// is the flat-stance reference.</param>
    /// <param name="left">Left leg chain bone indices.</param>
    /// <param name="right">Right leg chain bone indices.</param>
    /// <param name="up">World up direction of the clip's space.</param>
    /// <param name="leftPlants">Left-foot plant intervals (frame indices into
    /// <paramref name="frames"/>; out-of-range parts are clamped/ignored).</param>
    /// <param name="rightPlants">Right-foot plant intervals.</param>
    /// <param name="options">Tunables; defaults used when null.</param>
    public static FootGroundAlignReport Apply(
        List<XForm[]> frames,
        SkeletonModel skeleton,
        FootChain left,
        FootChain right,
        Vector3 up,
        IReadOnlyList<FrameRange> leftPlants,
        IReadOnlyList<FrameRange> rightPlants,
        FootGroundAlignOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(frames);
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(left);
        ArgumentNullException.ThrowIfNull(right);
        ArgumentNullException.ThrowIfNull(leftPlants);
        ArgumentNullException.ThrowIfNull(rightPlants);

        options ??= new FootGroundAlignOptions();
        var report = new FootGroundAlignReport
        {
            Left = new FootGroundAlignFootReport(),
            Right = new FootGroundAlignFootReport(),
        };
        if (frames.Count == 0 || up.LengthSquared() < 1e-12f)
            return report;
        up = Vector3.Normalize(up);

        RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);
        RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);
        return report;
    }

    private static void RecalibrateFoot(
        List<XForm[]> frames, SkeletonModel skeleton, FootChain chain, Vector3 up,
        IReadOnlyList<FrameRange> plants, FootGroundAlignOptions options,
        FootGroundAlignFootReport report)
    {
        int n = frames.Count;
        var foot = chain.Ankle;
        var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);
        var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);

        // ---- measurement: pooled planted sole normal over the stance plants ----
        var pooled = Vector3.Zero;
        foreach (var plant in plants)
        {
            int start = Math.Max(plant.Start, 0);
            int end = Math.Min(plant.End, n - 1);
            if (start > end)
                continue;

            var plantSum = Vector3.Zero;
            for (int f = start; f <= end; f++)
            {
                var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;
                plantSum += Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));
            }
            if (plantSum.LengthSquared() < 1e-8f
                || Vector3.Dot(Vector3.Normalize(plantSum), up) < maxStanceCos)
            {
                report.SkippedPlants++; // not standing on the sole — crawl/kneel/toe contact
                continue;
            }
            report.StancePlants++;
            pooled += plantSum; // frame-count-weighted: longer stances dominate
        }
        if (pooled.LengthSquared() < 1e-8f)
            return;
        pooled = Vector3.Normalize(pooled);

        var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);
        report.MeasuredOffsetDeg = offsetDeg;
        if (offsetDeg <= options.MinCorrectionDeg)
            return; // genuine planted articulation — leave the transfer byte-faithful

        // ---- correction: one constant per foot, every frame ----
        var footFix = MathQ.FromTo(pooled, up);
        report.AppliedFootDeg = offsetDeg;

        // Toe residual measured on top of the corrected foot, same dead zone.
        var toeFix = Quaternion.Identity;
        if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
        {
            var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);
            var toePooled = Vector3.Zero;
            foreach (var plant in plants)
            {
                int start = Math.Max(plant.Start, 0);
                int end = Math.Min(plant.End, n - 1);
                for (int f = start; f <= end && f >= 0; f++)
                {
                    var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;
                    toePooled += Vector3.Transform(
                        up, MathQ.Normalize(footFix * toeRot * restToeRotInv));
                }
            }
            if (toePooled.LengthSquared() > 1e-8f)
            {
                toePooled = Vector3.Normalize(toePooled);
                var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);
                if (toeDeg > options.MinCorrectionDeg && Vector3.Dot(toePooled, up) >= maxStanceCos)
                {
                    toeFix = MathQ.FromTo(toePooled, up);
                    report.AppliedToeDeg = toeDeg;
                }
            }
        }

        for (int f = 0; f < n; f++)
            CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);
    }

    /// <summary>Premultiplies the foot's world rotation by the constant fix (the joint
    /// position is untouched — the rotation pivots the foot about its own head), then gives
    /// the toe its own residual on top of the corrected foot.</summary>
    private static void CorrectFrame(
        XForm[] locals, SkeletonModel skeleton, FootChain chain,
        Quaternion footFix, Quaternion toeFix)
    {
        var foot = chain.Ankle;
        var parent = skeleton[foot].ParentIndex;
        var parentRot = parent < 0
            ? Quaternion.Identity
            : FkUtil.BoneWorld(locals, skeleton, parent).Rot;

        var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);
        var newFootWorld = MathQ.Normalize(footFix * footWorld);
        locals[foot] = new XForm(
            locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));

        if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
        {
            // Desired toe world = toeFix ∘ footFix ∘ original world; re-derive its local
            // against the corrected foot so it does not double-rotate with the foot fix.
            var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);
            var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);
            locals[toe] = new XForm(
                locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));
        }
    }
}
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Solve;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Dl;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>The z-normalization statistics shipped with the SAME checkpoint
/// (<c>ms_dict</c>): per-feature mean/std applied to every input except contact.</summary>
public sealed class SameStats
{
    internal float[] LoM, LoS, GoM, GoS, QM, QS, PM, PS, RM, RS, PvM, PvS, QvM, QvS, PprevM, PprevS;

    /// <summary>Reads the 16 <c>ms.*</c> arrays from a parsed weight blob.</summary>
    public SameStats(SameWeights weights)
    {
        ArgumentNullException.ThrowIfNull(weights);
        LoM = weights.Stat("lo_m"); LoS = weights.Stat("lo_s");
        GoM = weights.Stat("go_m"); GoS = weights.Stat("go_s");
        QM = weights.Stat("q_m"); QS = weights.Stat("q_s");
        PM = weights.Stat("p_m"); PS = weights.Stat("p_s");
        RM = weights.Stat("r_m"); RS = weights.Stat("r_s");
        PvM = weights.Stat("pv_m"); PvS = weights.Stat("pv_s");
        QvM = weights.Stat("qv_m"); QvS = weights.Stat("qv_s");
        PprevM = weights.Stat("pprev_m"); PprevS = weights.Stat("pprev_s");
    }
}

/// <summary>A batched per-frame source graph ready for <see cref="SameModel.Encode"/>.</summary>
public sealed class SameSourceGraph
{
    /// <summary>Normalized node features, flat [FrameCount·JointCount × 32].</summary>
    public required float[] X { get; init; }

    /// <summary>Edge sources (bidirectional + self-loops, all frames).</summary>
    public required int[] EdgeSrc { get; init; }

    /// <summary>Edge destinations.</summary>
    public required int[] EdgeDst { get; init; }

    /// <summary>Frame id per node.</summary>
    public required int[] Batch { get; init; }

    /// <summary>Number of feature frames (matches the clip's frame count in production
    /// mode; native frames − 2 in golden-parity mode).</summary>
    public required int FrameCount { get; init; }

    /// <summary>Graph joints per frame (hips subtree + end joints).</summary>
    public required int JointCount { get; init; }

    /// <summary>Graph node names within one frame (bone names; synthesized leaf tips get
    /// a <c>_end</c> suffix). For diagnostics and parity tests.</summary>
    public required string[] JointNames { get; init; }
}

/// <summary>
/// Source-side feature pipeline of the SAME port (FEASIBILITY.md "C# port work list"
/// steps 1–5): skeleton normalization, cm/Y-up/+Z-facing alignment, per-frame
/// q/p/r/pv/qv/pprev/c features in the root-facing frame, z-normalization, and the
/// bidirectional+self-loop edge list.
/// </summary>
/// <remarks>
/// <para><b>Skeleton normalization without an intermediate skeleton.</b> SAME's
/// <c>motion_normalize</c> rebuilds the rig with identity rest-local rotations and
/// re-expresses every frame against it. Algebraically the normalized motion's world
/// rotations are exactly the world-space deltas from the T-pose,
/// <c>Ĝ(j,t) = G(j,t) · G_tpose(j)⁻¹</c>, its local rotations are
/// <c>Ĝ(parent)⁻¹ · Ĝ(j)</c>, and its world positions equal the original world positions
/// — so this port computes the features directly from FK world transforms, no rebuilt
/// skeleton needed (verified against the Python pipeline by the golden-vector tests).</para>
/// <para><b>T-pose reference.</b> SAME consumes the source clip's first frame as the
/// reference; production keeps that convention but emits one feature frame per clip frame
/// (the sequence is computed over [f0, f0…fN−1] with f0 doubling as the reference — see
/// <see cref="TposeReference"/> for why the rest-pose alternative measurably loses).
/// Golden-parity mode replicates Python's frame accounting exactly (frame 0 = reference,
/// frame 1 dropped).</para>
/// <para><b>Alignment.</b> Features assume cm (guaranteed by the importers), Y-up and
/// rest facing +Z with +X to the character's left. The source is rotated by a world
/// alignment derived from the rig's rest geometry (<see cref="CharacterFrame"/> via the
/// mapping when computable, else the file's axis metadata), snapped to the nearest whole
/// axis permutation (an exact-axis rig must map to the identity — the rest-geometry tilt
/// of a few degrees otherwise leaks into every feature), and shifted so the lowest joint
/// over the clip sits on the ground plane.</para>
/// <para><b>Graph.</b> Nodes are the hips subtree (hips = mapped Hips role, else the
/// shallowest branch bone) in skeleton order — hips is always node 0, which is where the
/// root feature row lives — plus one synthesized end joint per childless leaf (BVH End
/// Sites already import as <c>_end</c> bones and are used as-is; FBX leaves get a
/// half-length continuation of their parent segment).</para>
/// </remarks>
public static class SameFeatures
{
    /// <summary>How the T-pose reference (skeleton normalization + lo/go features) is chosen.</summary>
    public enum TposeReference
    {
        /// <summary>The clip's own first frame — SAME's native convention and the
        /// production default. Empirically the pretrained checkpoint tracks arms FAR
        /// better against the clip's first frame than against a synthesized true T-pose,
        /// even though its training references are T-poses (measured on the fixture clip:
        /// mean role cosine vs the geometric solver 0.94 first-frame vs 0.57 rest-pose,
        /// hands flipping negative — reproduced identically in the Python reference
        /// pipeline, so it is a property of the checkpoint, not of this port).</summary>
        FirstFrame,

        /// <summary>Synthesize the reference from the skeleton's rest pose (the
        /// FEASIBILITY suggestion; kept for experiments — see above for why it lost).</summary>
        RestPose,
    }

    /// <summary>Options for <see cref="BuildSourceGraph"/>; defaults are production mode.</summary>
    public sealed class SourceOptions
    {
        /// <summary>T-pose reference choice (see <see cref="TposeReference"/>).</summary>
        public TposeReference Reference { get; init; } = TposeReference.FirstFrame;

        /// <summary>SAME's native frame accounting: the first frame is consumed as the
        /// reference and the next dropped for its undefined velocity, so the output has
        /// two frames fewer than the clip. Golden-parity tests only — production emits
        /// one feature frame per clip frame (the first frame doubles as the reference
        /// and gets zero velocity).</summary>
        public bool NativeFrameDrop { get; init; }

        /// <summary>Apply the rest-geometry world alignment (Y-up, +Z facing). Disabled
        /// only by golden-parity tests (Python applies none).</summary>
        public bool Align { get; init; } = true;

        /// <summary>Ground both the T-pose reference and the animation: the T-pose is
        /// shifted so its lowest joint sits at height 0 (a BVH rest pose has its root at
        /// the origin and would otherwise put the hips on the floor), and the animation is
        /// shifted by its own lowest joint height over the clip (no-op for the usual
        /// authored-ground-at-0 data). Disabled only by golden-parity tests (the Python
        /// reference consumes data as authored).</summary>
        public bool GroundShift { get; init; } = true;
    }

    private const float ContactHeightCm = 5f;
    private const float ContactSpeedMps = 0.4f;
    private const float VelocityFps = 30f;

    /// <summary>
    /// Builds the batched source graph for one clip: graph selection, alignment, per-frame
    /// features, normalization, edges.
    /// </summary>
    /// <param name="scene">Imported source (cm, native axes).</param>
    /// <param name="clipIndex">Clip to encode.</param>
    /// <param name="map">Source mapping; used only for hips identification and the
    /// rest-geometry alignment (the model itself is skeleton-agnostic). May be sparse —
    /// heuristics cover missing roles.</param>
    /// <param name="stats">Normalization statistics.</param>
    /// <param name="options">Null = production mode.</param>
    public static SameSourceGraph BuildSourceGraph(
        SourceScene scene, int clipIndex, MappingResult? map, SameStats stats, SourceOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(scene);
        ArgumentNullException.ThrowIfNull(stats);
        options ??= new SourceOptions();
        if (clipIndex < 0 || clipIndex >= scene.Clips.Count)
            throw new ArgumentOutOfRangeException(nameof(clipIndex));
        var clip = scene.Clips[clipIndex];
        if (clip.FrameCount < 1)
            throw new ArgumentException("Clip has no frames.", nameof(clipIndex));
        if (options.NativeFrameDrop && clip.FrameCount < 3)
            throw new ArgumentException("Native frame accounting needs at least 3 frames.", nameof(options));

        var skeleton = scene.Skeleton;
        var hips = FindHips(skeleton, map);
        var nodes = GraphNodes.Build(skeleton, hips);

        var align = options.Align ? ComputeAlignment(skeleton, map, scene) : Quaternion.Identity;

        // T-pose reference world transforms (aligned), grounded on its own lowest joint
        // (a BVH rest pose has the root at the origin — ungrounded, its hips would sit on
        // the floor and every height-bearing feature would be wrong).
        var tposeLocals = options.Reference == TposeReference.RestPose
            ? Pose.Rest(skeleton).Locals
            : clip.Frames[0];
        var tposeWorld = AlignedWorld(skeleton, tposeLocals, align, nodes);
        if (options.GroundShift)
            ShiftToGround(tposeWorld.Pos);

        // The pose sequence the features run over; features are emitted for seq[1..].
        var seq = new List<XForm[]>();
        if (options.NativeFrameDrop)
        {
            for (var f = 1; f < clip.FrameCount; f++)
                seq.Add(clip.Frames[f]);
        }
        else
        {
            seq.Add(clip.Frames[0]); // duplicated: gives the real first frame zero velocity
            for (var f = 0; f < clip.FrameCount; f++)
                seq.Add(clip.Frames[f]);
        }

        var frames = seq.Count - 1;
        var j = nodes.Count;

        // Pass 0: aligned world transforms; ground the whole clip on its lowest joint.
        var worlds = new AlignedFrame[seq.Count];
        for (var t = 0; t < seq.Count; t++)
            worlds[t] = AlignedWorld(skeleton, seq[t], align, nodes);
        if (options.GroundShift)
        {
            var ground = float.PositiveInfinity;
            foreach (var world in worlds)
            {
                foreach (var p in world.Pos)
                    ground = MathF.Min(ground, p.Y);
            }
            if (float.IsFinite(ground) && ground != 0f)
            {
                foreach (var world in worlds)
                {
                    for (var i = 0; i < j; i++)
                        world.Pos[i].Y -= ground;
                }
            }
        }

        // Pass 1: normalized-skeleton local rotations + facing per frame.
        var localRots = new Quaternion[seq.Count][]; // facing-adjusted at the root row
        var facing = new (float Yaw, Vector3 Pos)[seq.Count];
        for (var t = 0; t < seq.Count; t++)
        {
            var world = worlds[t];

            // Normalized-skeleton world rotations: world delta from the T-pose.
            var normWorld = new Quaternion[j];
            for (var i = 0; i < j; i++)
                normWorld[i] = MathQ.Normalize(world.Rot[i] * Quaternion.Conjugate(tposeWorld.Rot[i]));

            // Root facing: yaw (about +Y) of the normalized root rotation, at the root's
            // ground-plane position.
            var yaw = YawAngle(normWorld[0]);
            facing[t] = (yaw, new Vector3(world.Pos[0].X, 0f, world.Pos[0].Z));

            // Normalized-skeleton local rotations; root premultiplied by the inverse facing.
            var locals = new Quaternion[j];
            locals[0] = MathQ.Normalize(Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw) * normWorld[0]);
            for (var i = 1; i < j; i++)
            {
                locals[i] = MathQ.Normalize(
                    Quaternion.Conjugate(normWorld[nodes.Parent[i]]) * normWorld[i]);
            }
            localRots[t] = locals;
        }

        // Pass 2: feature rows.
        var x = new float[frames * j * SameModel.InputDim];
        for (var t = 1; t < seq.Count; t++)
        {
            var f = t - 1;
            var (yaw, fpos) = facing[t];
            var invFacing = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw);
            var (yawPrev, fposPrev) = facing[t - 1];
            var invFacingPrev = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yawPrev);

            // r: facing delta (dθ, dx, dz) + absolute root height.
            var dTheta = WrapPi(yaw - yawPrev);
            var dPlanar = Vector3.Transform(fpos - fposPrev, invFacingPrev);
            var rootHeight = worlds[t].Pos[0].Y;

            for (var i = 0; i < j; i++)
            {
                var row = (f * j + i) * SameModel.InputDim;
                var col = 0;

                // ---- skel: lo, go (tiled per frame) -------------------------------------
                Vector3 lo, go;
                if (i == 0)
                {
                    lo = new Vector3(0f, tposeWorld.Pos[0].Y, 0f);
                    go = lo;
                }
                else
                {
                    lo = tposeWorld.Pos[i] - tposeWorld.Pos[nodes.Parent[i]];
                    go = tposeWorld.Pos[i] - new Vector3(tposeWorld.Pos[0].X, 0f, tposeWorld.Pos[0].Z);
                }
                WriteNorm3(x, row, ref col, lo, stats.LoM, stats.LoS);
                WriteNorm3(x, row, ref col, go, stats.GoM, stats.GoS);

                // ---- q ------------------------------------------------------------------
                WriteNorm6(x, row, ref col, SixD(localRots[t][i]), stats.QM, stats.QS);

                // ---- p (facing-frame-relative global position) --------------------------
                var p = Vector3.Transform(worlds[t].Pos[i] - fpos, invFacing);
                WriteNorm3(x, row, ref col, p, stats.PM, stats.PS);

                // ---- r (root row only; other rows are the mean → zeros after norm) ------
                if (i == 0)
                {
                    x[row + col++] = (dTheta - stats.RM[0]) / stats.RS[0];
                    x[row + col++] = (dPlanar.X - stats.RM[1]) / stats.RS[1];
                    x[row + col++] = (dPlanar.Z - stats.RM[2]) / stats.RS[2];
                    x[row + col++] = (rootHeight - stats.RM[3]) / stats.RS[3];
                }
                else
                {
                    col += 4; // already zero
                }

                // ---- pv (facing-frame velocity, ×30 fps) ---------------------------------
                var pv = Vector3.Transform(worlds[t].Pos[i] - worlds[t - 1].Pos[i], invFacing) * VelocityFps;
                WriteNorm3(x, row, ref col, pv, stats.PvM, stats.PvS);

                // ---- qv (local rotation delta) -------------------------------------------
                var qv = MathQ.Normalize(Quaternion.Conjugate(localRots[t - 1][i]) * localRots[t][i]);
                WriteNorm6(x, row, ref col, SixD(qv), stats.QvM, stats.QvS);

                // ---- pprev (previous position in the CURRENT facing frame) ---------------
                var pprev = Vector3.Transform(worlds[t - 1].Pos[i] - fpos, invFacing);
                WriteNorm3(x, row, ref col, pprev, stats.PprevM, stats.PprevS);

                // ---- c (ground contact; not normalized) -----------------------------------
                var speedMps = (worlds[t].Pos[i] - worlds[t - 1].Pos[i]).Length() * VelocityFps / 100f;
                x[row + col] = worlds[t].Pos[i].Y < ContactHeightCm && speedMps < ContactSpeedMps ? 1f : 0f;
            }
        }

        var (edgeSrc, edgeDst) = BuildEdges(nodes.Parent, frames);
        var batch = new int[frames * j];
        for (var f = 0; f < frames; f++)
        {
            for (var i = 0; i < j; i++)
                batch[f * j + i] = f;
        }

        AssertFinite(x, "SAME source features");
        return new SameSourceGraph
        {
            X = x,
            EdgeSrc = edgeSrc,
            EdgeDst = edgeDst,
            Batch = batch,
            FrameCount = frames,
            JointCount = j,
            JointNames = nodes.Names,
        };
    }

    // ================================================================ graph topology

    /// <summary>The per-frame graph node set: hips-subtree bones in skeleton order
    /// (hips first) plus synthesized end joints for childless leaves.</summary>
    internal sealed class GraphNodes
    {
        /// <summary>Skeleton bone index per node; -1 for synthesized end joints.</summary>
        public required int[] Bone { get; init; }

        /// <summary>Graph-parent node index; -1 for the root (node 0).</summary>
        public required int[] Parent { get; init; }

        /// <summary>For synthesized end joints: the rest-local offset from the leaf bone
        /// (zero vector for real bones).</summary>
        public required Vector3[] EndOffset { get; init; }

        public required string[] Names { get; init; }

        public int Count => Bone.Length;

        public static GraphNodes Build(SkeletonModel skeleton, int hips)
        {
            // Hips subtree, skeleton order (parents precede children, hips first).
            var inSubtree = new bool[skeleton.Count];
            inSubtree[hips] = true;
            var bones = new List<int> { hips };
            for (var i = hips + 1; i < skeleton.Count; i++)
            {
                var parent = skeleton[i].ParentIndex;
                if (parent >= 0 && inSubtree[parent])
                {
                    inSubtree[i] = true;
                    bones.Add(i);
                }
            }

            var nodeOfBone = new Dictionary<int, int>(bones.Count);
            for (var n = 0; n < bones.Count; n++)
                nodeOfBone[bones[n]] = n;

            var hasChild = new bool[skeleton.Count];
            foreach (var b in bones)
            {
                var parent = skeleton[b].ParentIndex;
                if (parent >= 0 && inSubtree[parent])
                    hasChild[parent] = true;
            }

            var bone = new List<int>(bones);
            var parentNode = new List<int>(bones.Count);
            var endOffset = new List<Vector3>(bones.Count);
            var names = new List<string>(bones.Count);
            foreach (var b in bones)
            {
                var p = skeleton[b].ParentIndex;
                parentNode.Add(b == hips ? -1 : nodeOfBone[p]);
                endOffset.Add(Vector3.Zero);
                names.Add(skeleton[b].Name);
            }

            // Synthesized end joints: leaves with no children anywhere in the skeleton.
            // BVH End Sites already import as real `_end`/`_End` bones and ARE the end
            // joints — no tip on a tip. The tip continues the parent→leaf segment at half
            // length — a neutral stand-in for the unknown bone tail (FBX carries none).
            foreach (var b in bones)
            {
                if (hasChild[b]
                    || skeleton[b].Name.EndsWith("_end", StringComparison.OrdinalIgnoreCase))
                    continue;
                var p = skeleton[b].ParentIndex;
                var segment = p >= 0
                    ? skeleton.RestWorld[b].Pos - skeleton.RestWorld[p].Pos
                    : Vector3.Zero;
                var tip = segment.Length() > 1e-4f ? segment * 0.5f : new Vector3(0f, 2f, 0f);
                // Express in the leaf's rest-local frame (applied via the leaf's world rot).
                var local = Vector3.Transform(tip, Quaternion.Conjugate(skeleton.RestWorld[b].Rot));
                bone.Add(-1);
                parentNode.Add(nodeOfBone[b]);
                endOffset.Add(local);
                names.Add(skeleton[b].Name + "_end");
            }

            return new GraphNodes
            {
                Bone = bone.ToArray(),
                Parent = parentNode.ToArray(),
                EndOffset = endOffset.ToArray(),
                Names = names.ToArray(),
            };
        }
    }

    /// <summary>Aligned world transforms of the graph nodes for one pose.</summary>
    internal readonly struct AlignedFrame
    {
        public required Vector3[] Pos { get; init; }
        public required Quaternion[] Rot { get; init; }
    }

    private static AlignedFrame AlignedWorld(
        SkeletonModel skeleton, XForm[] locals, Quaternion align, GraphNodes nodes)
    {
        var world = new Pose(locals).ToWorld(skeleton);
        var pos = new Vector3[nodes.Count];
        var rot = new Quaternion[nodes.Count];
        for (var n = 0; n < nodes.Count; n++)
        {
            XForm w;
            if (nodes.Bone[n] >= 0)
            {
                w = world[nodes.Bone[n]];
            }
            else
            {
                // Synthesized end joint: rides its leaf bone (identity local rotation).
                var leaf = world[nodes.Bone[nodes.Parent[n]]];
                w = new XForm(leaf.TransformPoint(nodes.EndOffset[n]), leaf.Rot);
            }
            pos[n] = Vector3.Transform(w.Pos, align);
            rot[n] = MathQ.Normalize(align * w.Rot);
        }
        return new AlignedFrame { Pos = pos, Rot = rot };
    }

    /// <summary>Bidirectional parent↔child pairs plus one self-loop per node, replicated
    /// per frame with node indices offset.</summary>
    internal static (int[] Src, int[] Dst) BuildEdges(int[] parent, int frames)
    {
        var j = parent.Length;
        var nonRoot = 0;
        for (var i = 0; i < j; i++)
        {
            if (parent[i] >= 0)
                nonRoot++;
        }
        var perFrame = nonRoot * 2 + j;
        var src = new int[perFrame * frames];
        var dst = new int[perFrame * frames];
        var e = 0;
        for (var f = 0; f < frames; f++)
        {
            var offset = f * j;
            for (var i = 0; i < j; i++)
            {
                if (parent[i] < 0)
                    continue;
                src[e] = offset + parent[i];
                dst[e] = offset + i;
                e++;
                src[e] = offset + i;
                dst[e] = offset + parent[i];
                e++;
            }
            for (var i = 0; i < j; i++)
            {
                src[e] = offset + i;
                dst[e] = offset + i;
                e++;
            }
        }
        return (src, dst);
    }

    // ================================================================ alignment + hips

    /// <summary>Mapped Hips role when available, else the shallowest bone with two or more
    /// children (the hips of any humanoid: the legs/spine branch point).</summary>
    internal static int FindHips(SkeletonModel skeleton, MappingResult? map)
    {
        if (map is not null && map.RoleToBone.TryGetValue(BoneRole.Hips, out var mapped)
            && mapped >= 0 && mapped < skeleton.Count)
            return mapped;

        var childCount = new int[skeleton.Count];
        for (var i = 0; i < skeleton.Count; i++)
        {
            if (skeleton[i].ParentIndex >= 0)
                childCount[skeleton[i].ParentIndex]++;
        }

        var best = -1;
        var bestDepth = int.MaxValue;
        for (var i = 0; i < skeleton.Count; i++)
        {
            if (childCount[i] < 2)
                continue;
            var depth = 0;
            for (var a = skeleton[i].ParentIndex; a >= 0; a = skeleton[a].ParentIndex)
                depth++;
            if (depth < bestDepth)
            {
                best = i;
                bestDepth = depth;
            }
        }
        return best >= 0 ? best : 0;
    }

    /// <summary>
    /// World rotation taking the rig into the canonical SAME frame (X = character left,
    /// Y = up, Z = facing): rest-geometry character frame when computable from the mapping,
    /// else the file's recorded axis conventions.
    /// </summary>
    internal static Quaternion ComputeAlignment(SkeletonModel skeleton, MappingResult? map, SourceScene? scene)
    {
        if (map is not null)
        {
            try
            {
                var frame = CharacterFrame.Compute(skeleton, map, skeleton.RestWorld);
                return AlignFromBasis(frame.Lateral, frame.Up, frame.Forward);
            }
            catch (ArgumentException)
            {
                // fall through to axis metadata
            }
        }

        if (scene is not null)
        {
            var up = AxisVector(scene.UpAxis, scene.UpAxisSign);
            var forward = AxisVector(scene.FrontAxis, scene.FrontAxisSign);
            if (MathF.Abs(Vector3.Dot(up, forward)) < 0.5f)
                return AlignFromBasis(Vector3.Cross(up, forward), up, forward);
        }

        return Quaternion.Identity;
    }

    /// <summary>
    /// Rotation mapping the given (left, up, forward) world directions onto (+X, +Y, +Z),
    /// snapped to the nearest whole axis permutation when one is unambiguous: rigs authored
    /// on exact axes (BVH Y-up/+Z, the s&amp;box rig, Z-up FBX) must map by an exact
    /// quarter-turn — the few degrees of rest-geometry tilt (shoulders not exactly above
    /// hips) otherwise leak into every feature and measurably cost accuracy.
    /// </summary>
    internal static Quaternion AlignFromBasis(Vector3 left, Vector3 up, Vector3 forward)
    {
        var l = SnapAxis(left);
        var u = SnapAxis(up);
        var f = SnapAxis(forward);
        if (MathF.Abs(Vector3.Dot(l, u)) > 0.5f || MathF.Abs(Vector3.Dot(l, f)) > 0.5f
            || MathF.Abs(Vector3.Dot(u, f)) > 0.5f)
        {
            // Genuinely oblique rig: keep the exact (orthonormalized) directions.
            l = Vector3.Normalize(left);
            u = Vector3.Normalize(up - l * Vector3.Dot(up, l));
            f = Vector3.Cross(l, u);
        }

        // Row-major with rows = basis images maps +X→left, +Y→up, +Z→forward
        // (System.Numerics row-vector convention); the alignment is its inverse.
        var m = new Matrix4x4(
            l.X, l.Y, l.Z, 0f,
            u.X, u.Y, u.Z, 0f,
            f.X, f.Y, f.Z, 0f,
            0f, 0f, 0f, 1f);
        return Quaternion.Conjugate(MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m)));
    }

    private static Vector3 SnapAxis(Vector3 v)
    {
        var ax = MathF.Abs(v.X);
        var ay = MathF.Abs(v.Y);
        var az = MathF.Abs(v.Z);
        if (ax >= ay && ax >= az)
            return new Vector3(MathF.Sign(v.X), 0f, 0f);
        if (ay >= az)
            return new Vector3(0f, MathF.Sign(v.Y), 0f);
        return new Vector3(0f, 0f, MathF.Sign(v.Z));
    }

    private static Vector3 AxisVector(int axis, int sign) => axis switch
    {
        0 => new Vector3(sign, 0f, 0f),
        2 => new Vector3(0f, 0f, sign),
        _ => new Vector3(0f, sign, 0f),
    };

    // ================================================================ small math

    /// <summary>The yaw (rotation about +Y) closest to <paramref name="q"/> — fairmotion's
    /// <c>Q_closest(q, identity, +Y)</c>, reproduced exactly for parity.</summary>
    internal static float YawAngle(Quaternion q)
    {
        var alpha = Math.Atan2(q.W, q.Y);
        var theta1 = -2.0 * alpha + Math.PI;
        var theta2 = -2.0 * alpha - Math.PI;
        var d1 = q.Y * Math.Sin(theta1 * 0.5) + q.W * Math.Cos(theta1 * 0.5);
        var d2 = q.Y * Math.Sin(theta2 * 0.5) + q.W * Math.Cos(theta2 * 0.5);
        return (float)(d1 > d2 ? theta1 : theta2);
    }

    private static void ShiftToGround(Vector3[] positions)
    {
        var ground = float.PositiveInfinity;
        foreach (var p in positions)
            ground = MathF.Min(ground, p.Y);
        if (!float.IsFinite(ground) || ground == 0f)
            return;
        for (var i = 0; i < positions.Length; i++)
            positions[i].Y -= ground;
    }

    internal static float WrapPi(float angle)
    {
        while (angle > MathF.PI)
            angle -= 2f * MathF.PI;
        while (angle < -MathF.PI)
            angle += 2f * MathF.PI;
        return angle;
    }

    /// <summary>6D rotation representation: the first two columns of the rotation matrix
    /// (<c>R·e_x</c> then <c>R·e_y</c>).</summary>
    internal static (Vector3 C0, Vector3 C1) SixD(Quaternion q)
        => (Vector3.Transform(Vector3.UnitX, q), Vector3.Transform(Vector3.UnitY, q));

    private static void WriteNorm3(float[] x, int row, ref int col, Vector3 v, float[] m, float[] s)
    {
        x[row + col++] = (v.X - m[0]) / s[0];
        x[row + col++] = (v.Y - m[1]) / s[1];
        x[row + col++] = (v.Z - m[2]) / s[2];
    }

    private static void WriteNorm6(float[] x, int row, ref int col, (Vector3 C0, Vector3 C1) sixD, float[] m, float[] s)
    {
        x[row + col++] = (sixD.C0.X - m[0]) / s[0];
        x[row + col++] = (sixD.C0.Y - m[1]) / s[1];
        x[row + col++] = (sixD.C0.Z - m[2]) / s[2];
        x[row + col++] = (sixD.C1.X - m[3]) / s[3];
        x[row + col++] = (sixD.C1.Y - m[4]) / s[4];
        x[row + col++] = (sixD.C1.Z - m[5]) / s[5];
    }

    internal static void AssertFinite(float[] values, string what)
    {
        foreach (var v in values)
        {
            if (!float.IsFinite(v))
                throw new InvalidOperationException($"{what} contain non-finite values.");
        }
    }
}
using System.Collections.Generic;

namespace HumanoidRetargeter.Mapping;

/// <summary>
/// Built-in preset profiles, embedded as C# data (the same data is written to
/// <c>Assets/humanoid_retargeter/profiles/*.json</c> by a regenerate-and-diff test so the
/// shipped JSON can never drift from the code).
/// </summary>
public static class ProfileLibrary
{
    /// <summary>Mixamo / Adobe rigs: <c>mixamorig[N]:</c> namespace, <c>LeftArm</c> /
    /// <c>LeftForeArm</c> / <c>LeftHandIndex1..3</c> style names.</summary>
    public static Profile Mixamo { get; } = BuildMixamo();

    /// <summary>
    /// Reallusion ActorCore / AccuRig / Character Creator rigs (<c>CC_Base_*</c>).
    /// Empirical notes from <c>research/rig_actorcore.json</c>:
    /// <list type="bullet">
    /// <item><c>CC_Base_Hip</c> is the parent of BOTH <c>CC_Base_Pelvis</c> (leg branch) and
    /// <c>CC_Base_Waist</c> (spine branch), i.e. the LCA of legs+spine and the true animated
    /// hips root → it carries <see cref="BoneRole.Hips"/>; <c>CC_Base_Pelvis</c> is a
    /// leg-branch intermediate and stays unmapped.</item>
    /// <item>The neck chain is <c>CC_Base_NeckTwist01 → CC_Base_NeckTwist02 → CC_Base_Head</c>;
    /// despite the name, <c>NeckTwist01</c> IS the neck bone (there is no plain
    /// <c>CC_Base_Neck</c>), so it is the <see cref="BoneRole.Neck"/> alias. NeckTwist02 is
    /// left unmapped. All other Twist/ShareBone helpers are excluded (no aliases).</item>
    /// <item><c>CC_Base_L_ToeBase</c> is the toe role; the co-located
    /// <c>CC_Base_L_ToeBaseShareBone</c> is a helper and must never be mapped.</item>
    /// </list>
    /// </summary>
    public static Profile ActorCoreCc { get; } = BuildActorCoreCc();

    /// <summary>Unreal Engine mannequin (UE4/UE5): <c>pelvis</c>, <c>spine_01..05</c>,
    /// <c>clavicle_l</c>, <c>thumb_01_l</c>, UE5 <c>*_metacarpal_*</c>; <c>*_twist_*</c>
    /// bones have no aliases and are never mapped.</summary>
    public static Profile UeMannequin { get; } = BuildUeMannequin();

    /// <summary>Rokoko / Xsens style BVH rigs: plain <c>Hips</c>/<c>Spine..Spine4</c>/<c>
    /// LeftArm|LeftUpperArm</c> name variants, usually no fingers.</summary>
    public static Profile RokokoBvh { get; } = BuildRokokoBvh();

    /// <summary>
    /// SMPL body model family (AMASS exports, Meshcapade FBX rigs). Joint names per the
    /// published model (vchoutas/smplx <c>joint_names.py</c>, Meshcapade wiki):
    /// <c>pelvis</c>, sided <c>hip→knee→ankle→foot</c> legs (the "hip" joint IS the thigh;
    /// "ankle" is the foot, "foot" is the toe region) and <c>collar→shoulder→elbow→wrist</c>
    /// arms ("shoulder" is the upper arm, "wrist" is the hand; the <c>hand</c> joint is a
    /// finger stub and stays unmapped). Both spellings occur in the wild: <c>left_hip</c>
    /// (model joints) and <c>L_Hip</c> with gendered FBX prefixes <c>m_avg_</c>/<c>f_avg_</c>
    /// (SMPL Unity/FBX rigs). No fingers — that is SMPL-X (<see cref="SmplX"/>), kept as a
    /// separate preset so a finger-less SMPL rig still reaches full optional coverage.
    /// </summary>
    public static Profile Smpl { get; } = BuildSmpl(withFingers: false);

    /// <summary>
    /// SMPL-X: the SMPL body joints (<see cref="Smpl"/>) plus articulated hands —
    /// <c>left_thumb1..3</c>/<c>left_index1..3</c>-style finger joints per
    /// vchoutas/smplx <c>joint_names.py</c> (jaw/eye joints carry no humanoid role).
    /// Evaluated before <see cref="Smpl"/> so it wins the tie on SMPL-X rigs (both score
    /// the body fully; only this one maps the fingers).
    /// </summary>
    public static Profile SmplX { get; } = BuildSmpl(withFingers: true);

    /// <summary>
    /// NVIDIA SOMA uniform-proportion skeleton (SOMA/SEED BVH exports, e.g.
    /// github.com/NVIDIA/soma-retargeter <c>assets/motions/bvh</c>). Mixamo-identical
    /// upper-body and finger names, but: spine is <c>Spine1→Spine2→Chest</c> (no plain
    /// "Spine"), neck is <c>Neck1→Neck2</c>, and the legs are <c>LeftLeg→LeftShin</c> —
    /// SOMA's <c>LeftLeg</c> is the THIGH (mixamo's is the calf), which is exactly why the
    /// mixamo preset must never claim these rigs.
    /// </summary>
    public static Profile SomaBvh { get; } = BuildSomaBvh();

    /// <summary>
    /// Classic BVH / Character-Studio-friendly naming (MotionBuilder "Export BVH to
    /// Character Studio" convention, ACCAD-style mocap BVHs): <c>Hips</c>,
    /// <c>Chest[2..4]</c> spine, arms <c>Collar→Shoulder→Elbow→Wrist</c> (the "Shoulder"
    /// is the upper arm) and legs <c>Hip→Knee→Ankle→Toe</c> (the sided "Hip" is the
    /// thigh). No fingers.
    /// </summary>
    public static Profile ClassicBvh { get; } = BuildClassicBvh();

    /// <summary>
    /// 3ds Max Character Studio Biped rigs: every bone is "&lt;BipedName&gt; &lt;Part&gt;"
    /// where the biped name defaults to <c>Bip01</c> (3ds Max ≤2009) / <c>Bip001</c>
    /// (2010+) per the Autodesk "Naming the Biped" documentation; some exporters mangle
    /// the spaces to underscores (<c>Bip01_L_Thigh</c>), hence the <c>^Bip\d+[ _]</c>
    /// namespace pattern (alias comparison is separator-insensitive, so "L UpperArm" and
    /// "L_UpperArm" normalize identically). Sided bones use a bare mid-name <c>L/R</c>:
    /// <c>L Clavicle→L UpperArm→L Forearm→L Hand</c> arms,
    /// <c>L Thigh→L Calf→L Foot→L Toe0</c> legs. Fingers are numbered chains
    /// <c>L Finger0..4</c> (0 = thumb) with phalanx segments <c>Finger01/Finger02</c>
    /// etc. (MotionBuilder's "3ds Max Biped Template" characterization maps exactly these
    /// names). The COM root <c>Bip01</c> itself, <c>Footsteps</c>, toe segments
    /// <c>Toe01/Toe02</c> and <c>HorseLink</c> carry no aliases and are never mapped.
    /// </summary>
    public static Profile Biped { get; } = BuildBiped();

    /// <summary>
    /// DAZ/Poser classic naming (Poser 4 era figures, DAZ Generation-4 V4/M4, Genesis 1/2,
    /// MakeHuman's "Poser/DAZ names" BVH export — verified against the local
    /// <c>dev/corpus/unknown_rigs/makehuman_cmu_03_03_dazNames.bvh</c>): camel-case bones
    /// with a lower-case <c>l</c>/<c>r</c> side prefix — <c>hip</c> (the translating
    /// root), <c>abdomen[→abdomen2]→chest</c> spine, <c>neck</c>, <c>head</c>,
    /// <c>lCollar→lShldr→lForeArm→lHand</c> arms, <c>lThigh→lShin→lFoot→lToe</c> legs and
    /// <c>lThumb1..3/lIndex1..3/lMid1..3/lRing1..3/lPinky1..3</c> fingers. The
    /// <c>l/rButtock</c> thigh helpers and eye bones carry no aliases and stay unmapped.
    /// DAZ Genesis 3/8/9 renamed the skeleton (<c>abdomenLower</c>, <c>lShldrBend</c>, …)
    /// and is NOT covered by this preset.
    /// </summary>
    public static Profile DazPoser { get; } = BuildDazPoser();

    /// <summary>
    /// Blender Rigify human rigs, per the metarig definition in the rigify add-on
    /// (<c>rigify/metarigs/human.py</c>) and the Blender manual's basic.human reference:
    /// the spine chain is <c>spine→spine.001..spine.006</c> where <c>spine</c> IS the
    /// pelvis/hips bone (it sits at the pelvis and parents the thighs), spine.001–003 are
    /// the torso, spine.004/005 the two neck bones (004 carries <see cref="BoneRole.Neck"/>,
    /// 005 stays unmapped — same policy as ActorCore's NeckTwist02) and spine.006 is the
    /// head. Limbs: <c>shoulder.L→upper_arm.L→forearm.L→hand.L</c>,
    /// <c>thigh.L→shin.L→foot.L→toe.L</c>; fingers <c>thumb.01.L..03.L</c> and
    /// <c>f_index/f_middle/f_ring/f_pinky.01.L..03.L</c>. The <c>^DEF-</c> namespace
    /// pattern also matches rigify's generated deform skeleton (<c>DEF-spine.001</c>,
    /// <c>DEF-upper_arm.L</c>, …); the segmented deform twins (<c>DEF-upper_arm.L.001</c>),
    /// <c>palm.*</c>, <c>pelvis.L/R</c>, <c>heel.02.L</c>, face bones and the generated
    /// ORG-/MCH-/control bones have no aliases and are never mapped.
    /// </summary>
    public static Profile Rigify { get; } = BuildRigify();

    /// <summary>
    /// VRoid Studio / VRM avatars (UniVRM exports): <c>J_Bip_&lt;side&gt;_&lt;Part&gt;</c>
    /// bones where side is <c>C</c> (center), <c>L</c> or <c>R</c> — the standard VRoid
    /// skeleton behind the VRM humanoid spec (vrm-c/vrm-specification, humanoid bone map):
    /// <c>J_Bip_C_Hips/Spine/Chest/UpperChest/Neck/Head</c>,
    /// <c>J_Bip_L_Shoulder→UpperArm→LowerArm→Hand</c>,
    /// <c>J_Bip_L_UpperLeg→LowerLeg→Foot→ToeBase</c>, fingers
    /// <c>J_Bip_L_Thumb1..3/Index1..3/Middle1..3/Ring1..3/Little1..3</c> ("Little" is the
    /// pinky, per the VRM littleProximal/Intermediate/Distal humanoid bones). Secondary
    /// physics/adjust bones (<c>J_Sec_*</c>, <c>J_Adj_*</c>) and the <c>Root</c> bone have
    /// no aliases and are never mapped.
    /// </summary>
    public static Profile Vrm { get; } = BuildVrm();

    /// <summary>
    /// Blender Auto-Rig Pro humanoid FBX exports — bone names verified empirically against
    /// the local user repro <c>dev/corpus/todo/Defenses.fbx</c> (the PunchPerfect family):
    /// <c>.x</c> suffix marks center bones, <c>.l/.r</c> the sides, and the exported limb
    /// deform bones carry the <c>_stretch</c> twin name — <c>root.x</c> is the hips
    /// (under a ground bone <c>root</c>), <c>spine_01.x→spine_02.x→spine_03.x</c>,
    /// <c>neck.x</c>, <c>head.x</c>, arms <c>shoulder.l→arm_stretch.l→forearm_stretch.l→
    /// hand.l</c> (plain "arm", NOT "upperarm"), legs <c>thigh_stretch.l→leg_stretch.l→
    /// foot.l→toes_01.l</c> ("leg" is the calf). Fingers keep Auto-Rig Pro's <c>c_</c>
    /// control prefix on the exported deform chain: <c>c_thumb1.l..3.l</c>,
    /// <c>c_index/c_middle/c_ring/c_pinky1.l..3.l</c>. Leftover finger-tip markers
    /// (<c>mixamorig:LeftHandIndex4</c> in the repro) and <c>root</c> have no aliases.
    /// </summary>
    public static Profile AutoRigPro { get; } = BuildAutoRigPro();

    /// <summary>All built-in presets, in detection order (first wins score ties — see
    /// <see cref="SmplX"/> vs <see cref="Smpl"/>).</summary>
    public static IReadOnlyList<Profile> All { get; } =
        new[]
        {
            Mixamo, ActorCoreCc, UeMannequin, RokokoBvh, SmplX, Smpl, SomaBvh, ClassicBvh,
            Biped, DazPoser, Rigify, Vrm, AutoRigPro,
        };

    // ---------------------------------------------------------------- mixamo

    private static Profile BuildMixamo()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hips" },
            [BoneRole.Spine0] = new[] { "Spine" },
            [BoneRole.Spine1] = new[] { "Spine1" },
            [BoneRole.Spine2] = new[] { "Spine2" },
            [BoneRole.Neck] = new[] { "Neck" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, nameSide) in Sides())
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}UpLeg" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Leg" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}ToeBase" };

            foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
            {
                aliases[Role($"{finger}Prox", roleSide)] = new[] { $"{nameSide}Hand{finger}1" };
                aliases[Role($"{finger}Mid", roleSide)] = new[] { $"{nameSide}Hand{finger}2" };
                aliases[Role($"{finger}Dist", roleSide)] = new[] { $"{nameSide}Hand{finger}3" };
            }
        }
        // Both ':' (FBX namespace) and '_' (namespace mangled by some exporters) forms occur
        // in the wild; some Mixamo downloads ship with no namespace at all, which still
        // matches because the aliases are the bare names.
        return new Profile("mixamo", new[] { "^mixamorig[0-9]*:", "^mixamorig[0-9]*_" }, aliases);
    }

    // ---------------------------------------------------------------- actorcore / cc

    private static Profile BuildActorCoreCc()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hip" },
            [BoneRole.Spine0] = new[] { "Waist" },
            [BoneRole.Spine1] = new[] { "Spine01" },
            [BoneRole.Spine2] = new[] { "Spine02" },
            [BoneRole.Neck] = new[] { "NeckTwist01" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var roleSide in new[] { "L", "R" })
        {
            var nameSide = roleSide; // CC bones use the bare side letter: CC_Base_L_Thigh.
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}_Clavicle" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}_Upperarm" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}_Forearm" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}_Hand" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}_Thigh" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}_Calf" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}_Foot" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}_ToeBase" };

            foreach (var (role, cc) in new[]
            {
                ("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Mid"), ("Ring", "Ring"), ("Pinky", "Pinky"),
            })
            {
                aliases[Role($"{role}Prox", roleSide)] = new[] { $"{nameSide}_{cc}1" };
                aliases[Role($"{role}Mid", roleSide)] = new[] { $"{nameSide}_{cc}2" };
                aliases[Role($"{role}Dist", roleSide)] = new[] { $"{nameSide}_{cc}3" };
            }
        }
        return new Profile("actorcore_cc", new[] { "^CC_Base_" }, aliases);
    }

    // ---------------------------------------------------------------- ue mannequin

    private static Profile BuildUeMannequin()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "pelvis" },
            [BoneRole.Spine0] = new[] { "spine_01" },
            [BoneRole.Spine1] = new[] { "spine_02" },
            [BoneRole.Spine2] = new[] { "spine_03" },
            [BoneRole.Spine3] = new[] { "spine_04" },
            [BoneRole.Spine4] = new[] { "spine_05" },
            [BoneRole.Neck] = new[] { "neck_01" },
            [BoneRole.Head] = new[] { "head" },
        };
        foreach (var (roleSide, s) in new[] { ("L", "l"), ("R", "r") })
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"clavicle_{s}" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"upperarm_{s}" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"lowerarm_{s}" };
            aliases[Role("Hand", roleSide)] = new[] { $"hand_{s}" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"thigh_{s}" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"calf_{s}" };
            aliases[Role("Foot", roleSide)] = new[] { $"foot_{s}" };
            aliases[Role("Toe", roleSide)] = new[] { $"ball_{s}" };

            foreach (var (role, ue) in new[]
            {
                ("Thumb", "thumb"), ("Index", "index"), ("Middle", "middle"), ("Ring", "ring"), ("Pinky", "pinky"),
            })
            {
                // UE5 mannequin adds metacarpals for the four fingers (not the thumb).
                if (role != "Thumb")
                    aliases[Role($"{role}Meta", roleSide)] = new[] { $"{ue}_metacarpal_{s}" };
                aliases[Role($"{role}Prox", roleSide)] = new[] { $"{ue}_01_{s}" };
                aliases[Role($"{role}Mid", roleSide)] = new[] { $"{ue}_02_{s}" };
                aliases[Role($"{role}Dist", roleSide)] = new[] { $"{ue}_03_{s}" };
            }
        }
        return new Profile("ue_mannequin", new string[0], aliases);
    }

    // ---------------------------------------------------------------- rokoko / xsens bvh

    private static Profile BuildRokokoBvh()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hips" },
            // Spine naming varies (Spine, Spine1..Spine4); ordered alias preference plus the
            // used-bone exclusion in the detector shifts the chain up when "Spine" is absent.
            [BoneRole.Spine0] = new[] { "Spine", "Spine1" },
            [BoneRole.Spine1] = new[] { "Spine1", "Spine2" },
            [BoneRole.Spine2] = new[] { "Spine2", "Spine3" },
            [BoneRole.Spine3] = new[] { "Spine3", "Spine4" },
            [BoneRole.Spine4] = new[] { "Spine4" },
            [BoneRole.Neck] = new[] { "Neck", "Neck1" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, nameSide) in Sides())
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder", $"{nameSide}Collar" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm", $"{nameSide}UpperArm" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm", $"{nameSide}LowerArm" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}UpLeg", $"{nameSide}Thigh", $"{nameSide}UpperLeg" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Leg", $"{nameSide}Shin", $"{nameSide}LowerLeg" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}Toe", $"{nameSide}ToeBase" };
        }
        return new Profile("rokoko_bvh", new string[0], aliases);
    }

    // ---------------------------------------------------------------- smpl / smpl-x

    private static Profile BuildSmpl(bool withFingers)
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Pelvis" },
            [BoneRole.Spine0] = new[] { "Spine1" },
            [BoneRole.Spine1] = new[] { "Spine2" },
            [BoneRole.Spine2] = new[] { "Spine3" },
            [BoneRole.Neck] = new[] { "Neck" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, abbr, word) in new[] { ("L", "L", "left"), ("R", "R", "right") })
        {
            // Both documented spellings per role: abbreviated FBX-rig names ("L_Hip") and
            // spelled model joint names ("left_hip"). Comparison is separator-insensitive.
            aliases[Role("Clavicle", roleSide)] = new[] { $"{abbr}_Collar", $"{word}_collar" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{abbr}_Shoulder", $"{word}_shoulder" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{abbr}_Elbow", $"{word}_elbow" };
            aliases[Role("Hand", roleSide)] = new[] { $"{abbr}_Wrist", $"{word}_wrist" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{abbr}_Hip", $"{word}_hip" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{abbr}_Knee", $"{word}_knee" };
            aliases[Role("Foot", roleSide)] = new[] { $"{abbr}_Ankle", $"{word}_ankle" };
            aliases[Role("Toe", roleSide)] = new[] { $"{abbr}_Foot", $"{word}_foot" };

            if (!withFingers)
                continue;

            // SMPL-X finger joints (left_index1..3 etc., per vchoutas/smplx joint_names.py).
            foreach (var finger in new[] { "thumb", "index", "middle", "ring", "pinky" })
            {
                var name = char.ToUpperInvariant(finger[0]) + finger[1..];
                aliases[Role($"{name}Prox", roleSide)] = new[] { $"{word}_{finger}1" };
                aliases[Role($"{name}Mid", roleSide)] = new[] { $"{word}_{finger}2" };
                aliases[Role($"{name}Dist", roleSide)] = new[] { $"{word}_{finger}3" };
            }
        }
        // Gendered SMPL FBX rigs prefix every bone (m_avg_L_Hip, f_avg_Pelvis).
        return new Profile(withFingers ? "smpl_x" : "smpl", new[] { "^m_avg_", "^f_avg_" }, aliases);
    }

    // ---------------------------------------------------------------- nvidia soma bvh

    private static Profile BuildSomaBvh()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hips" },
            [BoneRole.Spine0] = new[] { "Spine1" },
            [BoneRole.Spine1] = new[] { "Spine2" },
            [BoneRole.Spine2] = new[] { "Chest" },
            [BoneRole.Neck] = new[] { "Neck1" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, nameSide) in Sides())
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Shoulder" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Arm" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}ForeArm" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Hand" };
            // SOMA's "Leg" is the thigh, "Shin" the calf — the decisive difference from
            // mixamo, where "Leg" is the calf under "UpLeg".
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}Leg" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Shin" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Foot" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}ToeBase" };

            // Mixamo-style finger names; segment 4 ("LeftHandIndex4") and the *End markers
            // carry no role.
            foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
            {
                aliases[Role($"{finger}Prox", roleSide)] = new[] { $"{nameSide}Hand{finger}1" };
                aliases[Role($"{finger}Mid", roleSide)] = new[] { $"{nameSide}Hand{finger}2" };
                aliases[Role($"{finger}Dist", roleSide)] = new[] { $"{nameSide}Hand{finger}3" };
            }
        }
        return new Profile("soma_bvh", new string[0], aliases);
    }

    // ---------------------------------------------------------------- classic bvh

    private static Profile BuildClassicBvh()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Hips" },
            [BoneRole.Spine0] = new[] { "Chest" },
            [BoneRole.Spine1] = new[] { "Chest2" },
            [BoneRole.Spine2] = new[] { "Chest3" },
            [BoneRole.Spine3] = new[] { "Chest4" },
            [BoneRole.Neck] = new[] { "Neck" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var (roleSide, nameSide) in Sides())
        {
            aliases[Role("Clavicle", roleSide)] = new[] { $"{nameSide}Collar" };
            aliases[Role("UpperArm", roleSide)] = new[] { $"{nameSide}Shoulder" };
            aliases[Role("LowerArm", roleSide)] = new[] { $"{nameSide}Elbow" };
            aliases[Role("Hand", roleSide)] = new[] { $"{nameSide}Wrist" };
            aliases[Role("UpperLeg", roleSide)] = new[] { $"{nameSide}Hip" };
            aliases[Role("LowerLeg", roleSide)] = new[] { $"{nameSide}Knee" };
            aliases[Role("Foot", roleSide)] = new[] { $"{nameSide}Ankle" };
            aliases[Role("Toe", roleSide)] = new[] { $"{nameSide}Toe" };
        }
        return new Profile("classic_bvh", new string[0], aliases);
    }

    // ---------------------------------------------------------------- 3ds max biped

    private static Profile BuildBiped()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "Pelvis" },
            [BoneRole.Spine0] = new[] { "Spine" },
            [BoneRole.Spine1] = new[] { "Spine1" },
            [BoneRole.Spine2] = new[] { "Spine2" },
            [BoneRole.Spine3] = new[] { "Spine3" },
            [BoneRole.Neck] = new[] { "Neck" },
            [BoneRole.Head] = new[] { "Head" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            aliases[Role("Clavicle", s)] = new[] { $"{s} Clavicle" };
            aliases[Role("UpperArm", s)] = new[] { $"{s} UpperArm" };
            aliases[Role("LowerArm", s)] = new[] { $"{s} Forearm" };
            aliases[Role("Hand", s)] = new[] { $"{s} Hand" };
            aliases[Role("UpperLeg", s)] = new[] { $"{s} Thigh" };
            aliases[Role("LowerLeg", s)] = new[] { $"{s} Calf" };
            aliases[Role("Foot", s)] = new[] { $"{s} Foot" };
            aliases[Role("Toe", s)] = new[] { $"{s} Toe0" };

            // Numbered finger chains: Finger0 is the thumb; segment names append the
            // phalanx digit (Finger0 → Finger01 → Finger02, Finger1 → Finger11 → ...).
            foreach (var (finger, n) in new[]
            {
                ("Thumb", 0), ("Index", 1), ("Middle", 2), ("Ring", 3), ("Pinky", 4),
            })
            {
                aliases[Role($"{finger}Prox", s)] = new[] { $"{s} Finger{n}" };
                aliases[Role($"{finger}Mid", s)] = new[] { $"{s} Finger{n}1" };
                aliases[Role($"{finger}Dist", s)] = new[] { $"{s} Finger{n}2" };
            }
        }
        // "Bip01 "/"Bip001 " biped-name prefix; underscore form covers exporters that
        // mangle the spaces ("Bip01_L_Thigh"). The bare COM root "Bip01" is untouched by
        // the pattern (no trailing separator) and has no alias.
        return new Profile("biped", new[] { @"^Bip\d+[ _]" }, aliases);
    }

    // ---------------------------------------------------------------- daz / poser

    private static Profile BuildDazPoser()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "hip" },
            [BoneRole.Spine0] = new[] { "abdomen" },
            // Poser classic / DAZ Gen4 spine is abdomen→chest; DAZ Genesis 1/2 inserts
            // abdomen2. Ordered preference + used-bone exclusion handles both: without
            // abdomen2 the chest falls back to Spine1 and Spine2 stays unmapped.
            [BoneRole.Spine1] = new[] { "abdomen2", "chest" },
            [BoneRole.Spine2] = new[] { "chest" },
            [BoneRole.Neck] = new[] { "neck" },
            [BoneRole.Head] = new[] { "head" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            var p = s == "L" ? "l" : "r"; // lower-case side prefix: lShldr, rThigh
            aliases[Role("Clavicle", s)] = new[] { $"{p}Collar" };
            aliases[Role("UpperArm", s)] = new[] { $"{p}Shldr" };
            aliases[Role("LowerArm", s)] = new[] { $"{p}ForeArm" };
            aliases[Role("Hand", s)] = new[] { $"{p}Hand" };
            aliases[Role("UpperLeg", s)] = new[] { $"{p}Thigh" };
            aliases[Role("LowerLeg", s)] = new[] { $"{p}Shin" };
            aliases[Role("Foot", s)] = new[] { $"{p}Foot" };
            aliases[Role("Toe", s)] = new[] { $"{p}Toe" };

            foreach (var (role, daz) in new[]
            {
                ("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Mid"), ("Ring", "Ring"), ("Pinky", "Pinky"),
            })
            {
                aliases[Role($"{role}Prox", s)] = new[] { $"{p}{daz}1" };
                aliases[Role($"{role}Mid", s)] = new[] { $"{p}{daz}2" };
                aliases[Role($"{role}Dist", s)] = new[] { $"{p}{daz}3" };
            }
        }
        return new Profile("daz_poser", new string[0], aliases);
    }

    // ---------------------------------------------------------------- blender rigify

    private static Profile BuildRigify()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            // rigify's "spine" bone sits AT the pelvis and parents both thighs — it is
            // the hips, not a spine link (rigify/metarigs/human.py).
            [BoneRole.Hips] = new[] { "spine" },
            [BoneRole.Spine0] = new[] { "spine.001" },
            [BoneRole.Spine1] = new[] { "spine.002" },
            [BoneRole.Spine2] = new[] { "spine.003" },
            // spine.004 + spine.005 are the two neck bones, spine.006 the head;
            // spine.005 stays unmapped (same policy as ActorCore's NeckTwist02).
            [BoneRole.Neck] = new[] { "spine.004" },
            [BoneRole.Head] = new[] { "spine.006" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            aliases[Role("Clavicle", s)] = new[] { $"shoulder.{s}" };
            aliases[Role("UpperArm", s)] = new[] { $"upper_arm.{s}" };
            aliases[Role("LowerArm", s)] = new[] { $"forearm.{s}" };
            aliases[Role("Hand", s)] = new[] { $"hand.{s}" };
            aliases[Role("UpperLeg", s)] = new[] { $"thigh.{s}" };
            aliases[Role("LowerLeg", s)] = new[] { $"shin.{s}" };
            aliases[Role("Foot", s)] = new[] { $"foot.{s}" };
            aliases[Role("Toe", s)] = new[] { $"toe.{s}" };

            foreach (var (role, rigify) in new[]
            {
                ("Thumb", "thumb"), ("Index", "f_index"), ("Middle", "f_middle"),
                ("Ring", "f_ring"), ("Pinky", "f_pinky"),
            })
            {
                aliases[Role($"{role}Prox", s)] = new[] { $"{rigify}.01.{s}" };
                aliases[Role($"{role}Mid", s)] = new[] { $"{rigify}.02.{s}" };
                aliases[Role($"{role}Dist", s)] = new[] { $"{rigify}.03.{s}" };
            }
        }
        // The generated deform skeleton prefixes every deform bone with "DEF-"; its
        // segmented limb twins ("DEF-upper_arm.L.001") keep their numeric suffix after
        // stripping and therefore never collide with the whole-bone aliases.
        return new Profile("rigify", new[] { "^DEF-" }, aliases);
    }

    // ---------------------------------------------------------------- vroid / vrm

    private static Profile BuildVrm()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "J_Bip_C_Hips" },
            [BoneRole.Spine0] = new[] { "J_Bip_C_Spine" },
            [BoneRole.Spine1] = new[] { "J_Bip_C_Chest" },
            [BoneRole.Spine2] = new[] { "J_Bip_C_UpperChest" },
            [BoneRole.Neck] = new[] { "J_Bip_C_Neck" },
            [BoneRole.Head] = new[] { "J_Bip_C_Head" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            aliases[Role("Clavicle", s)] = new[] { $"J_Bip_{s}_Shoulder" };
            aliases[Role("UpperArm", s)] = new[] { $"J_Bip_{s}_UpperArm" };
            aliases[Role("LowerArm", s)] = new[] { $"J_Bip_{s}_LowerArm" };
            aliases[Role("Hand", s)] = new[] { $"J_Bip_{s}_Hand" };
            aliases[Role("UpperLeg", s)] = new[] { $"J_Bip_{s}_UpperLeg" };
            aliases[Role("LowerLeg", s)] = new[] { $"J_Bip_{s}_LowerLeg" };
            aliases[Role("Foot", s)] = new[] { $"J_Bip_{s}_Foot" };
            aliases[Role("Toe", s)] = new[] { $"J_Bip_{s}_ToeBase" };

            foreach (var (role, vrm) in new[]
            {
                ("Thumb", "Thumb"), ("Index", "Index"), ("Middle", "Middle"),
                ("Ring", "Ring"), ("Pinky", "Little"),
            })
            {
                aliases[Role($"{role}Prox", s)] = new[] { $"J_Bip_{s}_{vrm}1" };
                aliases[Role($"{role}Mid", s)] = new[] { $"J_Bip_{s}_{vrm}2" };
                aliases[Role($"{role}Dist", s)] = new[] { $"J_Bip_{s}_{vrm}3" };
            }
        }
        return new Profile("vrm", new string[0], aliases);
    }

    // ---------------------------------------------------------------- auto-rig pro

    private static Profile BuildAutoRigPro()
    {
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "root.x" },
            [BoneRole.Spine0] = new[] { "spine_01.x" },
            [BoneRole.Spine1] = new[] { "spine_02.x" },
            [BoneRole.Spine2] = new[] { "spine_03.x" },
            [BoneRole.Neck] = new[] { "neck.x" },
            [BoneRole.Head] = new[] { "head.x" },
        };
        foreach (var s in new[] { "L", "R" })
        {
            var p = s == "L" ? "l" : "r";
            aliases[Role("Clavicle", s)] = new[] { $"shoulder.{p}" };
            aliases[Role("UpperArm", s)] = new[] { $"arm_stretch.{p}" };
            aliases[Role("LowerArm", s)] = new[] { $"forearm_stretch.{p}" };
            aliases[Role("Hand", s)] = new[] { $"hand.{p}" };
            aliases[Role("UpperLeg", s)] = new[] { $"thigh_stretch.{p}" };
            aliases[Role("LowerLeg", s)] = new[] { $"leg_stretch.{p}" };
            aliases[Role("Foot", s)] = new[] { $"foot.{p}" };
            aliases[Role("Toe", s)] = new[] { $"toes_01.{p}" };

            // Exported finger deform bones keep ARP's c_ control prefix (Defenses.fbx).
            foreach (var finger in new[] { "thumb", "index", "middle", "ring", "pinky" })
            {
                var role = char.ToUpperInvariant(finger[0]) + finger[1..];
                aliases[Role($"{role}Prox", s)] = new[] { $"c_{finger}1.{p}" };
                aliases[Role($"{role}Mid", s)] = new[] { $"c_{finger}2.{p}" };
                aliases[Role($"{role}Dist", s)] = new[] { $"c_{finger}3.{p}" };
            }
        }
        return new Profile("auto_rig_pro", new string[0], aliases);
    }

    // ---------------------------------------------------------------- helpers

    private static IEnumerable<(string RoleSide, string NameSide)> Sides()
    {
        yield return ("L", "Left");
        yield return ("R", "Right");
    }

    private static BoneRole Role(string baseName, string side)
        => System.Enum.Parse<BoneRole>(baseName + side);
}
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Target;

namespace HumanoidRetargeter.Solve;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>
/// Mirrors a solved TARGET-space clip across the target character's sagittal plane,
/// producing the left/right-swapped twin of an animation (e.g. a right-foot-lead walk from a
/// left-foot-lead one).
/// </summary>
/// <remarks>
/// <para><b>Mirror plane.</b> The plane through the rig-space origin spanned by the target
/// character's up and forward directions; its normal is the character's LATERAL axis,
/// computed from the target rig's rest geometry via <see cref="CharacterFrame"/> (never
/// hardcoded — an arbitrary target may be authored in any axis convention). When the
/// computed lateral lies on a coordinate axis up to float dirt (&lt; 1e-3 on the other two
/// components — true for every axis-aligned authored rig, including the s&amp;box citizen
/// rigs), it is snapped to that exact axis, which makes every reflection below an EXACT
/// sign-flip in IEEE arithmetic and therefore the whole mirror a bit-exact involution
/// (mirror ∘ mirror == identity, verified by test).</para>
/// <para><b>Math.</b> Let M = I − 2n̂n̂ᵀ be the reflection across the plane with unit normal
/// n̂. A world transform W = (R, t) maps to its mirror image by conjugation:
/// W′ = M̂ ∘ W ∘ M̂ (M̂ is its own inverse), giving rotation R′ = M·R·M and translation
/// t′ = M·t. For a quaternion q = (v, w), M·R·M is the rotation by the SAME angle about the
/// REFLECTED axis with REVERSED sense (a reflection flips orientation), i.e.
/// q′ = (2(n̂·v)n̂ − v, w); with n̂ = +X that is exactly q′ = (x, −y, −z, w), and positions
/// reflect as p′ = p − 2(n̂·p)n̂ = (−pₓ, p_y, p_z).</para>
/// <para><b>Locals, not worlds.</b> Because conjugation is a homomorphism
/// (M̂(AB)M̂ = (M̂AM̂)(M̂BM̂)) and world transforms are products of locals down the
/// hierarchy, mirroring every LOCAL transform and permuting bones by their L↔R partner is
/// exactly equivalent to mirroring the FK worlds — provided the partner permutation is
/// hierarchy-consistent (the partner's parent is the parent's partner), which is validated
/// and holds on structurally symmetric humanoid rigs. This avoids FK→inverse-FK float drift
/// entirely, which is what makes the double-mirror identity bit-exact.</para>
/// <para><b>Pairing.</b> Left/right bones are paired by the rig's canonical role annotations
/// first (UpperArmL ↔ UpperArmR, …); role-less bones (twist helpers, IK bones) fall back to
/// <c>_L</c>/<c>_R</c> name-token pairing (<c>arm_upper_L_twist0</c> ↔
/// <c>arm_upper_R_twist0</c>, <c>foot_L_IK_target</c> ↔ <c>foot_R_IK_target</c>); anything
/// unpaired (center bones: pelvis, spine, neck, head) mirrors in place, which reflects its
/// rotation across the sagittal plane and negates its lateral translation. IK-baked bones
/// should be re-baked from the mirrored body afterwards (<see cref="IkBoneBaker"/>) — the
/// pipeline does exactly that.</para>
/// </remarks>
public static class ClipMirror
{
    /// <summary>Maximum off-axis component magnitude below which the computed lateral axis is
    /// snapped to the exact coordinate axis (authored rigs are axis-aligned; the tiny rest
    /// asymmetries of a real mesh stay far below this).</summary>
    private const float AxisSnapTolerance = 1e-3f;

    /// <summary>
    /// Returns the mirrored copy of <paramref name="frames"/> (one new list, inputs
    /// untouched): per frame, bone i takes the conjugated local transform of its L↔R partner
    /// σ(i). See the class remarks for the math and pairing rules.
    /// </summary>
    /// <param name="frames">Solved per-frame local transforms (target skeleton bone order).</param>
    /// <param name="rig">The target rig (skeleton + roles) the frames belong to.</param>
    /// <exception cref="ArgumentException">Thrown when the rig maps a sided role without its
    /// counterpart, the pairing is not hierarchy-consistent, or the character frame is not
    /// computable — mirroring would silently produce garbage in those cases.</exception>
    public static List<XForm[]> Mirror(List<XForm[]> frames, TargetRig rig)
    {
        ArgumentNullException.ThrowIfNull(frames);
        ArgumentNullException.ThrowIfNull(rig);

        var skeleton = rig.Skeleton;
        var lateral = LateralAxis(rig);
        var pair = BuildPairing(rig);

        var result = new List<XForm[]>(frames.Count);
        foreach (var locals in frames)
        {
            if (locals.Length != skeleton.Count)
                throw new ArgumentException(
                    $"Frame has {locals.Length} bones but the target skeleton has {skeleton.Count}.",
                    nameof(frames));

            var mirrored = new XForm[locals.Length];
            for (var i = 0; i < locals.Length; i++)
            {
                var source = locals[pair[i]];
                mirrored[i] = new XForm(
                    ReflectPoint(source.Pos, lateral),
                    ReflectRotation(source.Rot, lateral));
            }
            result.Add(mirrored);
        }
        return result;
    }

    // ================================================================ mirror plane

    /// <summary>The unit mirror normal: the target character's lateral axis from rest
    /// geometry, snapped to an exact coordinate axis when within tolerance (bit-exact
    /// reflections, see class remarks).</summary>
    private static Vector3 LateralAxis(TargetRig rig)
    {
        Vector3 lateral;
        try
        {
            lateral = CharacterFrame.Compute(
                rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld).Lateral;
        }
        catch (ArgumentException e)
        {
            throw new ArgumentException(
                $"Cannot mirror: target character frame not computable ({e.Message}).", e);
        }

        var a = Vector3.Abs(lateral);
        if (a.Y <= AxisSnapTolerance && a.Z <= AxisSnapTolerance)
            return Vector3.UnitX;
        if (a.X <= AxisSnapTolerance && a.Z <= AxisSnapTolerance)
            return Vector3.UnitY;
        if (a.X <= AxisSnapTolerance && a.Y <= AxisSnapTolerance)
            return Vector3.UnitZ;
        return lateral; // general (non-axis-aligned) rig: exact involution is lost, math is not
    }

    /// <summary>p′ = p − 2(n̂·p)n̂. With a snapped axis this is an exact sign flip of one
    /// component (IEEE subtraction of representable values is exact).</summary>
    private static Vector3 ReflectPoint(Vector3 p, Vector3 n)
        => p - 2f * Vector3.Dot(p, n) * n;

    /// <summary>q′ = (2(n̂·v)n̂ − v, w): the conjugated rotation M·R·M — same angle, axis
    /// reflected, sense reversed. With n̂ = +X this is (x, −y, −z, w). Components are
    /// preserved exactly (no renormalization), keeping the double mirror bit-exact.</summary>
    private static Quaternion ReflectRotation(Quaternion q, Vector3 n)
    {
        var v = new Vector3(q.X, q.Y, q.Z);
        var reflected = 2f * Vector3.Dot(v, n) * n - v;
        return new Quaternion(reflected.X, reflected.Y, reflected.Z, q.W);
    }

    // ================================================================ L↔R pairing

    /// <summary>
    /// σ: bone → mirror partner (identity for center/unpaired bones). Roles pair first;
    /// role-less bones pair by <c>_L</c>/<c>_R</c> name tokens. Validated to be an involution
    /// consistent with the hierarchy (σ(parent(i)) == parent(σ(i))).
    /// </summary>
    private static int[] BuildPairing(TargetRig rig)
    {
        var skeleton = rig.Skeleton;
        var pair = new int[skeleton.Count];
        for (var i = 0; i < pair.Length; i++)
            pair[i] = i;

        for (var i = 0; i < skeleton.Count; i++)
        {
            if (rig.RoleOf(i) is { } role)
            {
                if (MirrorRole(role) is not { } mirroredRole)
                    continue; // center role: mirrors in place
                pair[i] = rig.BoneForRole(mirroredRole)
                    ?? throw new ArgumentException(
                        $"Cannot mirror: target rig maps role {role} ('{skeleton[i].Name}') "
                        + $"but not its counterpart {mirroredRole}.");
            }
            else
            {
                var partnerName = SwapSideTokens(skeleton[i].Name);
                if (partnerName is null)
                    continue; // no side token: center bone
                var partner = skeleton.IndexOf(partnerName);
                if (partner >= 0)
                    pair[i] = partner;
                // No partner bone: leave in place (e.g. an asymmetric prop bone) — its
                // rotation still mirrors across the sagittal plane.
            }
        }

        for (var i = 0; i < pair.Length; i++)
        {
            if (pair[pair[i]] != i)
                throw new ArgumentException(
                    $"Cannot mirror: bone pairing is not symmetric ('{skeleton[i].Name}' → "
                    + $"'{skeleton[pair[i]].Name}' → '{skeleton[pair[pair[i]]].Name}').");

            var parent = skeleton[i].ParentIndex;
            var partnerParent = skeleton[pair[i]].ParentIndex;
            var consistent = parent < 0
                ? partnerParent < 0
                : partnerParent == pair[parent];
            if (!consistent)
                throw new ArgumentException(
                    $"Cannot mirror: left/right pairing is not hierarchy-consistent — "
                    + $"'{skeleton[i].Name}' and partner '{skeleton[pair[i]].Name}' hang under "
                    + "non-mirrored parents.");
        }

        return pair;
    }

    /// <summary>UpperArmL → UpperArmR (and back); null for center roles. Every sided
    /// <see cref="BoneRole"/> ends in <c>L</c>/<c>R</c>; no center role does.</summary>
    private static BoneRole? MirrorRole(BoneRole role)
    {
        var name = role.ToString();
        var mirroredName = name[^1] switch
        {
            'L' => name[..^1] + "R",
            'R' => name[..^1] + "L",
            _ => null,
        };
        return mirroredName is not null && Enum.TryParse<BoneRole>(mirroredName, out var mirrored)
            ? mirrored
            : null;
    }

    /// <summary>Swaps <c>L</c>/<c>R</c> underscore-delimited name tokens
    /// (<c>foot_L_IK_target</c> → <c>foot_R_IK_target</c>); null when the name carries no
    /// side token.</summary>
    private static string? SwapSideTokens(string name)
    {
        var tokens = name.Split('_');
        for (var i = 0; i < tokens.Length; i++)
        {
            tokens[i] = tokens[i] switch
            {
                "L" => "R",
                "R" => "L",
                "l" => "r",
                "r" => "l",
                _ => tokens[i],
            };
        }
        var result = string.Join('_', tokens);
        return string.Equals(result, name, StringComparison.Ordinal) ? null : result;
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;

namespace HumanoidRetargeter.Solve;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>
/// Finger retargeting. Picks one of three strategies per finger chain:
/// <list type="number">
/// <item><b>1:1 absolute copy</b> (via the <c>transferOneToOne</c> callback into
/// <see cref="GeometricSolver"/>'s body path) when the source and target chains are
/// <i>geometrically identical</i> — same mapped role set, same canonical frames, same
/// normalized rest rotations. This is the same-rig round-trip case and is lossless (exact
/// identity, twist included).</item>
/// <item><b>Direction matching</b> when the phalanx counts match ordinally but the rigs
/// differ (the common cross-rig case, e.g. Mixamo Prox/Mid/Dist onto the s&amp;box finger
/// with its extra metacarpal — which keeps its rest local; a source metacarpal's rotation is
/// implicit in the proximal's absolute direction). Each target phalanx is swung — shortest
/// arc, rotation axis ⊥ the finger axis, hence <b>zero twist by construction</b> — so that its
/// segment direction matches the source phalanx's direction in character-frame coordinates
/// exactly. Curl and splay are both captured by the direction; the source's axial twist is
/// dropped (hinge-joint noise; copying it absolutely would read as roll through the
/// inter-phalanx canonical mismatch between rigs, measured up to ~12° on thumbs).</item>
/// <item><b>Proportional redistribution</b> when phalanx counts differ (e.g. a two-phalanx
/// source finger): per-phalanx local curls — swing-twist about the canonical hinge Y of
/// <c>λ_b = C_b⁻¹·(ΔR_prev⁻¹·ΔR_b)·C_b</c> — are summed over the source chain (metacarpal
/// included) and redistributed over the target phalanges proportional to rest segment
/// lengths; splay (metacarpal + proximal, swing-twist about canonical Z) goes 100% to the
/// target proximal; the X-twist residual is dropped.</item>
/// </list>
/// In every mode target world deltas rebuild hierarchically from the solved target hand:
/// <c>ΔR_i = ΔR_{i-1} · (C_i · λ_i · C_i⁻¹)</c>, then <c>W_i = ΔR_i · R_tgtNormRest,i</c>.
/// Instances are per-solve and not thread-safe.
/// </summary>
internal sealed class FingerSolver
{
    /// <summary>Two canonical frames / rest rotations within this angle count as identical
    /// (same-rig detection for the lossless 1:1 path); cross-rig differences are degrees.</summary>
    private const float SameRigToleranceRad = 1e-3f;

    private enum ChainMode
    {
        DirectionMatch,
        Proportional,
    }

    private readonly struct SourcePhalanx
    {
        public required int Slot { get; init; }
        public required Quaternion C { get; init; }
        public required Quaternion CInv { get; init; }
        public required bool TakesSplay { get; init; }
    }

    private readonly struct Recipient
    {
        public required int TgtBone { get; init; }
        public required Quaternion C { get; init; }
        public required Quaternion CInv { get; init; }
        public required Quaternion RestRot { get; init; }
        public required float Weight { get; init; }
        public required bool Splay { get; init; }
    }

    private sealed class Chain
    {
        public required ChainMode Mode { get; init; }
        public required int SrcHandSlot { get; init; }
        public required int TgtHandBone { get; init; }
        public required Quaternion TgtHandNormRestRotInv { get; init; }
        public required SourcePhalanx[] Sources { get; init; }
        public required Recipient[] Recipients { get; init; }
    }

    private readonly List<Chain> _chains;
    private readonly Quaternion _chrSrcInv;
    private readonly Quaternion _chrTgt;

    private FingerSolver(List<Chain> chains, Quaternion chrSrcInv, Quaternion chrTgt)
    {
        _chains = chains;
        _chrSrcInv = chrSrcInv;
        _chrTgt = chrTgt;
    }

    // ---------------------------------------------------------------- role tables

    private static readonly BoneRole[][] ChainRoles = BuildChainRoles();
    private static readonly HashSet<BoneRole> FingerRoleSet = ChainRoles.SelectMany(c => c.Skip(1)).ToHashSet();

    private static BoneRole[][] BuildChainRoles()
    {
        var chains = new List<BoneRole[]>();
        foreach (var side in new[] { "L", "R" })
        {
            foreach (var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" })
            {
                // Element 0 is the hand the chain hangs off; 1.. are Meta/Prox/Mid/Dist.
                chains.Add(new[]
                {
                    Enum.Parse<BoneRole>("Hand" + side),
                    Enum.Parse<BoneRole>(finger + "Meta" + side),
                    Enum.Parse<BoneRole>(finger + "Prox" + side),
                    Enum.Parse<BoneRole>(finger + "Mid" + side),
                    Enum.Parse<BoneRole>(finger + "Dist" + side),
                });
            }
        }
        return chains.ToArray();
    }

    /// <summary>True for the 40 per-finger segment roles (Meta/Prox/Mid/Dist × finger × side).</summary>
    public static bool IsFingerRole(BoneRole role) => FingerRoleSet.Contains(role);

    // ---------------------------------------------------------------- build

    /// <summary>
    /// Builds the per-chain plans. Geometrically identical chains are reported through
    /// <paramref name="transferOneToOne"/> instead of being planned here. Returns null when
    /// every mapped chain took that path (or none is mapped).
    /// </summary>
    public static FingerSolver? Build(
        MappingResult sourceMap,
        CanonicalFrames srcCanon,
        IReadOnlyList<XForm> srcNormRest,
        Func<BoneRole, int?> tgtBoneForRole,
        CanonicalFrames tgtCanon,
        IReadOnlyList<XForm> tgtNormRest,
        Quaternion chrSrcInv,
        Quaternion chrTgt,
        Func<int, int> registerSlot,
        Action<BoneRole> transferOneToOne)
    {
        var chains = new List<Chain>();
        foreach (var chainRoles in ChainRoles)
        {
            var handRole = chainRoles[0];
            var metaRole = chainRoles[1];
            var proxRole = chainRoles[2];
            var segments = chainRoles.Skip(1).ToArray();

            var srcRoles = segments
                .Where(r => sourceMap.RoleToBone.ContainsKey(r) && srcCanon.Has(r))
                .ToArray();
            var tgtRoles = segments
                .Where(r => tgtBoneForRole(r) is not null && tgtCanon.Has(r))
                .ToArray();
            if (srcRoles.Length == 0 || tgtRoles.Length == 0)
                continue;

            if (srcRoles.SequenceEqual(tgtRoles) && ChainsCoincide(
                srcRoles, sourceMap, srcCanon, srcNormRest, tgtBoneForRole, tgtCanon, tgtNormRest))
            {
                foreach (var role in srcRoles)
                    transferOneToOne(role);
                continue;
            }

            var srcPhalanges = srcRoles.Where(r => r != metaRole).ToArray();
            var tgtPhalanges = tgtRoles.Where(r => r != metaRole).ToArray();
            var recipientRoles = tgtPhalanges.Length > 0 ? tgtPhalanges : tgtRoles;
            var mode = srcPhalanges.Length == recipientRoles.Length && srcPhalanges.Length > 0
                ? ChainMode.DirectionMatch
                : ChainMode.Proportional;

            // Direction matching consumes only the non-meta phalanges (the metacarpal's
            // motion is implicit in the proximal's absolute direction); redistribution
            // decomposes every mapped source segment including the metacarpal.
            var sourceRolesUsed = mode == ChainMode.DirectionMatch ? srcPhalanges : srcRoles;
            var sources = sourceRolesUsed.Select(r =>
            {
                var c = srcCanon.WorldFrameOf(r);
                return new SourcePhalanx
                {
                    Slot = registerSlot(sourceMap.RoleToBone[r]),
                    C = c,
                    CInv = Quaternion.Conjugate(c),
                    TakesSplay = r == metaRole || r == proxRole,
                };
            }).ToArray();

            var weights = SegmentWeights(tgtRoles, recipientRoles, tgtBoneForRole, tgtNormRest);
            var recipients = recipientRoles.Select((r, i) =>
            {
                var bone = tgtBoneForRole(r)!.Value;
                var c = tgtCanon.WorldFrameOf(r);
                return new Recipient
                {
                    TgtBone = bone,
                    C = c,
                    CInv = Quaternion.Conjugate(c),
                    RestRot = tgtNormRest[bone].Rot,
                    Weight = weights[i],
                    Splay = i == 0,
                };
            }).ToArray();

            var tgtHand = tgtBoneForRole(handRole);
            chains.Add(new Chain
            {
                Mode = mode,
                SrcHandSlot = sourceMap.RoleToBone.TryGetValue(handRole, out var srcHand)
                    ? registerSlot(srcHand)
                    : -1,
                TgtHandBone = tgtHand ?? -1,
                TgtHandNormRestRotInv = tgtHand is int h
                    ? Quaternion.Conjugate(tgtNormRest[h].Rot)
                    : Quaternion.Identity,
                Sources = sources,
                Recipients = recipients,
            });
        }

        return chains.Count > 0 ? new FingerSolver(chains, chrSrcInv, chrTgt) : null;
    }

    /// <summary>Same-rig detection: every chain member's canonical frame and normalized rest
    /// rotation agree between source and target (within float noise). Only then is the 1:1
    /// absolute copy lossless.</summary>
    private static bool ChainsCoincide(
        BoneRole[] roles, MappingResult sourceMap, CanonicalFrames srcCanon,
        IReadOnlyList<XForm> srcNormRest, Func<BoneRole, int?> tgtBoneForRole,
        CanonicalFrames tgtCanon, IReadOnlyList<XForm> tgtNormRest)
    {
        foreach (var role in roles)
        {
            var srcBone = sourceMap.RoleToBone[role];
            var tgtBone = tgtBoneForRole(role)!.Value;
            if (MathQ.AngleBetween(srcCanon.WorldFrameOf(role), tgtCanon.WorldFrameOf(role)) > SameRigToleranceRad
                || MathQ.AngleBetween(srcNormRest[srcBone].Rot, tgtNormRest[tgtBone].Rot) > SameRigToleranceRad)
            {
                return false;
            }
        }
        return true;
    }

    /// <summary>Normalized rest segment lengths of the recipient phalanges (the proportional
    /// curl weights). The distal segment, having no chain child, is estimated as 0.8× its
    /// preceding segment.</summary>
    private static float[] SegmentWeights(
        BoneRole[] tgtRoles, BoneRole[] recipientRoles,
        Func<BoneRole, int?> tgtBoneForRole, IReadOnlyList<XForm> tgtNormRest)
    {
        var positions = tgtRoles.Select(r => tgtNormRest[tgtBoneForRole(r)!.Value].Pos).ToArray();
        var weights = new float[recipientRoles.Length];
        for (var i = 0; i < recipientRoles.Length; i++)
        {
            var j = Array.IndexOf(tgtRoles, recipientRoles[i]);
            weights[i] = j + 1 < positions.Length
                ? (positions[j + 1] - positions[j]).Length()
                : j > 0 ? 0.8f * (positions[j] - positions[j - 1]).Length() : 1f;
        }

        var sum = weights.Sum();
        if (sum <= 1e-6f)
            return Enumerable.Repeat(1f / weights.Length, weights.Length).ToArray();
        for (var i = 0; i < weights.Length; i++)
            weights[i] /= sum;
        return weights;
    }

    // ---------------------------------------------------------------- per frame

    /// <summary>
    /// Solves the planned chains for one frame. <paramref name="srcDeltas"/> holds the
    /// registered source world rotation deltas (from normalized rest); solved target world
    /// rotations are written into <paramref name="rot"/>/<paramref name="solved"/>. The target
    /// hands must already be solved (body pass runs first).
    /// </summary>
    public void Apply(Quaternion[] srcDeltas, bool[] solved, Quaternion[] rot)
    {
        foreach (var chain in _chains)
        {
            var acc = chain.TgtHandBone >= 0 && solved[chain.TgtHandBone]
                ? MathQ.Normalize(rot[chain.TgtHandBone] * chain.TgtHandNormRestRotInv)
                : Quaternion.Identity;

            if (chain.Mode == ChainMode.DirectionMatch)
                ApplyDirectionMatch(chain, srcDeltas, acc, solved, rot);
            else
                ApplyProportional(chain, srcDeltas, acc, solved, rot);
        }
    }

    private void ApplyDirectionMatch(
        Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)
    {
        for (var i = 0; i < chain.Recipients.Length; i++)
        {
            var sp = chain.Sources[i];
            var rc = chain.Recipients[i];

            // Source phalanx direction in character coords; re-expressed in the target world,
            // then relative to the already-reconstructed parent delta, then in the phalanx's
            // canonical frame — where the rest direction is unit X.
            var srcAbs = MathQ.Normalize(_chrSrcInv * srcDeltas[sp.Slot] * sp.C);
            var dirChr = Vector3.Transform(Vector3.UnitX, srcAbs);
            var dirTgtWorld = Vector3.Transform(dirChr, _chrTgt);
            var dirLocal = Vector3.Transform(dirTgtWorld, Quaternion.Conjugate(acc));
            var dirCanon = Vector3.Transform(dirLocal, rc.CInv);

            // Shortest-arc swing X -> dir: rotation axis ⊥ X, so it carries zero finger-axis
            // twist by construction.
            var swing = MathQ.FromTo(Vector3.UnitX, dirCanon);

            acc = MathQ.Normalize(acc * (rc.C * swing * rc.CInv));
            rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);
            solved[rc.TgtBone] = true;
        }
    }

    private static void ApplyProportional(
        Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)
    {
        // Decompose: total local curl over the chain, splay from metacarpal + proximal.
        var prev = chain.SrcHandSlot >= 0 ? srcDeltas[chain.SrcHandSlot] : Quaternion.Identity;
        float totalCurl = 0f, splay = 0f;
        foreach (var sp in chain.Sources)
        {
            var dr = srcDeltas[sp.Slot];
            var local = MathQ.Normalize(Quaternion.Conjugate(prev) * dr);
            var canon = MathQ.Normalize(sp.CInv * local * sp.C);

            MathQ.SwingTwist(canon, Vector3.UnitY, out var swing, out var curlQ);
            totalCurl += SignedAngle(curlQ, Vector3.UnitY);

            if (sp.TakesSplay)
            {
                MathQ.SwingTwist(swing, Vector3.UnitZ, out _, out var splayQ);
                splay += SignedAngle(splayQ, Vector3.UnitZ);
            }

            prev = dr;
        }

        foreach (var rc in chain.Recipients)
        {
            var mu = Quaternion.CreateFromAxisAngle(Vector3.UnitY, totalCurl * rc.Weight);
            if (rc.Splay)
                mu = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, splay) * mu;

            acc = MathQ.Normalize(acc * (rc.C * mu * rc.CInv));
            rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);
            solved[rc.TgtBone] = true;
        }
    }

    /// <summary>Signed rotation angle of an axis-aligned twist quaternion about
    /// <paramref name="axis"/>, wrapped to (−π, π].</summary>
    private static float SignedAngle(Quaternion twist, Vector3 axis)
    {
        var s = twist.X * axis.X + twist.Y * axis.Y + twist.Z * axis.Z;
        var angle = 2f * MathF.Atan2(s, twist.W);
        if (angle > MathF.PI)
            angle -= 2f * MathF.PI;
        else if (angle < -MathF.PI)
            angle += 2f * MathF.PI;
        return angle;
    }
}
using System.Collections.Generic;
using HumanoidRetargeter.Mapping;

namespace HumanoidRetargeter.Solve;

/// <summary>How a mapped role's rotation is transferred by the <see cref="GeometricSolver"/>.</summary>
public enum RoleTransferMode
{
    /// <summary>
    /// Absolute canonical-orientation matching: the target's animated chain direction is
    /// driven to <b>equal</b> the source's (in character-frame coordinates). Right for limbs
    /// and the spine — the pose IS the direction — but it also imposes the source rig's rest
    /// proportions/posture on roles whose rest directions legitimately differ between rigs.
    /// </summary>
    AbsoluteDirection,

    /// <summary>
    /// Rest-relative delta: the source's canonical-space rotation <i>delta from its own
    /// normalized rest</i> is replayed onto the <b>target's</b> normalized rest
    /// (<c>W_t(f) = C_t·ΔC(f)·C_t⁻¹·R_tgtNormRest</c> with
    /// <c>ΔC(f) = C_s⁻¹·ΔR(f)·C_s</c>). The target keeps its own rest carriage (shoulder
    /// line height, neck-base angle) and moves with the source. Identical to
    /// <see cref="AbsoluteDirection"/> when source and target rigs coincide.
    /// </summary>
    DeltaFromRest,

    /// <summary>
    /// Character-space delta: the source's world-rotation delta from its normalized rest is
    /// re-expressed in character coordinates and applied to the <b>target's</b> normalized
    /// rest (<c>W_t(f) = M·ΔR(f)·M⁻¹·R_tgtNormRest</c> with <c>M = Q_tgt·Q_src⁻¹</c>, the
    /// same character basis change <see cref="AbsoluteDirection"/> premultiplies). Like
    /// <see cref="DeltaFromRest"/> the target keeps its own rest carriage, but the delta
    /// keeps its <i>world</i> rotation axes instead of being remapped through the per-role
    /// canonical frames — the faithful replay when the rigs' rest chain directions diverge
    /// so far that canonical-axis remapping would tilt every rotation axis by that
    /// divergence (measured 23–44° on feet: CMU/ARP ankle anatomy vs the s&amp;box rig's
    /// steep ankle, where canonical remapping mis-pitched planted feet by up to 47°).
    /// Identical to the other modes when source and target rigs coincide.
    /// </summary>
    CharacterDeltaFromRest,
}

/// <summary>Options controlling a single retarget solve (one clip → one output clip).</summary>
public sealed class SolveOptions
{
    /// <summary>
    /// Default per-role transfer modes: shoulder girdle and neck carriage are
    /// <see cref="RoleTransferMode.DeltaFromRest"/> (each rig's clavicle line / neck-base
    /// direction is rig anatomy, not pose — absolute matching was measured to drag the
    /// s&amp;box shoulders 6–28° toward the source's flatter/lower clavicle line and is the
    /// "low shoulders, hunched neck" artifact), and feet are
    /// <see cref="RoleTransferMode.CharacterDeltaFromRest"/> (a rest foot→toe direction is
    /// ankle anatomy too — rigs diverge 11–44° from the s&amp;box rig's steep ankle, so
    /// absolute matching pitched planted feet up to 25° off flat, the "feet bent
    /// upward/inward" artifact; the character-space delta keeps the rotation's world axes,
    /// which canonical-frame remapping would tilt by that same divergence). The head is
    /// <see cref="RoleTransferMode.CharacterDeltaFromRest"/> for the same reason: the rest
    /// neck→head direction is head-joint-placement anatomy (measured 0–27° forward lean
    /// across neutral-rest rigs vs the s&amp;box rig's 25.5°), so the target keeps its own
    /// neutral skull attitude and replays the source's attitude <i>changes</i> — for the
    /// head this computes exactly what the previous virtual-frame absolute matching did.
    /// Two solver fallbacks adjust these defaults per rig pair: on a toe-less source the
    /// foot entries become <see cref="RoleTransferMode.DeltaFromRest"/> (virtual-foot
    /// fallback), and a source whose normalized rest head attitude is implausible as a
    /// neutral carriage (a posed bind — e.g. a chin-down/tilted fighting-stance rest,
    /// measured 40.7° forward / 16.9° lateral on such a rig where the delta replay read
    /// ~12° "looking up at an angle") switches the head to
    /// <see cref="RoleTransferMode.AbsoluteDirection"/> so the gaze follows the source
    /// absolutely instead of replaying deltas from a posed reference (see the
    /// <see cref="GeometricSolver"/> remarks for both). Everything else (limbs, spine,
    /// toes, fingers) stays absolute: there the worldspace direction IS the pose.
    /// </summary>
    public static IReadOnlyDictionary<BoneRole, RoleTransferMode> DefaultTransferModes { get; } =
        new Dictionary<BoneRole, RoleTransferMode>
        {
            [BoneRole.ClavicleL] = RoleTransferMode.DeltaFromRest,
            [BoneRole.ClavicleR] = RoleTransferMode.DeltaFromRest,
            [BoneRole.Neck] = RoleTransferMode.DeltaFromRest,
            [BoneRole.Head] = RoleTransferMode.CharacterDeltaFromRest,
            [BoneRole.FootL] = RoleTransferMode.CharacterDeltaFromRest,
            [BoneRole.FootR] = RoleTransferMode.CharacterDeltaFromRest,
        };

    /// <summary>
    /// Per-role transfer modes. Null (default) = <see cref="DefaultTransferModes"/> plus the
    /// solver's fallback heuristics (a toe-less source's virtual foot direction overrides
    /// the foot default to <see cref="RoleTransferMode.DeltaFromRest"/>, and a posed-rest
    /// source head overrides the head default to
    /// <see cref="RoleTransferMode.AbsoluteDirection"/> — see the
    /// <see cref="GeometricSolver"/> remarks). A non-null map REPLACES the defaults entirely
    /// and disables every fallback heuristic: each role uses exactly the mode in the map, and
    /// roles absent from it are <see cref="RoleTransferMode.AbsoluteDirection"/>. Pass an
    /// empty dictionary for fully absolute (legacy) behavior — API callers supplying a map
    /// opt out of all heuristics.
    /// </summary>
    public IReadOnlyDictionary<BoneRole, RoleTransferMode>? TransferModes { get; init; }

    /// <summary>
    /// Scale applied to the pelvis translation components perpendicular to the character up
    /// direction. Null (default) = automatic: target hip height / source hip height, both
    /// measured on the normalized rests.
    /// </summary>
    public float? HipScaleHorizontal { get; init; }

    /// <summary>
    /// Scale applied to the pelvis translation component along the character up direction.
    /// Null (default) = the same automatic hip-height ratio as <see cref="HipScaleHorizontal"/>.
    /// </summary>
    public float? HipScaleVertical { get; init; }

    /// <summary>Whether finger roles are transferred; when false, target finger bones keep
    /// their rest locals.</summary>
    public bool TransferFingers { get; init; } = true;

    /// <summary>Output clip name; null = the source clip's name.</summary>
    public string? ClipName { get; init; }

    /// <summary>Index of the source clip to retarget (<c>SourceScene.Clips</c>).</summary>
    public int ClipIndex { get; init; }
}
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Maths;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Cleanup;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>Tunables for the grounded-foot stance recalibration pass.</summary>
public sealed class FootGroundAlignOptions
{
    /// <summary>
    /// Dead zone (degrees): measured stance offsets at or below this are genuine planted
    /// articulation (heel-roll bias, natural lean — measured 2–4° on well-rested rigs and
    /// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.
    /// Only offsets beyond it are clearly rest-pose artifacts (measured 12–25° on the
    /// repro rig) and get recalibrated.
    /// </summary>
    public float MinCorrectionDeg { get; set; } = 8f;

    /// <summary>
    /// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE
    /// for the offset measurement. Plants beyond this are not standing on the sole (crawls,
    /// kneels, prone contact — measured 60–90° there) and are excluded; genuine rest-pose
    /// stance artifacts measure well below it (largest seen: 27°).
    /// </summary>
    public float MaxStanceDeviationDeg { get; set; } = 35f;
}

/// <summary>Per-foot results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignFootReport
{
    /// <summary>Plants that contributed to the stance measurement.</summary>
    public int StancePlants { get; set; }

    /// <summary>Plants excluded as non-stance (mean sole deviation beyond
    /// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/>).</summary>
    public int SkippedPlants { get; set; }

    /// <summary>Measured planted sole offset from the ground plane, degrees (0 when no
    /// stance plants exist).</summary>
    public float MeasuredOffsetDeg { get; set; }

    /// <summary>Foot correction applied to every frame, degrees (0 = inside the dead zone,
    /// nothing changed).</summary>
    public float AppliedFootDeg { get; set; }

    /// <summary>Toe correction applied to every frame, degrees.</summary>
    public float AppliedToeDeg { get; set; }
}

/// <summary>Results of a <see cref="FootGroundAlign.Apply"/> run.</summary>
public sealed class FootGroundAlignReport
{
    /// <summary>Left-foot results.</summary>
    public required FootGroundAlignFootReport Left { get; init; }

    /// <summary>Right-foot results.</summary>
    public required FootGroundAlignFootReport Right { get; init; }
}

/// <summary>
/// Grounded-foot stance recalibration: measures how far the foot's SOLE sits from the ground
/// plane while planted, and — when that offset is clearly a rest-pose artifact — rotates it
/// out with one constant per foot, applied to every frame of the clip.
/// </summary>
/// <remarks>
/// <para><b>Why a cleanup pass.</b> The solver transfers feet as rest-relative deltas
/// (<see cref="Solve.RoleTransferMode.CharacterDeltaFromRest"/>), so the target keeps its own
/// ankle anatomy — correct whenever the source's rest pose is a flat-footed stance (the delta
/// is then "deviation from standing"). Some rigs ship a NON-stance rest (measured: an
/// Auto-Rig-Pro export whose rest foot sits 12–25° from its planted stance), and that constant
/// offset rides into every frame of the replay — planted feet hover toe-down/heel-up. What a
/// stance actually looks like is animation evidence (planted phases), which a per-frame
/// solver cannot see, so the recalibration lives here.</para>
/// <para><b>Measurement.</b> Per foot: over every planted frame, the sole normal = rest up
/// carried by the foot's world delta from the target bind rest (whose feet stand on the
/// ground by construction); plants whose own mean normal sits beyond
/// <see cref="FootGroundAlignOptions.MaxStanceDeviationDeg"/> are excluded (crawl/kneel/prone
/// contact is not a stance). The pooled mean normal's deviation from up is the stance
/// offset.</para>
/// <para><b>Correction.</b> Offsets inside <see cref="FootGroundAlignOptions.MinCorrectionDeg"/>
/// are genuine articulation — nothing is changed (well-rested rigs and same-rig round trips
/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the
/// pooled normal back to up (pitch+roll only — yaw/toe-out is pose and follows the source)
/// premultiplies the foot's world rotation on EVERY frame: a rest artifact is constant, so
/// the fix is too — within-plant heel-roll, swing styling and frame-to-frame continuity are
/// preserved exactly, and no blending is needed. The toe then receives its own residual
/// constant measured on top of the corrected foot (it neither double-rotates with the foot
/// fix nor inherits the source toe's own rest artifact). Corrections rotate bones about
/// their own joints: ankle positions are untouched, so the pass composes freely with the
/// <see cref="FootPlant"/> position pinning (which preserves foot world rotations).</para>
/// <para><b>Plant intervals come from the caller</b> (the pipeline detects them on the
/// SOURCE clip via <see cref="FootPlant.DetectPlantIntervals"/> — ground truth, immune to
/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned
/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass
/// only when the source's normalized rest is implausible as a flat stance (toe at/above
/// ankle level or asymmetric feet — see <c>Retargeter.GroundAlignFeet</c>); on plausible
/// stance rests the solver's rest-relative transfer is already faithful and planted-sole
/// deviations are genuine articulation (boxing stances, heel rolls) that must not be
/// flattened.</para>
/// </remarks>
public static class FootGroundAlign
{
    /// <summary>Measures planted stance offsets and recalibrates feet whose offset is a
    /// rest-pose artifact; returns what was measured and done.</summary>
    /// <param name="frames">Per-frame local transforms (skeleton bone order); modified in place.</param>
    /// <param name="skeleton">Bone hierarchy the frames are expressed against; its bind rest
    /// is the flat-stance reference.</param>
    /// <param name="left">Left leg chain bone indices.</param>
    /// <param name="right">Right leg chain bone indices.</param>
    /// <param name="up">World up direction of the clip's space.</param>
    /// <param name="leftPlants">Left-foot plant intervals (frame indices into
    /// <paramref name="frames"/>; out-of-range parts are clamped/ignored).</param>
    /// <param name="rightPlants">Right-foot plant intervals.</param>
    /// <param name="options">Tunables; defaults used when null.</param>
    public static FootGroundAlignReport Apply(
        List<XForm[]> frames,
        SkeletonModel skeleton,
        FootChain left,
        FootChain right,
        Vector3 up,
        IReadOnlyList<FrameRange> leftPlants,
        IReadOnlyList<FrameRange> rightPlants,
        FootGroundAlignOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(frames);
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(left);
        ArgumentNullException.ThrowIfNull(right);
        ArgumentNullException.ThrowIfNull(leftPlants);
        ArgumentNullException.ThrowIfNull(rightPlants);

        options ??= new FootGroundAlignOptions();
        var report = new FootGroundAlignReport
        {
            Left = new FootGroundAlignFootReport(),
            Right = new FootGroundAlignFootReport(),
        };
        if (frames.Count == 0 || up.LengthSquared() < 1e-12f)
            return report;
        up = Vector3.Normalize(up);

        RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);
        RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);
        return report;
    }

    private static void RecalibrateFoot(
        List<XForm[]> frames, SkeletonModel skeleton, FootChain chain, Vector3 up,
        IReadOnlyList<FrameRange> plants, FootGroundAlignOptions options,
        FootGroundAlignFootReport report)
    {
        int n = frames.Count;
        var foot = chain.Ankle;
        var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);
        var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);

        // ---- measurement: pooled planted sole normal over the stance plants ----
        var pooled = Vector3.Zero;
        foreach (var plant in plants)
        {
            int start = Math.Max(plant.Start, 0);
            int end = Math.Min(plant.End, n - 1);
            if (start > end)
                continue;

            var plantSum = Vector3.Zero;
            for (int f = start; f <= end; f++)
            {
                var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;
                plantSum += Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));
            }
            if (plantSum.LengthSquared() < 1e-8f
                || Vector3.Dot(Vector3.Normalize(plantSum), up) < maxStanceCos)
            {
                report.SkippedPlants++; // not standing on the sole — crawl/kneel/toe contact
                continue;
            }
            report.StancePlants++;
            pooled += plantSum; // frame-count-weighted: longer stances dominate
        }
        if (pooled.LengthSquared() < 1e-8f)
            return;
        pooled = Vector3.Normalize(pooled);

        var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);
        report.MeasuredOffsetDeg = offsetDeg;
        if (offsetDeg <= options.MinCorrectionDeg)
            return; // genuine planted articulation — leave the transfer byte-faithful

        // ---- correction: one constant per foot, every frame ----
        var footFix = MathQ.FromTo(pooled, up);
        report.AppliedFootDeg = offsetDeg;

        // Toe residual measured on top of the corrected foot, same dead zone.
        var toeFix = Quaternion.Identity;
        if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
        {
            var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);
            var toePooled = Vector3.Zero;
            foreach (var plant in plants)
            {
                int start = Math.Max(plant.Start, 0);
                int end = Math.Min(plant.End, n - 1);
                for (int f = start; f <= end && f >= 0; f++)
                {
                    var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;
                    toePooled += Vector3.Transform(
                        up, MathQ.Normalize(footFix * toeRot * restToeRotInv));
                }
            }
            if (toePooled.LengthSquared() > 1e-8f)
            {
                toePooled = Vector3.Normalize(toePooled);
                var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);
                if (toeDeg > options.MinCorrectionDeg && Vector3.Dot(toePooled, up) >= maxStanceCos)
                {
                    toeFix = MathQ.FromTo(toePooled, up);
                    report.AppliedToeDeg = toeDeg;
                }
            }
        }

        for (int f = 0; f < n; f++)
            CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);
    }

    /// <summary>Premultiplies the foot's world rotation by the constant fix (the joint
    /// position is untouched — the rotation pivots the foot about its own head), then gives
    /// the toe its own residual on top of the corrected foot.</summary>
    private static void CorrectFrame(
        XForm[] locals, SkeletonModel skeleton, FootChain chain,
        Quaternion footFix, Quaternion toeFix)
    {
        var foot = chain.Ankle;
        var parent = skeleton[foot].ParentIndex;
        var parentRot = parent < 0
            ? Quaternion.Identity
            : FkUtil.BoneWorld(locals, skeleton, parent).Rot;

        var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);
        var newFootWorld = MathQ.Normalize(footFix * footWorld);
        locals[foot] = new XForm(
            locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));

        if (chain.Toe is { } toe && skeleton[toe].ParentIndex == foot)
        {
            // Desired toe world = toeFix ∘ footFix ∘ original world; re-derive its local
            // against the corrected foot so it does not double-rotate with the foot fix.
            var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);
            var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);
            locals[toe] = new XForm(
                locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));
        }
    }
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;

namespace HumanoidRetargeter.Formats.Bvh;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>Options for <see cref="BvhImporter.Import"/>.</summary>
public sealed class BvhImportOptions
{
    /// <summary>Fixed resampling rate for the motion data, frames per second.</summary>
    public float SampleFps { get; init; } = 30f;
}

/// <summary>
/// BVH (Biovision Hierarchy) → <see cref="SourceScene"/> importer.
/// </summary>
/// <remarks>
/// <para><b>Format conventions implemented</b> (verified against Blender's
/// <c>io_anim_bvh</c> importer, which is the project's ground-truth extractor):</para>
/// <list type="bullet">
/// <item><b>Rest pose:</b> each joint's rest local translation is its <c>OFFSET</c>; rest
/// rotation is identity (BVH stores no rest orientation).</item>
/// <item><b>Rotation channels:</b> the channel list order IS the rotation order. The listed
/// rotations apply left-to-right as intrinsic rotations, which in this library's
/// column-vector convention (<c>a * b</c> applies <c>b</c> first) is the product
/// <c>R = R_chan1 * R_chan2 * R_chan3</c> — e.g. <c>Zrotation Yrotation Xrotation</c> gives
/// <c>R = Rz * Ry * Rx</c>. This matches Blender, which builds
/// <c>Euler((x,y,z), reversed(channelOrder))</c> for the same matrix. Angles are degrees.</item>
/// <item><b>Position channels:</b> when a joint has any position channel, the channel values
/// REPLACE the joint's local translation (missing components are 0) — they are not added to
/// the <c>OFFSET</c>. This is Blender's behavior; in practice roots have OFFSET 0 so the two
/// readings only diverge on non-root position channels (e.g. Bandai-Namco exports).</item>
/// <item><b>End Sites:</b> synthesized as a channel-less leaf bone named
/// <c>"&lt;parent&gt;_end"</c> so chain tips keep their direction information (Blender instead
/// folds them into the parent bone's tail).</item>
/// </list>
/// <para><b>Units</b>: BVH files carry no unit declaration. Heuristic: compute the rest
/// skeleton height (max−min world Y over all joints); if it is &lt; 10 the file is assumed
/// to be in meters and all translations (offsets AND position channels, root included) are
/// scaled ×100 to centimeters, otherwise it is assumed to already be centimeters (×1).
/// Millimeter-scale files (height &gt; 400) are not special-cased — they are rare and
/// ambiguous against cm mocap of long ranges; <see cref="SourceScene.UnitScaleCm"/> records
/// whichever factor was applied for diagnostics.</para>
/// <para><b>Resampling</b>: motion frames are resampled from the file's <c>Frame Time</c>
/// grid onto <see cref="BvhImportOptions.SampleFps"/>. Each native frame's euler channels are
/// converted to a quaternion FIRST and bracketing frames are then slerped (positions lerped).
/// Interpolating raw euler angles across frames would mostly work at mocap densities
/// (30–120 fps, small per-frame deltas) but breaks down when an angle wraps ±180° between
/// frames; per-frame quaternion + slerp has no such failure mode, so that is what we do.</para>
/// <para><b>Axes</b>: BVH is conventionally Y-up / Z-forward / X-right. Native axes are
/// preserved (no conversion), matching the FBX importer's policy; the conventional axes are
/// recorded on the <see cref="SourceScene"/> (up = Y, front = Z, coord = X).</para>
/// </remarks>
public static class BvhImporter
{
    private const float MeterHeightThreshold = 10f;

    /// <summary>Parses BVH bytes and builds the source scene.</summary>
    /// <exception cref="FormatException">Malformed or truncated BVH.</exception>
    public static SourceScene Import(byte[] data, BvhImportOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(data);
        options ??= new BvhImportOptions();
        if (!(options.SampleFps > 0f) || !float.IsFinite(options.SampleFps))
            throw new ArgumentOutOfRangeException(nameof(options), "SampleFps must be positive.");

        var cursor = new TokenCursor(Encoding.UTF8.GetString(data));

        // ---- HIERARCHY -----------------------------------------------------------------
        cursor.ExpectKeyword("HIERARCHY");
        var joints = new List<Joint>();
        int channelCount = 0;
        if (!cursor.PeekIs("ROOT"))
            throw new FormatException("BVH: expected ROOT after HIERARCHY.");
        while (cursor.PeekIs("ROOT")) // multiple roots are out of spec but harmless to accept
        {
            cursor.Next();
            ParseJoint(cursor, joints, parent: -1, ref channelCount);
        }

        // ---- MOTION ---------------------------------------------------------------------
        cursor.ExpectKeyword("MOTION");
        cursor.ExpectKeyword("FRAMES:");
        int frameCount = cursor.NextInt();
        if (frameCount < 0)
            throw new FormatException($"BVH: negative frame count {frameCount}.");
        cursor.ExpectKeyword("FRAME");
        cursor.ExpectKeyword("TIME:");
        float frameTime = cursor.NextFloat();
        if (!(frameTime > 0f) || !float.IsFinite(frameTime))
            throw new FormatException($"BVH: invalid Frame Time {frameTime}.");

        var motion = new float[frameCount][];
        for (int f = 0; f < frameCount; f++)
        {
            var row = new float[channelCount];
            for (int c = 0; c < channelCount; c++)
                row[c] = cursor.NextFloat();
            motion[f] = row;
        }

        // ---- units heuristic --------------------------------------------------------------
        float unitScale = HeuristicUnitScale(joints);

        // ---- skeleton ----------------------------------------------------------------------
        var defs = new List<BoneDefinition>(joints.Count);
        foreach (var j in joints)
        {
            defs.Add(new BoneDefinition(
                j.Name,
                j.Parent < 0 ? null : joints[j.Parent].Name,
                new XForm(j.Offset * unitScale, Quaternion.Identity)));
        }
        var skeleton = Skeleton.Skeleton.Create(defs);

        // ---- clip ----------------------------------------------------------------------------
        var clips = new List<Clip>();
        if (frameCount > 0)
            clips.Add(ResampleClip(joints, skeleton, motion, frameTime, unitScale, options.SampleFps));

        // BVH conventional axes: Y-up (1), Z-front (2), X-coord (0) — recorded, not converted.
        return new SourceScene(
            skeleton, clips, unitScale,
            upAxis: 1, upAxisSign: 1,
            frontAxis: 2, frontAxisSign: 1,
            coordAxis: 0, coordAxisSign: 1,
            originalUpAxis: -1);
    }

    // =====================================================================================
    // hierarchy parsing
    // =====================================================================================

    private sealed class Joint
    {
        public required string Name;
        public required int Parent;          // index into the joint list, -1 for roots
        public Vector3 Offset;               // raw file units
        public int PosX = -1, PosY = -1, PosZ = -1;            // motion column per position axis
        public List<(int Axis, int Column)> Rot = new();        // rotation channels in file order
        public bool HasPos => PosX >= 0 || PosY >= 0 || PosZ >= 0;
    }

    private static void ParseJoint(TokenCursor cursor, List<Joint> joints, int parent, ref int channelCount)
    {
        // Joint name: tokens up to '{', joined with '_' (mirrors Blender's handling of
        // names containing spaces).
        var nameParts = new List<string>();
        while (!cursor.PeekIs("{"))
        {
            if (cursor.AtEnd)
                throw new FormatException("BVH: unexpected end of file in joint name.");
            nameParts.Add(cursor.Next());
        }
        if (nameParts.Count == 0)
            throw new FormatException("BVH: joint with no name.");
        string name = UniqueName(string.Join('_', nameParts), joints);

        cursor.ExpectKeyword("{");
        cursor.ExpectKeyword("OFFSET");
        var joint = new Joint { Name = name, Parent = parent };
        joint.Offset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());
        int index = joints.Count;
        joints.Add(joint);

        if (cursor.PeekIs("CHANNELS"))
        {
            cursor.Next();
            int n = cursor.NextInt();
            if (n < 0 || n > 6)
                throw new FormatException($"BVH: joint '{name}' has invalid channel count {n}.");
            for (int i = 0; i < n; i++)
            {
                string channel = cursor.Next();
                int column = channelCount++;
                switch (channel.ToUpperInvariant())
                {
                    case "XPOSITION": joint.PosX = column; break;
                    case "YPOSITION": joint.PosY = column; break;
                    case "ZPOSITION": joint.PosZ = column; break;
                    case "XROTATION": joint.Rot.Add((0, column)); break;
                    case "YROTATION": joint.Rot.Add((1, column)); break;
                    case "ZROTATION": joint.Rot.Add((2, column)); break;
                    default:
                        throw new FormatException($"BVH: unknown channel '{channel}' on joint '{name}'.");
                }
            }
        }

        while (!cursor.PeekIs("}"))
        {
            if (cursor.AtEnd)
                throw new FormatException($"BVH: unexpected end of file inside joint '{name}'.");
            if (cursor.PeekIs("JOINT"))
            {
                cursor.Next();
                ParseJoint(cursor, joints, index, ref channelCount);
            }
            else if (cursor.PeekIs("END"))
            {
                cursor.Next();
                cursor.ExpectKeyword("SITE");
                while (!cursor.PeekIs("{")) // a name after "End Site" is out of spec; skip it
                {
                    if (cursor.AtEnd)
                        throw new FormatException("BVH: unexpected end of file in End Site.");
                    cursor.Next();
                }
                cursor.ExpectKeyword("{");
                cursor.ExpectKeyword("OFFSET");
                var endOffset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());
                cursor.ExpectKeyword("}");

                // Synthesize a channel-less leaf so the chain tip's direction is kept.
                joints.Add(new Joint
                {
                    Name = UniqueName(name + "_end", joints),
                    Parent = index,
                    Offset = endOffset,
                });
            }
            else
            {
                throw new FormatException(
                    $"BVH: unexpected token '{cursor.Next()}' inside joint '{name}'.");
            }
        }
        cursor.ExpectKeyword("}");
    }

    private static string UniqueName(string name, List<Joint> joints)
    {
        bool Taken(string candidate)
        {
            foreach (var j in joints)
                if (string.Equals(j.Name, candidate, StringComparison.Ordinal))
                    return true;
            return false;
        }

        if (!Taken(name))
            return name;
        for (int i = 1; ; i++)
        {
            string candidate = $"{name}#{i}";
            if (!Taken(candidate))
                return candidate;
        }
    }

    // =====================================================================================
    // units
    // =====================================================================================

    /// <summary>
    /// Meters-vs-centimeters heuristic: rest skeleton height (max−min world Y over all
    /// joints, end sites included) &lt; 10 → meters → ×100; otherwise centimeters → ×1.
    /// </summary>
    private static float HeuristicUnitScale(List<Joint> joints)
    {
        Span<float> worldY = joints.Count <= 256 ? stackalloc float[joints.Count] : new float[joints.Count];
        float min = float.MaxValue, max = float.MinValue;
        for (int i = 0; i < joints.Count; i++)
        {
            worldY[i] = (joints[i].Parent < 0 ? 0f : worldY[joints[i].Parent]) + joints[i].Offset.Y;
            min = MathF.Min(min, worldY[i]);
            max = MathF.Max(max, worldY[i]);
        }
        float height = max - min;
        return height > 0f && height < MeterHeightThreshold ? 100f : 1f;
    }

    // =====================================================================================
    // motion sampling
    // =====================================================================================

    /// <summary>
    /// Decodes every native frame to per-joint local transforms (quaternions built per frame
    /// from the joint's channel order), then resamples onto the <paramref name="fps"/> grid —
    /// positions lerped, rotations slerped between the bracketing native frames.
    /// </summary>
    private static Clip ResampleClip(
        List<Joint> joints, Skeleton.Skeleton skeleton, float[][] motion,
        float frameTime, float unitScale, float fps)
    {
        int jointCount = joints.Count;
        int nativeCount = motion.Length;

        // Joint order may differ from skeleton bone order (topological sort) — map.
        var toSkeleton = new int[jointCount];
        for (int i = 0; i < jointCount; i++)
            toSkeleton[i] = skeleton.IndexOf(joints[i].Name);

        // Native-frame locals.
        var native = new XForm[nativeCount][];
        for (int f = 0; f < nativeCount; f++)
        {
            var row = motion[f];
            var locals = new XForm[jointCount];
            for (int i = 0; i < jointCount; i++)
                locals[i] = EvaluateLocal(joints[i], row, unitScale);
            native[f] = locals;
        }

        double duration = (nativeCount - 1) * (double)frameTime;
        int outCount = Math.Max(1, (int)Math.Round(duration * fps) + 1);

        var frames = new List<XForm[]>(outCount);
        for (int f = 0; f < outCount; f++)
        {
            double s = f / (double)fps / frameTime; // position on the native frame grid
            int i0 = Math.Clamp((int)Math.Floor(s), 0, nativeCount - 1);
            int i1 = Math.Min(i0 + 1, nativeCount - 1);
            float u = Math.Clamp((float)(s - i0), 0f, 1f);

            var frame = new XForm[skeleton.Count];
            var a = native[i0];
            var b = native[i1];
            for (int i = 0; i < jointCount; i++)
            {
                frame[toSkeleton[i]] = new XForm(
                    Vector3.Lerp(a[i].Pos, b[i].Pos, u),
                    MathQ.Normalize(Quaternion.Slerp(a[i].Rot, b[i].Rot, u)));
            }
            frames.Add(frame);
        }

        // NativeFps records the file's authored frame rate (1 / FrameTime): external frame
        // ranges (Unity .meta clipAnimations) are expressed in it.
        float nativeFps = frameTime > 0f ? (float)(1.0 / frameTime) : fps;
        return new Clip("motion", fps, looping: false, frames, nativeFps);
    }

    /// <summary>One joint's local transform from one motion row (see class remarks).</summary>
    private static XForm EvaluateLocal(Joint joint, float[] row, float unitScale)
    {
        // Position channels replace the OFFSET; absent channels (or no position channels at
        // all) fall back per Blender's semantics described in the class remarks.
        Vector3 pos = joint.HasPos
            ? new Vector3(
                joint.PosX >= 0 ? row[joint.PosX] : 0f,
                joint.PosY >= 0 ? row[joint.PosY] : 0f,
                joint.PosZ >= 0 ? row[joint.PosZ] : 0f)
            : joint.Offset;

        // R = R_chan1 * R_chan2 * R_chan3 (column-vector convention; degrees in the file).
        var rot = Quaternion.Identity;
        foreach (var (axis, column) in joint.Rot)
        {
            float radians = row[column] * (MathF.PI / 180f);
            var axisVector = axis switch
            {
                0 => Vector3.UnitX,
                1 => Vector3.UnitY,
                _ => Vector3.UnitZ,
            };
            rot *= Quaternion.CreateFromAxisAngle(axisVector, radians);
        }

        return new XForm(pos * unitScale, MathQ.Normalize(rot));
    }

    // =====================================================================================
    // tokenizer
    // =====================================================================================

    /// <summary>Whitespace token stream over the BVH text (BVH is line-format agnostic).</summary>
    private sealed class TokenCursor
    {
        private readonly string[] _tokens;
        private int _pos;

        public TokenCursor(string text)
            => _tokens = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);

        public bool AtEnd => _pos >= _tokens.Length;

        public bool PeekIs(string keywordUpper)
            => _pos < _tokens.Length &&
               string.Equals(_tokens[_pos], keywordUpper, StringComparison.OrdinalIgnoreCase);

        public string Next()
        {
            if (AtEnd)
                throw new FormatException("BVH: unexpected end of file.");
            return _tokens[_pos++];
        }

        public void ExpectKeyword(string keywordUpper)
        {
            string token = Next();
            if (!string.Equals(token, keywordUpper, StringComparison.OrdinalIgnoreCase))
                throw new FormatException($"BVH: expected '{keywordUpper}', found '{token}'.");
        }

        public int NextInt()
        {
            string token = Next();
            if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
                throw new FormatException($"BVH: expected an integer, found '{token}'.");
            return value;
        }

        public float NextFloat()
        {
            string token = Next();
            if (!float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value) ||
                !float.IsFinite(value))
                throw new FormatException($"BVH: expected a number, found '{token}'.");
            return value;
        }
    }
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using HumanoidRetargeter.Skeleton;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Formats.Dmx;

/// <summary>Options for <see cref="DmxWriter.Write"/>.</summary>
public sealed class DmxWriteOptions
{
    /// <summary>Model/clip name written into the DmeModel element (e.g. the sequence name).</summary>
    public string Name { get; set; } = "";

    /// <summary>Free-form provenance note written as the DmeDCCMakefile source name
    /// (fbx2dmx writes the source .fbx path here).</summary>
    public string SourceNote { get; set; } = "";

    /// <summary>When true (default, matching fbx2dmx output) the file declares a Y-up axis
    /// system; when false it declares Z-up. Data is written as-is either way.</summary>
    public bool UpAxisY { get; set; } = true;

    /// <summary>
    /// Skeleton bone indices that get NO DmeChannel pair: the bones keep their DmeJoint and
    /// bind (rest) transform, but no animation channels are written for them — the engine then
    /// drives them itself (e.g. ConstraintDriven twist/helper bones, design §3). Null (default)
    /// writes channels for every bone.
    /// </summary>
    public IReadOnlySet<int>? ChannelExcludedBones { get; set; }
}

/// <summary>
/// Writes an animation DMX in <c>keyvalues2_noids</c> text encoding, replicating the exact
/// element/attribute shape of fbx2dmx output (authoritative reference:
/// <c>dev/m0/ref_idlepose.dmx</c>): a root DmElement holding an inline DmeModel (joint GUID
/// refs + bind base state), a top-level DmeAnimationList with one DmeChannelsClip carrying a
/// position and an orientation channel per bone, and top-level DmeTransform/DmeJoint elements
/// the channels and joint lists reference by GUID. Output is fully deterministic: GUIDs are
/// MD5-derived from the options name and an element path, and export tags use fixed
/// placeholder strings.
/// </summary>
public static class DmxWriter
{
    private const string Header = "<!-- dmx encoding keyvalues2_noids 4 format model 22 -->";

    /// <summary>
    /// Serializes <paramref name="clip"/> on <paramref name="skeleton"/> to DMX text.
    /// Frames must contain one local transform per bone in skeleton order.
    /// </summary>
    /// <exception cref="ArgumentException">Thrown when the clip is empty or a frame's bone
    /// count does not match the skeleton.</exception>
    public static string Write(SkeletonModel skeleton, Clip clip, DmxWriteOptions options)
    {
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(clip);
        ArgumentNullException.ThrowIfNull(options);

        if (clip.FrameCount == 0)
            throw new ArgumentException("Clip has no frames.", nameof(clip));
        for (var f = 0; f < clip.FrameCount; f++)
        {
            if (clip.Frames[f].Length != skeleton.Count)
                throw new ArgumentException(
                    $"Frame {f} has {clip.Frames[f].Length} bone transforms, skeleton has {skeleton.Count}.",
                    nameof(clip));
        }

        var w = new Emitter();
        var animListGuid = GuidString(options.Name, "animationList");
        var jointGuids = new string[skeleton.Count];
        var transformGuids = new string[skeleton.Count];
        for (var i = 0; i < skeleton.Count; i++)
        {
            jointGuids[i] = GuidString(options.Name, "joint:" + skeleton[i].Name);
            transformGuids[i] = GuidString(options.Name, "transform:" + skeleton[i].Name);
        }

        w.Raw(Header);

        // ---- root DmElement -------------------------------------------------
        w.BeginTopLevel("DmElement");
        w.Attr("name", "string", "root");

        w.BeginInlineAttr("skeleton", "DmeModel");
        w.Attr("name", "string", options.Name);
        w.BeginInlineAttr("transform", "DmeTransform");
        w.Attr("position", "vector3", "0 0 0");
        w.Attr("orientation", "quaternion", "0 0 0 1");
        w.EndInlineAttr();
        w.Attr("shape", "element", "");
        w.Attr("visible", "bool", "1");

        w.BeginArray("children");
        var roots = new List<int>();
        for (var i = 0; i < skeleton.Count; i++)
        {
            if (skeleton[i].ParentIndex < 0)
                roots.Add(i);
        }
        for (var r = 0; r < roots.Count; r++)
            w.ElementRef(jointGuids[roots[r]], last: r == roots.Count - 1);
        w.EndArray();

        w.BeginArray("jointList");
        for (var i = 0; i < skeleton.Count; i++)
            w.ElementRef(jointGuids[i], last: i == skeleton.Count - 1);
        w.EndArray();

        w.BeginArray("baseStates");
        w.BeginArrayElement("DmeTransformList");
        w.Attr("name", "string", "bind");
        w.BeginArray("transforms");
        for (var i = 0; i < skeleton.Count; i++)
        {
            w.BeginArrayElement("DmeTransform");
            w.Attr("name", "string", skeleton[i].Name);
            w.Attr("position", "vector3", Vec(skeleton[i].RestLocal));
            w.Attr("orientation", "quaternion", Quat(skeleton[i].RestLocal));
            w.EndArrayElement(last: i == skeleton.Count - 1);
        }
        w.EndArray();
        w.EndArrayElement(last: true);
        w.EndArray();

        w.Attr("upAxis", "string", options.UpAxisY ? "Y" : "Z");
        w.BeginInlineAttr("axisSystem", "DmeAxisSystem");
        w.Attr("upAxis", "int", options.UpAxisY ? "2" : "3");
        w.Attr("forwardParity", "int", "2");
        w.Attr("coordSys", "int", "0");
        w.EndInlineAttr();
        w.Attr("animationList", "element", animListGuid);
        w.EndInlineAttr(); // skeleton DmeModel

        w.BeginInlineAttr("makefile", "DmeDCCMakefile");
        w.Attr("name", "string", "makefile");
        w.BeginArray("sources");
        w.BeginArrayElement("DmeSource");
        w.Attr("name", "string", options.SourceNote);
        w.EndArrayElement(last: true);
        w.EndArray();
        w.EndInlineAttr();

        // Deterministic placeholders — never wall-clock/user data, so output is reproducible.
        w.BeginInlineAttr("exportTags", "DmeExportTags");
        w.Attr("name", "string", "exportTags");
        w.Attr("date", "string", "2026/01/01");
        w.Attr("time", "string", "12:00:00 am");
        w.Attr("user", "string", "retargeter");
        w.Attr("machine", "string", "retargeter");
        w.Attr("app", "string", "humanoid-retargeter");
        w.Attr("appVersion", "string", "1.0");
        w.Attr("cmdLine", "string", "humanoid-retargeter");
        w.Attr("pwd", "string", "");
        w.EndInlineAttr();

        w.Attr("animationList", "element", animListGuid);
        w.EndTopLevel();

        // ---- DmeAnimationList ----------------------------------------------
        w.BeginTopLevel("DmeAnimationList");
        w.Attr("id", "elementid", animListGuid);
        w.Attr("name", "string", "anim");
        w.BeginArray("animations");
        w.BeginArrayElement("DmeChannelsClip");
        w.Attr("name", "string", "anim");

        w.BeginInlineAttr("timeFrame", "DmeTimeFrame");
        w.Attr("start", "time", Time(0.0));
        w.Attr("duration", "time", Time((clip.FrameCount - 1) / (double)clip.Fps));
        w.Attr("offset", "time", Time(0.0));
        w.Attr("scale", "float", "1");
        w.EndInlineAttr();

        w.Attr("color", "color", "0 0 0 0");
        w.Attr("text", "string", "");
        w.Attr("mute", "bool", "0");
        w.BeginArray("trackGroups");
        w.EndArray();
        w.Attr("displayScale", "float", "1");

        var channelBones = new List<int>(skeleton.Count);
        for (var i = 0; i < skeleton.Count; i++)
        {
            if (options.ChannelExcludedBones is null || !options.ChannelExcludedBones.Contains(i))
                channelBones.Add(i);
        }

        w.BeginArray("channels");
        for (var n = 0; n < channelBones.Count; n++)
        {
            var i = channelBones[n];
            WriteChannel(w, skeleton, clip, i, transformGuids[i], position: true, last: false);
            WriteChannel(w, skeleton, clip, i, transformGuids[i], position: false,
                last: n == channelBones.Count - 1);
        }
        w.EndArray();

        w.Attr("frameRate", "int",
            ((int)MathF.Round(clip.Fps)).ToString(CultureInfo.InvariantCulture));
        w.EndArrayElement(last: true);
        w.EndArray();
        w.EndTopLevel();

        // ---- top-level channel-target DmeTransforms (rest values) -----------
        for (var i = 0; i < skeleton.Count; i++)
        {
            w.BeginTopLevel("DmeTransform");
            w.Attr("id", "elementid", transformGuids[i]);
            w.Attr("name", "string", skeleton[i].Name);
            w.Attr("position", "vector3", Vec(skeleton[i].RestLocal));
            w.Attr("orientation", "quaternion", Quat(skeleton[i].RestLocal));
            w.EndTopLevel();
        }

        // ---- top-level DmeJoints --------------------------------------------
        for (var i = 0; i < skeleton.Count; i++)
        {
            w.BeginTopLevel("DmeJoint");
            w.Attr("id", "elementid", jointGuids[i]);
            w.Attr("name", "string", skeleton[i].Name);
            w.Attr("transform", "element", transformGuids[i]);
            w.Attr("shape", "element", "");
            w.Attr("visible", "bool", "1");
            w.BeginArray("children");
            var children = new List<int>();
            for (var c = 0; c < skeleton.Count; c++)
            {
                if (skeleton[c].ParentIndex == i)
                    children.Add(c);
            }
            for (var c = 0; c < children.Count; c++)
                w.ElementRef(jointGuids[children[c]], last: c == children.Count - 1);
            w.EndArray();
            w.EndTopLevel();
        }

        return w.ToString();
    }

    /// <summary>
    /// Deterministic element GUID: MD5 over <c>"&lt;name&gt;\n&lt;path&gt;"</c> (UTF-8)
    /// interpreted as <see cref="Guid"/> bytes. Exposed so tests can verify the scheme.
    /// </summary>
    public static Guid ElementGuid(string name, string path)
        => new(MD5.HashData(Encoding.UTF8.GetBytes(name + "\n" + path)));

    private static string GuidString(string name, string path)
        => ElementGuid(name, path).ToString("D", CultureInfo.InvariantCulture);

    // ---------------------------------------------------------------- channels

    private static void WriteChannel(Emitter w, SkeletonModel skeleton, Clip clip, int bone,
        string transformGuid, bool position, bool last)
    {
        var logClass = position ? "DmeVector3Log" : "DmeQuaternionLog";
        var layerClass = position ? "DmeVector3LogLayer" : "DmeQuaternionLogLayer";
        var logName = position ? "vector3 log" : "quaternion log";

        w.BeginArrayElement("DmeChannel");
        w.Attr("name", "string", skeleton[bone].Name + (position ? "_p" : "_o"));
        w.Attr("fromElement", "element", "");
        w.Attr("fromAttribute", "string", "");
        w.Attr("fromIndex", "int", "0");
        w.Attr("toElement", "element", transformGuid);
        w.Attr("toAttribute", "string", position ? "position" : "orientation");
        w.Attr("toIndex", "int", "0");
        w.Attr("mode", "int", "3");

        w.BeginInlineAttr("log", logClass);
        w.Attr("name", "string", logName);
        w.BeginArray("layers");
        w.BeginArrayElement(layerClass);
        w.Attr("name", "string", logName);

        w.BeginArray("times", "time_array");
        for (var f = 0; f < clip.FrameCount; f++)
            w.ArrayValue(Time(f / (double)clip.Fps), last: f == clip.FrameCount - 1);
        w.EndArray();

        w.BeginArray("curvetypes", "int_array");
        w.EndArray();

        w.BeginArray("values", position ? "vector3_array" : "quaternion_array");
        // Orientation values are hemisphere-aligned on the fly (q and -q are the same
        // rotation, but the engine interpolates between DMX samples numerically — see
        // QuaternionContinuity). The clip itself is never mutated.
        var prev = System.Numerics.Quaternion.Identity;
        for (var f = 0; f < clip.FrameCount; f++)
        {
            var x = clip.Frames[f][bone];
            string value;
            if (position)
            {
                value = Vec(x);
            }
            else
            {
                var q = x.Rot;
                if (f > 0 && System.Numerics.Quaternion.Dot(prev, q) < 0f)
                    q = System.Numerics.Quaternion.Negate(q);
                prev = q;
                value = Quat(q);
            }
            w.ArrayValue(value, last: f == clip.FrameCount - 1);
        }
        w.EndArray();

        w.EmptyBinaryAttr("compressed");
        w.EndArrayElement(last: true);
        w.EndArray(); // layers

        w.Attr("curveinfo", "element", "");
        w.Attr("usedefaultvalue", "bool", "0");
        w.Attr("defaultvalue", position ? "vector3" : "quaternion", position ? "0 0 0" : "0 0 0 1");
        w.BeginArray("bookmarksX", "time_array");
        w.EndArray();
        w.BeginArray("bookmarksY", "time_array");
        w.EndArray();
        w.BeginArray("bookmarksZ", "time_array");
        w.EndArray();
        w.EndInlineAttr(); // log

        w.EndArrayElement(last);
    }

    // ---------------------------------------------------------------- formatting

    /// <summary>fbx2dmx float style: up to 10 decimal places, trailing zeros stripped,
    /// invariant culture, negative zero normalized.</summary>
    private static string F(float value)
    {
        if (value == 0f)
            return "0";
        return ((double)value).ToString("0.##########", CultureInfo.InvariantCulture);
    }

    private static string Time(double seconds)
        => seconds.ToString("0.0000", CultureInfo.InvariantCulture);

    private static string Vec(in Maths.XForm x)
        => $"{F(x.Pos.X)} {F(x.Pos.Y)} {F(x.Pos.Z)}";

    private static string Quat(in Maths.XForm x) => Quat(x.Rot);

    private static string Quat(in System.Numerics.Quaternion q)
        => $"{F(q.X)} {F(q.Y)} {F(q.Z)} {F(q.W)}";

    // ---------------------------------------------------------------- emitter

    /// <summary>
    /// Low-level keyvalues2 text emitter reproducing fbx2dmx layout quirks: CRLF endings,
    /// tab indentation, a trailing space after array-typed attribute names, and an
    /// indentation-only line after every inline element attribute closes.
    /// </summary>
    private sealed class Emitter
    {
        private readonly StringBuilder _sb = new();
        private int _indent;

        public void Raw(string text)
        {
            _sb.Append(text).Append("\r\n");
        }

        private void Line(string text)
        {
            _sb.Append('\t', _indent).Append(text).Append("\r\n");
        }

        public void Attr(string name, string type, string value)
            => Line($"\"{name}\" \"{type}\" \"{value}\"");

        public void BeginTopLevel(string className)
        {
            Line($"\"{className}\"");
            Line("{");
            _indent++;
        }

        public void EndTopLevel()
        {
            _indent--;
            Line("}");
            _sb.Append("\r\n"); // blank separator after every top-level element (incl. the last)
        }

        public void BeginInlineAttr(string name, string className)
        {
            Line($"\"{name}\" \"{className}\"");
            Line("{");
            _indent++;
        }

        public void EndInlineAttr()
        {
            _indent--;
            Line("}");
            Line(""); // indentation-only line, as fbx2dmx emits
        }

        public void BeginArrayElement(string className)
        {
            Line($"\"{className}\"");
            Line("{");
            _indent++;
        }

        public void EndArrayElement(bool last)
        {
            _indent--;
            Line(last ? "}" : "},");
        }

        public void BeginArray(string name, string type = "element_array")
        {
            Line($"\"{name}\" \"{type}\" ");
            Line("[");
            _indent++;
        }

        public void EndArray()
        {
            _indent--;
            Line("]");
        }

        public void ElementRef(string guid, bool last)
            => Line($"\"element\" \"{guid}\"" + (last ? "" : ","));

        public void ArrayValue(string value, bool last)
            => Line($"\"{value}\"" + (last ? "" : ","));

        public void EmptyBinaryAttr(string name)
        {
            Line($"\"{name}\" \"binary\" ");
            Line("\"");
            Line("\"");
        }

        public override string ToString() => _sb.ToString();
    }
}
using System;
using System.Collections.Generic;
using HumanoidRetargeter.Cleanup;
using HumanoidRetargeter.Formats;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Solve;
using HumanoidRetargeter.Target;

namespace HumanoidRetargeter;

/// <summary>Which solver retargets a request's clips (design §10).</summary>
public enum SolverKind
{
    /// <summary>The deterministic <see cref="Solve.GeometricSolver"/> (default; better
    /// wherever a role mapping exists).</summary>
    Geometric,

    /// <summary>The experimental skeleton-agnostic deep-learning solver
    /// (<see cref="Dl.DlSolver"/>, SAME pretrained checkpoint) — the no-profile fallback.
    /// Requires <see cref="RetargetTargetSpec.DlWeights"/>; ignores per-role mapping
    /// (only hips/alignment heuristics consult it) and leaves fingers at rest.</summary>
    DeepLearning,
}

/// <summary>
/// One source animation file to retarget (engine-agnostic: bytes in, no file IO). Every
/// request runs its OWN profile detection, so a single batch may mix Mixamo + ActorCore +
/// BVH sources — unless <see cref="MappingOverride"/> supplies a mapping explicitly.
/// </summary>
public sealed class RetargetRequest
{
    /// <summary>Solver choice for this request's clips. <see cref="SolverKind.DeepLearning"/>
    /// requires the batch's <see cref="RetargetTargetSpec.DlWeights"/> to be set; the
    /// conversion fails per-clip with a clear error otherwise.</summary>
    public SolverKind Solver { get; init; } = SolverKind.Geometric;

    /// <summary>Raw bytes of the source file (.fbx, .bvh, .glb, .gltf or .vrm).</summary>
    public required byte[] SourceData { get; init; }

    /// <summary>
    /// Source file name (used for the report and DMX provenance). The extension drives the
    /// format choice (<c>.fbx</c> / <c>.bvh</c> / <c>.glb</c> / <c>.gltf</c> / <c>.vrm</c> —
    /// a VRM is a glTF container whose authored humanoid bone map becomes the mapping);
    /// when the extension is unknown the content is sniffed (FBX binary magic /
    /// "FBXHeaderExtension" / BVH "HIERARCHY" / GLB 'glTF' magic / glTF JSON).
    /// </summary>
    public required string SourceFileName { get; init; }

    /// <summary>
    /// Caller-supplied identity of this request, echoed verbatim on every produced
    /// <see cref="ClipResult.SourceId"/> so callers can join results back to their own
    /// entries unambiguously (e.g. the editor window passes the FULL file path here, since
    /// two files in different folders may share the same <see cref="SourceFileName"/>).
    /// Null = <see cref="SourceFileName"/>.
    /// </summary>
    public string? SourceId { get; init; }

    /// <summary>
    /// Import sample rate the source clips are resampled to (BVH native frames / FBX curves
    /// are evaluated on this grid). Null = the importer default (30 fps).
    /// </summary>
    public float? SampleFps { get; init; }

    /// <summary>
    /// Restricts the conversion to ONE take of the source file (0-based index into the
    /// imported scene's clips). Null = convert all takes. Out of range fails the request's
    /// clip result with a clear error (the batch continues). UI listings that expand a
    /// multi-take file into one entry per take submit one request per selected take.
    /// When <see cref="ClipDefinitions"/> is set this index addresses the DEFINITIONS
    /// instead (each definition is what a UI row represents then).
    /// </summary>
    public int? TakeIndex { get; init; }

    /// <summary>
    /// Optional external clip definitions, parsed from a Unity <c>&lt;file&gt;.fbx.meta</c>
    /// sidecar (<see cref="UnityMeta.ParseClipAnimations"/>): Unity animation packs ship FBX
    /// files whose clips are sub-ranges of ONE source timeline. When set (non-empty), the
    /// conversion produces one output clip per definition instead of one per take: the
    /// definition's take (matched by <see cref="ExternalClipDef.TakeName"/>, falling back to
    /// the file's first take) is sliced to the definition's native-frame range
    /// (<see cref="UnityMeta.Slice"/>), named <see cref="ExternalClipDef.Name"/> (sanitized
    /// like take names, collision-suffixed across the batch) and looped per
    /// <see cref="ExternalClipDef.Loop"/> unless <see cref="LoopingOverride"/> is set.
    /// <see cref="TakeIndex"/> then indexes INTO this list. Null = no definitions.
    /// </summary>
    public IReadOnlyList<ExternalClipDef>? ClipDefinitions { get; init; }

    /// <summary>
    /// UI-supplied mapping (manual mapping table or a user preset loaded Editor-side).
    /// Null = auto-detect per request: preset profiles via <see cref="ProfileDetector"/>,
    /// then the <see cref="AutoMapper"/> as best-effort fallback.
    /// </summary>
    public MappingResult? MappingOverride { get; init; }

    /// <summary>Solver tunables (hip scales, finger transfer). ClipIndex/ClipName are managed
    /// by the pipeline per take and ignored here. Null = defaults.</summary>
    public SolveOptions? Solve { get; init; }

    /// <summary>
    /// Root-motion handling. <see cref="RootMotionMode.Extract"/> on a target without a
    /// dedicated animated root bone (the s&amp;box rig: pelvis is parentless, root_IK is
    /// IkBaked) leaves the frames untouched and instead sets the ExtractMotion flag on the
    /// clip's vmdl AnimFile entry — Source 2's compile-time extraction replaces the missing
    /// bone-level extraction. <see cref="RootMotionMode.InPlace"/> always operates on the
    /// hips directly.
    /// </summary>
    public RootMotionMode RootMotion { get; init; } = RootMotionMode.Off;

    /// <summary>Run the Kovar foot-plant cleanup pass on the solved frames (default on).</summary>
    public bool FootPlantCleanup { get; init; } = true;

    /// <summary>
    /// Optional arm end-effector IK pass pulling the wrists onto limb-length-normalized
    /// source hand positions. Default OFF: the geometric solver already matches anatomical
    /// directions, so arm IK only helps reach-critical work (props, contact poses) and can
    /// otherwise disturb elbow styling.
    /// </summary>
    public bool ArmEffectorIk { get; init; }

    /// <summary>
    /// Generate <c>AE_FOOTSTEP</c> AnimEvent nodes on each produced clip's vmdl AnimFile
    /// entry (default OFF). After solving and cleanup, foot-plant intervals are detected on
    /// the SOLVED target clip (<see cref="Cleanup.FootPlant.DetectPlantIntervals"/>); each
    /// plant's start frame is a touchdown and becomes one footstep event, in the exact node
    /// shape the shipped citizen data uses (see <see cref="Target.FootstepEvents"/>).
    /// Skipped (with a report note) when the target rig lacks complete leg chains.
    /// </summary>
    public bool GenerateFootstepEvents { get; init; }

    /// <summary>
    /// Additionally produce a mirrored twin of every converted clip (default OFF), named
    /// <c>&lt;clip&gt;_M</c> (collision-suffixed across the batch as usual). Mirroring runs
    /// in TARGET space on the solved clip (<see cref="Solve.ClipMirror"/>): left/right role
    /// bone channels swap and everything is reflected across the target character's sagittal
    /// plane; IK-baked helper bones are re-baked from the mirrored body afterwards.
    /// </summary>
    public bool CreateMirroredVariant { get; init; }

    /// <summary>
    /// Additionally register an additive (delta) twin of every converted clip in the
    /// generated/augmented vmdl (default OFF), named <c>&lt;clip&gt;_delta</c> (the shipped
    /// citizen naming; collision-suffixed across the batch as usual). The twin is a second
    /// AnimFile entry REUSING the clip's DMX with an <c>AnimSubtract</c> child
    /// (<c>anim_name</c> = the base sequence, <c>frame</c> = 0) — exactly the shipped
    /// <c>IdleLayer_01</c>/<c>IdleLayer_01_delta</c> pattern, where resourcecompiler
    /// subtracts the reference frame at compile time (no frame math happens here). The
    /// resulting <c>_delta</c> sequence is what s&amp;box layered animation additively
    /// blends on top of a base pose.
    /// </summary>
    public bool CreateAdditiveVariant { get; init; }

    /// <summary>Output clip name override; with multiple takes an index suffix is appended.
    /// Null = the source take name.</summary>
    public string? ClipNameOverride { get; init; }

    /// <summary>Force the looping flag on the output sequence(s); null = the source clip's flag.</summary>
    public bool? LoopingOverride { get; init; }
}

/// <summary>
/// Axis/unit convention of a <see cref="RetargetTargetSpec"/>'s rig data — drives the DMX
/// axis-system declaration, foot-plant threshold units, and the editor preview's
/// rig-space → engine-space conversion.
/// </summary>
public enum TargetUpAxis
{
    /// <summary>
    /// The s&amp;box source convention: rig authored in centimeters, Y-up (the shipped
    /// citizen rig, FBX targets). The vmdl's ScaleAndMirror 0.3937 + resourcecompiler's
    /// Y-up→Z-up conversion take it to engine space at compile time. Default.
    /// </summary>
    YUpCm,

    /// <summary>
    /// Engine space already: rig read from a compiled model's <c>Model.Bones</c>
    /// (inches, Z-up). The DMX declares a Z-up axis system so the compiler performs no
    /// further axis conversion.
    /// </summary>
    ZUpEngine,
}

/// <summary>
/// The conversion target shared by all requests of one <see cref="Retargeter.Convert"/> /
/// <see cref="Retargeter.ConvertBatch"/> call: the rig plus the vmdl generation parameters.
/// </summary>
public sealed class RetargetTargetSpec
{
    /// <summary>The s&amp;box-source → engine-units vmdl scale (cm rigs like the citizen).</summary>
    public const float SboxSourceScale = 0.3937f;

    /// <summary>The committed asset path of the s&amp;box human male model.</summary>
    public const string SboxHumanMalePath = "models/citizen_human/citizen_human_male.vmdl";

    /// <summary>The committed asset path of the classic (4-finger) s&amp;box citizen model.</summary>
    public const string SboxCitizenPath = "models/citizen/citizen.vmdl";

    /// <summary>Target rig (skeleton + bone classes + roles).</summary>
    public required TargetRig Rig { get; init; }

    /// <summary>ModelModifier_ScaleAndMirror scale written into standalone vmdls:
    /// <c>0.3937</c> for cm-authored s&amp;box-source rigs, <c>1.0</c> for engine-unit rigs
    /// (the modifier node is omitted at 1.0).</summary>
    public required float VmdlScale { get; init; }

    /// <summary>base_model_name of generated standalone vmdls (the model that owns the mesh).</summary>
    public string BaseModelPath { get; init; } = "";

    /// <summary>default_root_bone_name of the generated AnimationList (also the bone vmdl
    /// ExtractMotion nodes operate on).</summary>
    public string DefaultRootBone { get; init; } = "pelvis";

    /// <summary>
    /// Axis/unit convention of <see cref="Rig"/>. <see cref="TargetUpAxis.YUpCm"/> (default)
    /// for cm Y-up source-space rigs (DMX declares Y-up, compiler converts);
    /// <see cref="TargetUpAxis.ZUpEngine"/> for rigs read from compiled engine models
    /// (DMX declares Z-up so no double conversion happens at compile, and cm-tuned cleanup
    /// thresholds are rescaled to inches).
    /// </summary>
    public TargetUpAxis UpAxis { get; init; } = TargetUpAxis.YUpCm;

    /// <summary>
    /// Raw bytes of the committed SAME weight blob
    /// (<c>Assets/humanoid_retargeter/dl/same_v1.weights</c>; callers do the file IO).
    /// Required only when a request selects <see cref="SolverKind.DeepLearning"/>; the
    /// solver instance is built once per batch from these bytes.
    /// </summary>
    public byte[]? DlWeights { get; init; }

    /// <summary>
    /// The shipped s&amp;box default target: rig parsed from the committed
    /// <c>Assets/humanoid_retargeter/target_rig_sbox.json</c> text (callers do the file IO),
    /// 0.3937 vmdl scale, citizen human male base model, pelvis root. Pass the committed
    /// SAME weight bytes as <paramref name="dlWeights"/> to enable the deep-learning solver.
    /// </summary>
    public static RetargetTargetSpec SboxDefault(string targetRigJson, byte[]? dlWeights = null) => new()
    {
        Rig = TargetRig.SboxDefault(targetRigJson),
        VmdlScale = SboxSourceScale,
        BaseModelPath = SboxHumanMalePath,
        DefaultRootBone = "pelvis",
        DlWeights = dlWeights,
    };

    /// <summary>
    /// The classic (4-finger) s&amp;box citizen target: rig parsed from the committed
    /// <c>Assets/humanoid_retargeter/target_rig_sbox_citizen.json</c> text (callers do the
    /// file IO), 0.3937 vmdl scale, citizen base model, pelvis root, Y-up cm. The rig has no
    /// pinky bones, so pinky roles stay unassigned — the engine's own constraints handle the
    /// pinky at runtime for models that have one. Pass the committed SAME weight bytes as
    /// <paramref name="dlWeights"/> to enable the deep-learning solver.
    /// </summary>
    public static RetargetTargetSpec SboxCitizen(string targetRigJson, byte[]? dlWeights = null) => new()
    {
        Rig = TargetRig.Load(targetRigJson),
        VmdlScale = SboxSourceScale,
        BaseModelPath = SboxCitizenPath,
        DefaultRootBone = "pelvis",
        UpAxis = TargetUpAxis.YUpCm,
        DlWeights = dlWeights,
    };
}

/// <summary>Options for <see cref="Retargeter.ConvertBatch"/> output assembly.</summary>
public sealed class BatchOptions
{
    /// <summary>
    /// When set, the batch additionally augments this existing vmdl text (all successful
    /// clips spliced into its AnimationList via <see cref="VmdlAugmenter"/>) and returns the
    /// result in <see cref="RetargetBatchResult.AugmentedVmdl"/>.
    /// </summary>
    public string? AugmentVmdlText { get; init; }

    /// <summary>Assets-relative folder the DMX files will be written to by the caller; used
    /// to build each AnimFile's <c>source_filename</c>.</summary>
    public string DmxFolderRelative { get; init; } = "animations/retargeted";

    /// <summary>Auto-suffix colliding clip names (<c>_2</c>, <c>_3</c>, …) across the whole
    /// batch (default on). When off, duplicate names are kept as-is.</summary>
    public bool AutoSuffixCollisions { get; init; } = true;

    /// <summary>
    /// After conversion, scan the batch's successful clip names for directional locomotion
    /// families (default OFF): <c>_N</c>/<c>_NE</c>/…/<c>_NW</c> compass suffixes and
    /// <c>_Forward</c>/<c>_Backward</c>(/<c>_Back</c>)/<c>_Left</c>/<c>_Right</c> word forms
    /// sharing a stem. Each complete family (all four cardinals) is grouped under a Folder
    /// node with a <c>2DBlend</c> wired to the citizen <c>move_x</c>/<c>move_y</c> pose
    /// parameters, replicating the shipped citizen locomotion layout (see
    /// <see cref="Target.LocomotionSetDetector"/>); detection results land on
    /// <see cref="RetargetBatchResult.LocomotionSets"/>. Custom (non-citizen) base models
    /// must declare <c>move_x</c>/<c>move_y</c> pose parameters themselves for the blends to
    /// be drivable.
    /// </summary>
    public bool DetectLocomotionSets { get; init; }
}
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;

namespace HumanoidRetargeter.Solve;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>
/// Hand rest-geometry helpers shared by <see cref="CanonicalFrames"/> (finger secondary axes)
/// and <see cref="RestNormalizer"/> (palm-down roll correction). Everything derives from joint
/// positions only — bone local axes carry no anatomical meaning on the s&amp;box rig.
/// </summary>
internal static class HandGeometry
{
    private static readonly BoneRole[] LeftProximals =
    {
        BoneRole.ThumbProxL, BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,
    };

    private static readonly BoneRole[] RightProximals =
    {
        BoneRole.ThumbProxR, BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,
    };

    // Index → pinky order; the knuckle line is taken from the first and last mapped of these.
    private static readonly BoneRole[] LeftNonThumbProximals =
    {
        BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,
    };

    private static readonly BoneRole[] RightNonThumbProximals =
    {
        BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,
    };

    /// <summary>
    /// Midpoint of all mapped finger proximal heads of one hand (the hand's anatomical
    /// "chain child" point), or null when no finger proximal is mapped.
    /// </summary>
    public static Vector3? FingerProximalMidpoint(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)
    {
        var sum = Vector3.Zero;
        var count = 0;
        foreach (var role in left ? LeftProximals : RightProximals)
        {
            if (map.RoleToBone.TryGetValue(role, out var index))
            {
                sum += worldRest[index].Pos;
                count++;
            }
        }
        return count > 0 ? sum / count : null;
    }

    /// <summary>
    /// Dorsal palm normal of one hand: the unit vector pointing out of the <b>back</b> of the
    /// hand (away from the palm), or null when the hand/finger geometry is unmapped or
    /// degenerate.
    /// </summary>
    /// <remarks>
    /// Formula (mirror-consistent by construction, verified on the ActorCore fixture by the
    /// finger-curl test): <c>dorsal = sideSign · cross(knuckle, fingerDir)</c> with
    /// <c>sideSign = +1</c> left / <c>−1</c> right, <c>knuckle = IndexProx.head −
    /// PinkyProx.head</c> (first/last mapped non-thumb proximal), and <c>fingerDir =
    /// FingerProximalMidpoint − Hand.head</c>. On every fixture rig the thumb proximal lies on
    /// the −dorsal (palmar) side of the hand plane, grounding the sign anatomically. A positive
    /// rotation about a finger frame's hinge axis (frame Y = cross(dorsal, fingerChainDir))
    /// curls the fingertip toward the palm on <b>both</b> hands.
    /// </remarks>
    public static Vector3? Dorsal(MappingResult map, IReadOnlyList<XForm> worldRest, bool left)
    {
        if (!map.RoleToBone.TryGetValue(left ? BoneRole.HandL : BoneRole.HandR, out var handIndex))
            return null;
        var hand = worldRest[handIndex].Pos;

        var nonThumb = left ? LeftNonThumbProximals : RightNonThumbProximals;
        Vector3? first = null, last = null;
        foreach (var role in nonThumb)
        {
            if (!map.RoleToBone.TryGetValue(role, out var index))
                continue;
            first ??= worldRest[index].Pos;
            last = worldRest[index].Pos;
        }
        if (first is null || last is null || (first.Value - last.Value).LengthSquared() < 1e-8f)
            return null;

        var midpoint = FingerProximalMidpoint(map, worldRest, left);
        if (midpoint is null)
            return null;

        var knuckle = first.Value - last.Value;
        var fingerDir = midpoint.Value - hand;
        var raw = Vector3.Cross(knuckle, fingerDir) * (left ? 1f : -1f);
        return raw.LengthSquared() < 1e-8f ? null : Vector3.Normalize(raw);
    }
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "goo" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "goo" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "xaz" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "xaz.goo" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "6/14/2026 7:34:04 PM" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "25" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.116.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.116.0")]
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SmoothFloat
{
    public float Current;
    public float Target;
    public float Velocity;
    public float SmoothTime;

    public SmoothFloat(float initial, float smoothTime)
    {
        Current = initial;
        Target = initial;
        Velocity = 0f;
        SmoothTime = smoothTime;
    }

    public void Update(float dt) =>
        Current = MathX.SmoothDamp(Current, Target, ref Velocity, SmoothTime, dt);

    public bool IsSettled =>
        MathF.Abs(Target - Current) < 0.0001f && MathF.Abs(Velocity) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SpringColor
{
    public Color Current;
    public Color Target;
    public Color Velocity;
    public float Frequency;
    public float Damping;

    public SpringColor(Color initial, float frequency, float damping)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        Frequency = frequency;
        Damping = damping;
    }

    public void Update(float dt)
    {
        float vr = Velocity.r, vg = Velocity.g, vb = Velocity.b, va = Velocity.a;
        Current = new Color(
            MathX.SpringDamp(Current.r, Target.r, ref vr, dt, Frequency, Damping),
            MathX.SpringDamp(Current.g, Target.g, ref vg, dt, Frequency, Damping),
            MathX.SpringDamp(Current.b, Target.b, ref vb, dt, Frequency, Damping),
            MathX.SpringDamp(Current.a, Target.a, ref va, dt, Frequency, Damping));
        Velocity = new Color(vr, vg, vb, va);
    }

    public bool IsSettled =>
        MathF.Abs(Target.r - Current.r) < 0.0001f &&
        MathF.Abs(Target.g - Current.g) < 0.0001f &&
        MathF.Abs(Target.b - Current.b) < 0.0001f &&
        MathF.Abs(Target.a - Current.a) < 0.0001f &&
        MathF.Abs(Velocity.r) < 0.0001f &&
        MathF.Abs(Velocity.g) < 0.0001f &&
        MathF.Abs(Velocity.b) < 0.0001f &&
        MathF.Abs(Velocity.a) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SpringColor
{
    public Color Current;
    public Color Target;
    public Color Velocity;
    public float Frequency;
    public float Damping;

    public SpringColor(Color initial, float frequency, float damping)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        Frequency = frequency;
        Damping = damping;
    }

    public void Update(float dt)
    {
        float vr = Velocity.r, vg = Velocity.g, vb = Velocity.b, va = Velocity.a;
        Current = new Color(
            MathX.SpringDamp(Current.r, Target.r, ref vr, dt, Frequency, Damping),
            MathX.SpringDamp(Current.g, Target.g, ref vg, dt, Frequency, Damping),
            MathX.SpringDamp(Current.b, Target.b, ref vb, dt, Frequency, Damping),
            MathX.SpringDamp(Current.a, Target.a, ref va, dt, Frequency, Damping));
        Velocity = new Color(vr, vg, vb, va);
    }

    public bool IsSettled =>
        MathF.Abs(Target.r - Current.r) < 0.0001f &&
        MathF.Abs(Target.g - Current.g) < 0.0001f &&
        MathF.Abs(Target.b - Current.b) < 0.0001f &&
        MathF.Abs(Target.a - Current.a) < 0.0001f &&
        MathF.Abs(Velocity.r) < 0.0001f &&
        MathF.Abs(Velocity.g) < 0.0001f &&
        MathF.Abs(Velocity.b) < 0.0001f &&
        MathF.Abs(Velocity.a) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
namespace Goo.Animation;

public record struct TimelineAnimator
{
    public Timeline Timeline;
    public float    Elapsed;
    public bool     Paused;
    public float    Speed;

    public TimelineAnimator(Timeline timeline)
    {
        Timeline = timeline;
        Elapsed  = 0f;
        Paused   = false;
        Speed    = 1f;
    }

    public void Update(float dt) { if (!Paused) Elapsed += dt * Speed; }
    public void Pause()          { Paused = true; }
    public void Resume()         { Paused = false; }
    public void Restart()        { Elapsed = 0f; Paused = false; }
    public void Seek(float t)    { Elapsed = t; }

    public readonly TimelineSample Sample => Timeline.Eval(Elapsed);

    public readonly bool IsFinished
    {
        get
        {
            if (Timeline.Iterations <= 0) return false;
            if (Timeline.Duration   <= 0f) return true;
            return Elapsed >= Timeline.Duration * Timeline.Iterations;
        }
    }
}

namespace Goo;

/// <summary>Compile-time constraint for blob struct types. Never use as storage, return, or parameter type (boxes the struct, destroys per-Rebuild allocation profile); only valid in where T : struct, IBlob.</summary>
public interface IBlob
{
    static abstract BlobKind Kind { get; }
    string? Key { get; }
    internal void WriteTo(ref Frame frame);
}

/// <summary>Returns the single root Blob for a GooView build. A named delegate rather than
/// Func&lt;IBlob&gt; because IBlob has a static-abstract member (Kind) and C# bars such interfaces
/// as generic type arguments (CS8920). Consequence for Razor markup: a bare method group cannot
/// bind (its natural type is the illegal Func&lt;IBlob&gt;), so write
/// <c>Build=@(new BlobBuilder(MyBuild))</c>. See docs/site/docs/gotchas.md.</summary>
public delegate IBlob BlobBuilder();
using Sandbox.UI;

namespace Goo;

/// <summary>Length helpers for typography properties whose engine unit semantics are non-obvious.</summary>
public static class Typography
{
    /// <summary>CSS-style unitless LineHeight multiplier (e.g. 1.5 = 1.5x of FontSize).</summary>
    public static Length LineHeightMultiplier(float multiplier)
        => Length.Percent(multiplier * 100f).Value;

    /// <summary>CSS-style em-relative LetterSpacing (e.g. -0.02 for tight headings).</summary>
    public static Length LetterSpacingEm(float em)
        => Length.Em(em);
}
using System;
using System.Collections.Generic;
using Sandbox.UI;

namespace Goo.Input;

// Lazily caches one stable Action<MousePanelEvent> per key so successive Builds
// compare equal (Delegate.Equals) and emit no SetEvents op. 0 alloc on cache hit.
public sealed class HandlerTable<TKey> where TKey : notnull
{
    readonly Action<TKey, MousePanelEvent> _action;
    readonly Dictionary<TKey, Action<MousePanelEvent>> _cache = new();

    public HandlerTable(Action<TKey> action) : this((k, _) => action(k)) { }

    public HandlerTable(Action<TKey, MousePanelEvent> action) => _action = action;

    public Action<MousePanelEvent> this[TKey key]
    {
        get
        {
            if (!_cache.TryGetValue(key, out var h))
            {
                var k = key;
                h = e => _action(k, e);
                _cache[key] = h;
            }
            return h;
        }
    }
}
code xaz.goo Controls.csLibrary
using System;
using Sandbox;
using Sandbox.UI;

namespace Goo;

// Composed control factories (compose-list widgets). Stateless: each returns a
// Container subtree, following the Shapes/Skins idiom.
public static partial class Controls
{
    /// <summary>Goo primitive button: unstyled click target wrapping a text label. Pass a null onClick for a display-only button (no handler is wired). Distinct from Components.Button in the app project, which applies brand tokens and visual chrome. Use this to build custom-styled buttons without inheriting app-level styling. Style fields the factory sets (PointerEvents, FlexDirection, AlignItems, JustifyContent, and conditionally HoverBackgroundColor) cannot be overridden via <c>with</c> - first-declared wins; see Hud.Fill for the same constraint.</summary>
    public static Container Button(
        string  label,
        Action? onClick    = null,
        Color?  hoverColor = null,
        string? key        = null )
    {
        return new Container
        {
            Key                  = key,
            PointerEvents        = PointerEvents.All,
            FlexDirection        = FlexDirection.Row,
            AlignItems           = Align.Center,
            JustifyContent       = Justify.Center,
            HoverBackgroundColor = hoverColor,
            OnClick              = onClick is null ? null : _ => onClick(),
            Children             = { new Text( label ) },
        };
    }


    // Maps a cursor X (in the root's rendered pixel frame) to a snapped, clamped value.
    // Ratio-based (localX / width) so it is scale-invariant; engine UI scaling changes
    // the absolute pixels but not the ratio (engine-fact-mousepanelevent-rendered-frame).
    internal static float ValueAt(float localX, float width, float min, float max, float step)
    {
        if (width <= 0f || max <= min) return min;
        float norm = Math.Clamp(localX / width, 0f, 1f);
        float v = min + norm * (max - min);
        if (step > 0f) v = MathF.Round(v / step) * step;
        return Math.Clamp(v, min, max);
    }

    // Opacity multiplier applied to a Container when Disabled = true.
    public const float DisabledOpacity = 0.45f;

    // Canonical disabled dimming; multiplies any declared opacity.
    internal static float ResolveDisabledOpacity(float? declaredOpacity)
        => (declaredOpacity ?? 1f) * DisabledOpacity;

    // Dev-diagnostic sink for the degenerate max<=min case (mirrors Skins.OnZeroBorder).
    public static Action<string>? OnDegenerateRange;

    static readonly Color TrackBg = new( 0.28f, 0.28f, 0.34f, 1f );
    static readonly Color FillBg  = new( 0.55f, 0.78f, 0.95f, 1f );
    static readonly Color ThumbBg = new( 0.95f, 0.96f, 1.00f, 1f );

    // Controlled, stateless horizontal slider: the caller owns value, updates it in onChanged, and re-renders. Press-to-jump + drag within the bar (no engine move-capture; engine-fact-sbox-ui-input-and-drag); key pins reconciler identity so the Active pointer survives per-move re-renders.
    // disabled: when true, sets Disabled on the root container (forces pointer-off and opacity dim); onChanged is not called. Callers that wrap this in their own disabled container must pass disabled=true here and omit Disabled on the wrapper to avoid double-dim (0.45 x 0.45).
    public static Container Slider(
        float value, float min, float max, float step,
        Action<float> onChanged, string? key = null, bool disabled = false )
    {
        if ( max <= min )
            OnDegenerateRange?.Invoke( $"Goo.Controls.Slider: max ({max}) <= min ({min}); rendering an inert track." );

        float pct = (max > min ? Math.Clamp( (value - min) / (max - min), 0f, 1f ) : 0f) * 100f;

        void Set( MousePanelEvent e )
            => onChanged( ValueAt( e.LocalPosition.x, e.Target.Box.Rect.Size.x, min, max, step ) );

        return new Container
        {
            Key            = key,
            Disabled       = disabled ? (bool?)true : null,
            PointerEvents  = PointerEvents.All,
            Width          = Length.Percent( 100 ),
            Height         = 20f,
            FlexDirection  = FlexDirection.Column,
            JustifyContent = Justify.Center,
            OnMouseDown    = Set,                                   // press jumps to position
            OnMouseMove    = e => { if ( e.Target.HasActive ) Set( e ); }, // drag while pressed
            Children =
            {
                // track/fill/thumb are inert: handler-less, variant-less panels resolve to
                // PointerEvents.None, so the slider parent stays e.Target for press/drag.
                new Container
                {
                    Key             = "track",
                    Position        = PositionMode.Relative,        // positioned ancestor for fill/thumb
                    Width           = Length.Percent( 100 ),
                    Height          = 7f,
                    BorderRadius    = 4f,
                    BackgroundColor = TrackBg,
                    Children =
                    {
                        new Container
                        {
                            Key             = "fill",
                            Position        = PositionMode.Absolute,
                            Left            = 0f,
                            Height          = Length.Percent( 100 ),
                            Width           = Length.Percent( pct ),
                            BorderRadius    = 4f,
                            BackgroundColor = FillBg,
                        },
                        new Container
                        {
                            Key             = "thumb",
                            Position        = PositionMode.Absolute,
                            Left            = Length.Percent( pct ),
                            Top             = Length.Percent( 50 ),
                            Width           = 16f,
                            Height          = 16f,
                            BorderRadius    = Length.Percent( 50 ),
                            BackgroundColor = ThumbBg,
                            // Center the thumb on the (x = value, y = track-mid) point.
                            // Length.Percent returns Length?, never null here; ?? default unwraps
                            // it the same way Px.Of does (the codebase's nullable-Length idiom).
                            Transform       = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( -50 ) ?? default ),
                        },
                    },
                },
            },
        };
    }
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Sandbox;

namespace Goo;

internal sealed class Fiber
{
    public BlobKind Kind;
    public string? Key;
    public string Content = "";
    public StyleList Style = StyleList.Empty;
    public List<Fiber>? Children;
    public Texture? Texture;
    public string? Path;          // Image (texture path), ScenePanel (.scene path), SvgPanel (svg path), or WebPanel (URL)
    public Scene? Scene;
    public bool RenderOnce;
    public bool Paused;             // WebPanel only
    public string? Color;
    // TextEntry-only carry-fields. Path slot is reused for the text value.
    public string? Placeholder;
    public int?    MaxLength;
    public bool    Disabled;
    public bool    Numeric;
    public float?  MinValue;
    public float?  MaxValue;
    public string? NumberFormat;
    public bool    Multiline;
    public int?    MinLength;
    public string? CharacterRegex;
    public string? StringRegex;
    public Func<char, bool>?   CanEnterChar;
    public Func<string, bool>? Validate;
    public Action<bool>?       OnValidationChanged;
    public Action<string>? OnChange;
    public Action<string>? OnSubmit;
    public Action?         OnFocus;
    public Action<string>? OnBlur;
    public Action?         OnCancel;
    public bool    IsControlled;
    public BlobEvents PrevEvents;
    public ShapeParams Shape;       // Sector / Arc only
    public Vector2[]? Points;       // Polygon only
    // Custom-shader channel (Container only). Previous-render snapshot for delta comparison.
    public ShaderEffect? Effect;
    // Custom-draw callback (Container only). Previous-render snapshot for delta comparison.
    public DrawCallback? Draw;
    // Declared layout-move transition (Container only). Previous-render snapshot for delta comparison.
    public LayoutTransition? LayoutTransition;
    public object? Instance;        // Cell only: the persistent self-owning state instance
}
// <auto-generated />
// Generated by tools/StyleFacadeEmit. Do not edit by hand.
// Source of truth: tools/StyleFacadeEmit/style-manifest.json
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

public readonly partial record struct Sector
{
    public Length? Width { init => _style = StyleAccumulator.Add(_style, StyleField.Width, value); }
    public Length? Height { init => _style = StyleAccumulator.Add(_style, StyleField.Height, value); }
    public Length? Margin { init => _style = StyleAccumulator.Add(_style, StyleField.Margin, value); }
    public Length? MarginLeft { init => _style = StyleAccumulator.Add(_style, StyleField.MarginLeft, value); }
    public Length? MarginTop { init => _style = StyleAccumulator.Add(_style, StyleField.MarginTop, value); }
    public Length? MarginRight { init => _style = StyleAccumulator.Add(_style, StyleField.MarginRight, value); }
    public Length? MarginBottom { init => _style = StyleAccumulator.Add(_style, StyleField.MarginBottom, value); }
    public Color? BackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundColor, value, StyleValue.FromColor); }
    public Color? BackgroundTint { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundTint, value, StyleValue.FromColor); }
    public Length? Bottom { init => _style = StyleAccumulator.Add(_style, StyleField.Bottom, value); }
    public Length? Left { init => _style = StyleAccumulator.Add(_style, StyleField.Left, value); }
    public Length? Right { init => _style = StyleAccumulator.Add(_style, StyleField.Right, value); }
    public Length? Top { init => _style = StyleAccumulator.Add(_style, StyleField.Top, value); }
    public Length? FlexBasis { init => _style = StyleAccumulator.Add(_style, StyleField.FlexBasis, value); }
    public float? FlexGrow { init => _style = StyleAccumulator.Add(_style, StyleField.FlexGrow, value); }
    public float? FlexShrink { init => _style = StyleAccumulator.Add(_style, StyleField.FlexShrink, value); }
    public Length? MaxHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MaxHeight, value); }
    public Length? MaxWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MaxWidth, value); }
    public Length? MinHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MinHeight, value); }
    public Length? MinWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MinWidth, value); }
    public float? Opacity { init => _style = StyleAccumulator.Add(_style, StyleField.Opacity, value); }
    public PointerEvents? PointerEvents { init => _style = StyleAccumulator.Add(_style, StyleField.PointerEvents, value, StyleValue.FromPointerEvents); }
    public PositionMode? Position { init => _style = StyleAccumulator.Add(_style, StyleField.Position, value, StyleValue.FromPositionMode); }
    public Goo.PanelTransform? Transform { init => _style = StyleAccumulator.Add(_style, StyleField.Transform, value, StyleValue.FromPanelTransform); }
}
using System;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

internal enum StyleField : ushort
{
    None = 0,
    FlexDirection, JustifyContent, AlignItems, Display,
    Width, Height,
    Padding, PaddingLeft, PaddingTop, PaddingRight, PaddingBottom,
    Margin, MarginLeft, MarginTop, MarginRight, MarginBottom,
    Gap, RowGap, ColumnGap,
    BackgroundColor,
    BorderRadius, BorderTopLeftRadius, BorderTopRightRadius,
    BorderBottomRightRadius, BorderBottomLeftRadius,
    // Extended to cover remaining settable PanelStyle properties.
    AlignContent, AlignSelf,
    AspectRatio,
    BackdropFilterBlur, BackdropFilterBrightness, BackdropFilterContrast,
    BackdropFilterHueRotate, BackdropFilterInvert, BackdropFilterSaturate, BackdropFilterSepia,
    BackgroundAngle, BackgroundBlendMode, BackgroundImage, BackgroundPlaybackPaused,
    BackgroundPositionX, BackgroundPositionY, BackgroundRepeat,
    BackgroundSizeX, BackgroundSizeY, BackgroundTint,
    BorderBottomColor, BorderBottomWidth,
    BorderColor,
    BorderImageFill, BorderImageRepeat, BorderImageSource, BorderImageTint,
    BorderImageWidthBottom, BorderImageWidthLeft, BorderImageWidthRight, BorderImageWidthTop,
    BorderLeftColor, BorderLeftWidth,
    BorderRightColor, BorderRightWidth,
    BorderTopColor, BorderTopWidth,
    BorderWidth,
    Bottom,
    CaretColor,
    Cursor,
    FilterBlur, FilterBorderColor, FilterBorderWidth,
    FilterBrightness, FilterContrast, FilterHueRotate, FilterInvert, FilterSaturate,
    FilterSepia, FilterTint,
    FlexBasis, FlexGrow, FlexShrink, FlexWrap,
    FontColor, FontFamily, FontSize,
    FontSmooth, FontStyle, FontVariantNumeric, FontWeight,
    ImageRendering,
    Left, LetterSpacing, LineHeight,
    MaskAngle, MaskImage, MaskMode, MaskPositionX, MaskPositionY,
    MaskRepeat, MaskScope, MaskSizeX, MaskSizeY,
    MaxHeight, MaxWidth, MinHeight, MinWidth,
    MixBlendMode,
    ObjectFit,
    Opacity,
    Order,
    OutlineColor, OutlineOffset, OutlineWidth,
    Overflow, OverflowX, OverflowY,
    PerspectiveOriginX, PerspectiveOriginY,
    PointerEvents,
    Position,
    Right,
    SoundIn, SoundOut,
    TextAlign,
    TextBackgroundAngle,
    TextDecorationColor, TextDecorationLine, TextDecorationSkipInk,
    TextDecorationStyle, TextDecorationThickness,
    TextFilter,
    TextLineThroughOffset,
    TextOverflow, TextOverlineOffset,
    TextStrokeColor, TextStrokeWidth,
    TextTransform,
    TextUnderlineOffset,
    Top,
    Transform,
    TransformOriginX, TransformOriginY,
    WhiteSpace,
    WordBreak, WordSpacing,
    ZIndex,
    HoverBackgroundColor, ActiveBackgroundColor, FocusBackgroundColor,
    HoverFontColor,       ActiveFontColor,       FocusFontColor,
    TransitionMs,
    Disabled,
}

internal enum StyleValueKind : byte
{
    None = 0,
    Length,
    Color,
    FlexDirection,
    Justify,
    Align,
    DisplayMode,
    String,
    Single,
    Boolean,
    Texture,
    Int32,
    Wrap,
    BackgroundRepeat,
    BorderImageFill,
    BorderImageRepeat,
    FontSmooth,
    FontStyle,
    FontVariantNumeric,
    ImageRendering,
    MaskMode,
    MaskScope,
    ObjectFit,
    OverflowMode,
    PointerEvents,
    PositionMode,
    TextAlign,
    TextDecoration,
    TextDecorationStyle,
    TextSkipInk,
    FilterMode,
    TextOverflow,
    TextTransform,
    WhiteSpace,
    WordBreak,
    PanelTransform,
}

internal struct StyleValue : IEquatable<StyleValue>
{
    public StyleValueKind Kind;
    public Length LengthVal;     // valid when Kind == Length
    public Color ColorVal;       // valid when Kind == Color
    public int EnumVal;          // packed enum value for any enum-typed kind
    public int RefSlot;          // packed value for Single (float bits), Boolean (0/1), Int32 (raw)
    public object? RefVal;       // String or Texture reference

    public static StyleValue FromLength(Length v) => new() { Kind = StyleValueKind.Length, LengthVal = v };
    public static StyleValue FromColor(Color v)   => new() { Kind = StyleValueKind.Color, ColorVal = v };
    public static StyleValue FromFlexDirection(FlexDirection v) => new() { Kind = StyleValueKind.FlexDirection, EnumVal = (int)v };
    public static StyleValue FromJustify(Justify v) => new() { Kind = StyleValueKind.Justify, EnumVal = (int)v };
    public static StyleValue FromAlign(Align v)     => new() { Kind = StyleValueKind.Align, EnumVal = (int)v };
    public static StyleValue FromDisplay(DisplayMode v) => new() { Kind = StyleValueKind.DisplayMode, EnumVal = (int)v };

    public static StyleValue FromString(string v) => new() { Kind = StyleValueKind.String, RefVal = v };
    public static StyleValue FromSingle(float v)  => new() { Kind = StyleValueKind.Single, RefSlot = BitConverter.SingleToInt32Bits(v) };
    public static StyleValue FromBoolean(bool v)  => new() { Kind = StyleValueKind.Boolean, RefSlot = v ? 1 : 0 };
    public static StyleValue FromInt32(int v)     => new() { Kind = StyleValueKind.Int32, RefSlot = v };
    public static StyleValue FromTexture(Texture v) => new() { Kind = StyleValueKind.Texture, RefVal = v };
    public static StyleValue FromWrap(Wrap v) => new() { Kind = StyleValueKind.Wrap, EnumVal = (int)v };
    public static StyleValue FromBackgroundRepeat(BackgroundRepeat v) => new() { Kind = StyleValueKind.BackgroundRepeat, EnumVal = (int)v };
    public static StyleValue FromBorderImageFill(BorderImageFill v) => new() { Kind = StyleValueKind.BorderImageFill, EnumVal = (int)v };
    public static StyleValue FromBorderImageRepeat(BorderImageRepeat v) => new() { Kind = StyleValueKind.BorderImageRepeat, EnumVal = (int)v };
    public static StyleValue FromFontSmooth(FontSmooth v) => new() { Kind = StyleValueKind.FontSmooth, EnumVal = (int)v };
    public static StyleValue FromFontStyle(FontStyle v) => new() { Kind = StyleValueKind.FontStyle, EnumVal = (int)v };
    public static StyleValue FromFontVariantNumeric(FontVariantNumeric v) => new() { Kind = StyleValueKind.FontVariantNumeric, EnumVal = (int)v };
    public static StyleValue FromImageRendering(ImageRendering v) => new() { Kind = StyleValueKind.ImageRendering, EnumVal = (int)v };
    public static StyleValue FromMaskMode(MaskMode v) => new() { Kind = StyleValueKind.MaskMode, EnumVal = (int)v };
    public static StyleValue FromMaskScope(MaskScope v) => new() { Kind = StyleValueKind.MaskScope, EnumVal = (int)v };
    public static StyleValue FromObjectFit(ObjectFit v) => new() { Kind = StyleValueKind.ObjectFit, EnumVal = (int)v };
    public static StyleValue FromOverflowMode(OverflowMode v) => new() { Kind = StyleValueKind.OverflowMode, EnumVal = (int)v };
    public static StyleValue FromPointerEvents(PointerEvents v) => new() { Kind = StyleValueKind.PointerEvents, EnumVal = (int)v };
    public static StyleValue FromPositionMode(PositionMode v) => new() { Kind = StyleValueKind.PositionMode, EnumVal = (int)v };
    public static StyleValue FromTextAlign(TextAlign v) => new() { Kind = StyleValueKind.TextAlign, EnumVal = (int)v };
    public static StyleValue FromTextDecoration(TextDecoration v) => new() { Kind = StyleValueKind.TextDecoration, EnumVal = (int)v };
    public static StyleValue FromTextDecorationStyle(TextDecorationStyle v) => new() { Kind = StyleValueKind.TextDecorationStyle, EnumVal = (int)v };
    public static StyleValue FromTextSkipInk(TextSkipInk v) => new() { Kind = StyleValueKind.TextSkipInk, EnumVal = (int)v };
    public static StyleValue FromFilterMode(FilterMode v) => new() { Kind = StyleValueKind.FilterMode, EnumVal = (int)v };
    public static StyleValue FromTextOverflow(TextOverflow v) => new() { Kind = StyleValueKind.TextOverflow, EnumVal = (int)v };
    public static StyleValue FromTextTransform(TextTransform v) => new() { Kind = StyleValueKind.TextTransform, EnumVal = (int)v };
    public static StyleValue FromWhiteSpace(WhiteSpace v) => new() { Kind = StyleValueKind.WhiteSpace, EnumVal = (int)v };
    public static StyleValue FromWordBreak(WordBreak v) => new() { Kind = StyleValueKind.WordBreak, EnumVal = (int)v };

    public static StyleValue FromPanelTransform(Goo.PanelTransform v)
        => new() { Kind = StyleValueKind.PanelTransform, RefVal = v._entries };

    public bool Equals(StyleValue other)
    {
        if (Kind != other.Kind) return false;
        return Kind switch
        {
            StyleValueKind.Length         => LengthVal.Equals(other.LengthVal),
            StyleValueKind.Color          => ColorVal.Equals(other.ColorVal),
            StyleValueKind.String         => ReferenceEquals(RefVal, other.RefVal),
            StyleValueKind.Texture        => ReferenceEquals(RefVal, other.RefVal),
            StyleValueKind.Single         => RefSlot == other.RefSlot,
            StyleValueKind.Boolean        => RefSlot == other.RefSlot,
            StyleValueKind.Int32          => RefSlot == other.RefSlot,
            StyleValueKind.PanelTransform => Goo.PanelTransform.EntriesEqual(
                RefVal as System.Collections.Immutable.ImmutableList<Sandbox.UI.PanelTransform.Entry>,
                other.RefVal as System.Collections.Immutable.ImmutableList<Sandbox.UI.PanelTransform.Entry>),
            _                             => EnumVal == other.EnumVal,
        };
    }

    public override bool Equals(object? obj) => obj is StyleValue v && Equals(v);
    public override int GetHashCode() => Kind switch
    {
        // PanelTransform stores an ImmutableList in RefVal; Equals is structural via
        // EntriesEqual, so the hash must be structural too, hashing RefVal as object
        // would give reference identity and break the Equals/GetHashCode contract.
        StyleValueKind.PanelTransform => HashCode.Combine(
            (byte)Kind,
            Goo.PanelTransform.ComputeEntriesHash(
                RefVal as System.Collections.Immutable.ImmutableList<Sandbox.UI.PanelTransform.Entry>)),
        _ => HashCode.Combine((byte)Kind, LengthVal.GetHashCode(), ColorVal.GetHashCode(), EnumVal, RefSlot, RefVal),
    };
}

internal struct StyleEntry
{
    public StyleField Field;
    public StyleValue Value;
}

internal sealed class StyleList
{
    internal static readonly StyleList Empty = new(readOnly: true);

    StyleEntry[] _items;
    int _count;
    readonly bool _readonly;

    public StyleList() : this(readOnly: false) { }
    StyleList(bool readOnly) { _items = new StyleEntry[4]; _count = 0; _readonly = readOnly; }

    public int Count => _count;
    public ref StyleEntry this[int i] => ref _items[i];

    public void Add(StyleField field, StyleValue value)
    {
        if (_readonly) throw new InvalidOperationException("Cannot mutate StyleList.Empty sentinel.");
        if (_count == _items.Length) Array.Resize(ref _items, _items.Length * 2);
        _items[_count++] = new StyleEntry { Field = field, Value = value };
    }

    public bool TryGet(StyleField field, out StyleValue value)
    {
        for (int i = 0; i < _count; i++)
            if (_items[i].Field == field) { value = _items[i].Value; return true; }
        value = default;
        return false;
    }

    internal void Reset()
    {
        if (_readonly) return;
        Array.Clear(_items, 0, _count);
        _count = 0;
    }

    internal void CopyFrom(StyleList src)
    {
        if (_readonly) throw new InvalidOperationException("Cannot mutate StyleList.Empty sentinel.");
        if (_items.Length < src._count) _items = new StyleEntry[src._items.Length];
        Array.Copy(src._items, _items, src._count);
        _count = src._count;
    }

    public static bool ContentsEqual(StyleList a, StyleList b)
    {
        if (ReferenceEquals(a, b)) return true;
        if (a._count != b._count) return false;
        for (int i = 0; i < a._count; i++)
            if (a._items[i].Field != b._items[i].Field || !a._items[i].Value.Equals(b._items[i].Value))
                return false;
        return true;
    }
}
using System;
using System.Collections.Generic;

namespace Goo;

// Lexically-scoped dynamic token lookup. Push a dict via Scope, read via Get inside the body.
public static class Tokens
{
    [ThreadStatic] private static Stack<IReadOnlyDictionary<string, object>>? _stack;

    public static T Scope<T>(IReadOnlyDictionary<string, object> tokens, Func<T> body)
    {
        var stack = _stack ??= new Stack<IReadOnlyDictionary<string, object>>();
        stack.Push(tokens);
        try { return body(); }
        finally { stack.Pop(); }
    }

    public static T Get<T>(string key)
    {
        if (_stack != null)
        {
            foreach (var dict in _stack)
            {
                if (dict.TryGetValue(key, out var value))
                    return (T)value;
            }
        }
        throw new KeyNotFoundException($"Token '{key}' not found in any active Scope.");
    }

    public static bool TryGet<T>(string key, out T value)
    {
        if (_stack != null)
        {
            foreach (var dict in _stack)
            {
                if (dict.TryGetValue(key, out var obj) && obj is T typed)
                {
                    value = typed;
                    return true;
                }
            }
        }
        value = default!;
        return false;
    }
}