3087 results

global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Twitch Poop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "twitchpoop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "garry" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "garry.twitchpoop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "6/6/2026 7:39:31 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.128.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.128.0")]
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
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 Editor;
using Sandbox;
using Sandbox.Helpers;
using System.Collections.Generic;
using System.Linq;

namespace SFXR.Editor;

[CustomEditor( typeof( List<SFXRSequencer.Note> ) )]
public class SFXRNotesListControlWidget : ControlWidget
{
    private SerializedCollection Collection;

    private Layout Content;

    private Button addButton;

    public override bool SupportsMultiEdit => false;

    SFXRSequencer Sequencer;

    public SFXRNotesListControlWidget( SerializedProperty property )
        : base( property )
    {
        SetSizeMode( SizeMode.Ignore, SizeMode.Ignore );

        base.Layout = Layout.Column();
        base.Layout.Spacing = 2f;
        if ( property.TryGetAsObject( out var obj ) && obj is SerializedCollection collection )
        {
            if ( property.Parent.Targets.First() is SFXRSequencer sequencer )
            {
                Sequencer = sequencer;
            }

            Collection = collection;
            Collection.OnEntryAdded = Rebuild;
            Collection.OnEntryRemoved = Rebuild;
            Content = Layout.Column();
            base.Layout.Add( Content );
            Layout layout = base.Layout.AddRow();
            layout.Margin = 8;
            layout.AddStretchCell();
            addButton = layout.Add( new Button( "Add Note" )
            {
                ToolTip = "Add new note",
            } );
            addButton.MinimumWidth = 200;
            addButton.Clicked = () => AddEntry();
            layout.AddStretchCell();
            Rebuild();
        }
    }

    public void Rebuild()
    {
        Content.Clear( deleteWidgets: true );
        Content.Margin = 0f;
        Layout layout = Layout.Column();
        layout.Spacing = 2f;
        int num = 0;
        int count = Collection.Count();
        for ( int i = 0; i < count; i++ )
        {
            var item = Collection.ElementAt( i );
            int index = num;
            var itemLayout = Layout.Row();
            itemLayout.Spacing = 4f;
            // try to get object
            if ( item.TryGetAsObject( out var obj ) )
            {
                var thing = new SFXRNoteSheet( obj );
                itemLayout.Add( thing );
            }
            else
            {
                var thing = ControlWidget.Create( item );
                thing.MinimumHeight = 100;
                itemLayout.Add( thing );
            }

            var buttonLayout = Layout.Column();
            if ( i > 0 )
            {
                buttonLayout.Add( new IconButton( "arrow_upward", delegate
                {
                    MoveUp( index );
                } )
                {
                    Background = Color.Transparent,
                    FixedWidth = ControlWidget.ControlRowHeight,
                    FixedHeight = ControlWidget.ControlRowHeight,
                    ToolTip = "Move note up"
                } );
            }
            else
            {
                buttonLayout.AddSpacingCell( 25 );
            }

            buttonLayout.Add( new IconButton( "delete", delegate
            {
                RemoveEntry( index );
            } )
            {
                Background = Color.Red,
                FixedWidth = ControlWidget.ControlRowHeight,
                FixedHeight = ControlWidget.ControlRowHeight,
                ToolTip = "Delete note"
            } );

            if ( i < count - 1 )
            {
                buttonLayout.Add( new IconButton( "arrow_downward", delegate
                {
                    MoveDown( index );
                } )
                {
                    Background = Color.Transparent,
                    FixedWidth = ControlWidget.ControlRowHeight,
                    FixedHeight = ControlWidget.ControlRowHeight,
                    ToolTip = "Move note down"
                } );
            }
            else
            {
                buttonLayout.AddSpacingCell( 25 );
            }

            itemLayout.Add( buttonLayout );
            layout.Add( itemLayout );
            num++;
        }

        MinimumHeight = 50 + (num * 105);

        Content.Add( layout );
        Content.Margin = ((num > 0) ? 3 : 0);
    }

    private void AddEntry()
    {
        Collection.Add( new SFXRSequencer.Note() );
    }

    private void RemoveEntry( int index )
    {
        Collection.RemoveAt( index );
    }

    private void MoveUp( int index )
    {
        // Move the index up in Sequencer.Notes list
        if ( index > 0 )
        {
            var note = Sequencer.Notes[index];
            Sequencer.Notes.RemoveAt( index );
            Sequencer.Notes.Insert( index - 1, note );
        }

        Rebuild();
    }

    private void MoveDown( int index )
    {
        // Move the index down in Sequencer.Notes list
        if ( index < Sequencer.Notes.Count - 1 )
        {
            var note = Sequencer.Notes[index];
            Sequencer.Notes.RemoveAt( index );
            Sequencer.Notes.Insert( index + 1, note );
        }

        Rebuild();
    }

    protected override void OnPaint()
    {

    }

    public void AddEffectDialog( Button source )
    {
        var s = new SFXREffectTypeSelector( this );
        s.OnSelect += ( t ) => AddEffect( t );
        s.OpenAt( source.ScreenRect.BottomLeft, animateOffset: new Vector2( 0, -4 ) );
        s.FixedWidth = source.Width;
    }

    void AddEffect( TypeDescription type )
    {
        if ( !type.TargetType.IsAssignableTo( typeof( SFXREffect ) ) )
        {
            Log.Error( $"Type {type.TargetType} is not assignable to {typeof( SFXREffect )}" );
            return;
        }

        SFXREffect effect = type.Create<SFXREffect>();
        Collection.Add( effect );

        Log.Info( effect );
    }


}
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 Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}

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 Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
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; }

}
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);
        }
    }
}
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 Editor;

public static class MyEditorMenu
{
	[Menu( "Editor", "CrosshairBuilder/My Menu Option" )]
	public static void OpenMyMenu()
	{
		EditorUtility.DisplayDialog( "It worked!", "This is being called from your library's editor code!" );
	}
}
using Braxnet;
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			// var go = new GameObject();
			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
			Assert.IsTrue( scene.Directory.FindByName( "LibraryTestComponent" ) != null );
		}
	}

}

[Autoload]
public class LibraryTestComponent : Component
{
	
}
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 System;
global using System.Linq;
global using System.Collections.Generic;
global using Editor;
global using Sandbox;
global using PathTool;
global using Application = Editor.Application;
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.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
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;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using Editor;
using Sandbox;
using System.Text;

class SpectogramWidget : Widget 
{
    private short[] samples;
    private int sampleRate;
    private List<int> splitPoints = new List<int>();
    private int? dragPoint = null;
    private Label loadingLabel;
    private Label dropLabel;
    private bool isLoading = true;
    public SoundFile CurrentSound { get; private set; }

    public SpectogramWidget(SoundFile soundFile) : base(null)
    {
        MinimumSize = 100;
        MouseTracking = true;
        AcceptDrops = true;

        loadingLabel = new Label(this);
        loadingLabel.Text = "Loading audio data...";
        loadingLabel.Visible = false;
        
        dropLabel = new Label(this);
        dropLabel.Text = "Drop a sound file here";
        dropLabel.SetStyles("font-size: 18px; color: #aaa; text-align: center;");
        
        if (soundFile != null)
        {
            LoadSound(soundFile);
        }
    }

    public async void LoadSound(SoundFile soundFile)
    {
        CurrentSound = soundFile;
        isLoading = true;
        samples = null;
        splitPoints.Clear();
        
        loadingLabel.Visible = true;
        dropLabel.Visible = false;

        await LoadAudioDataAsync(soundFile);
    }

    private async Task LoadAudioDataAsync(SoundFile soundFile)
    {
        try 
        {
            await soundFile.LoadAsync();
            samples = await soundFile.GetSamplesAsync();
            
            if (samples == null)
            {
                loadingLabel.Text = "Failed to load audio data";
                return;
            }

            sampleRate = soundFile.Rate;
            splitPoints.Add(0);
            splitPoints.Add(samples.Length - 1);

            loadingLabel.Visible = false;
            isLoading = false;

            Update();
        }
        catch (Exception ex)
        {
            loadingLabel.Text = $"Error loading audio: {ex.Message}";
        }
    }

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

        if (loadingLabel != null)
        {
            loadingLabel.Position = new Vector2(10, Height / 2 - 10);
            loadingLabel.Size = new Vector2(Width - 20, 20);
        }

        if (dropLabel != null)
        {
            dropLabel.Position = new Vector2(10, Height / 2 - 10);
            dropLabel.Size = new Vector2(Width - 20, 20);
        }
    }

    public override void OnDragDrop(DragEvent e)
    {
        base.OnDragDrop(e);

        if (!e.Data.HasFileOrFolder) return;

        var asset = AssetSystem.FindByPath(e.Data.FileOrFolder);
        if (asset?.AssetType != AssetType.SoundFile) return;

        var soundFile = SoundFile.Load(asset.Path);
        if (soundFile != null)
        {
            LoadSound(soundFile);
        }
    }

    public override void OnDragHover(DragEvent e)
    {
        base.OnDragHover(e);

        if (!e.Data.HasFileOrFolder) return;

        var asset = AssetSystem.FindByPath(e.Data.FileOrFolder);
        if (asset?.AssetType != AssetType.SoundFile) return;

        e.Action = DropAction.Link;
    }

    protected override void OnMouseClick(MouseEvent e)
    {
        if (isLoading) return;
        
        base.OnMouseClick(e);

        if (e.Button == MouseButtons.Left)
        {
            var samplePos = (int)(e.LocalPosition.x / Width * samples.Length);
            var nearPoint = splitPoints.FirstOrDefault(p => Math.Abs(p - samplePos) < (samples.Length / Width * 5));
            
            if (nearPoint != default)
            {
                dragPoint = splitPoints.IndexOf(nearPoint);
            }
            else
            {
                splitPoints.Add(samplePos);
                splitPoints.Sort();
                Update();
            }
        }
    }

    protected override void OnMouseMove(MouseEvent e)
    {
        if (isLoading) return;
        
        base.OnMouseMove(e);

        if (dragPoint.HasValue)
        {
            var samplePos = (int)(e.LocalPosition.x / Width * samples.Length);
            splitPoints[dragPoint.Value] = samplePos;
            splitPoints.Sort();
            Update();
        }
    }

    protected override void OnMouseReleased(MouseEvent e)
    {
        if (isLoading) return;
        
        base.OnMouseReleased(e);
        dragPoint = null;
    }

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

        if (isLoading || samples == null)
        {
            return;
        }

        Paint.ClearPen();
        Paint.SetBrush(Theme.Grey.WithAlpha(0.1f));
        Paint.DrawRect(LocalRect);

        Paint.SetPen(Theme.Blue);
        var samplesPerPixel = samples.Length / Width;
        for (int x = 0; x < Width; x++)
        {
            var startSample = (int)(x * samplesPerPixel);
            var endSample = Math.Min(startSample + samplesPerPixel, samples.Length);
            
            var max = short.MinValue;
            var min = short.MaxValue;
            
            for (int i = startSample; i < endSample; i++)
            {
                max = Math.Max(max, samples[i]);
                min = Math.Min(min, samples[i]);
            }

            var y1 = Height / 2 + (min / (float)short.MaxValue * Height / 2);
            var y2 = Height / 2 + (max / (float)short.MaxValue * Height / 2);
            
            Paint.DrawLine(new Vector2(x, y1), new Vector2(x, y2));
        }

        Paint.SetPen(Theme.Red);
        foreach (var point in splitPoints)
        {
            var x = point / (float)samples.Length * Width;
            Paint.DrawLine(new Vector2(x, 0), new Vector2(x, Height));
        }
    }

    public List<int> GetSplitPoints()
    {
        return new List<int>(splitPoints);
    }

    public void SplitCurrentSound(Action<SoundFile> onSoundCreated)
    {
        if (CurrentSound == null || samples == null) return;

        var splitPoints = GetSplitPoints();
        if (splitPoints.Count < 2) return;

        try
        {
            var baseFileName = Path.GetFileNameWithoutExtension(CurrentSound.ResourcePath);
            var outputDir = Path.Combine(
                Project.Current.GetAssetsPath(),
                "generated",
                $"{baseFileName}_splits"
            );
            Directory.CreateDirectory(outputDir);

            for (int i = 0; i < splitPoints.Count - 1; i++)
            {
                var start = splitPoints[i];
                var end = splitPoints[i + 1];
                var length = end - start;

                var segmentSamples = new short[length];
                Array.Copy(samples, start, segmentSamples, 0, length);

                var wavPath = Path.Combine(outputDir, $"{baseFileName}_part_{i + 1}.wav");

                using (var writer = new BinaryWriter(File.Create(wavPath)))
                {
                    writer.Write(Encoding.ASCII.GetBytes("RIFF"));
                    writer.Write(36 + (segmentSamples.Length * 2));
                    writer.Write(Encoding.ASCII.GetBytes("WAVE"));

                    writer.Write(Encoding.ASCII.GetBytes("fmt "));
                    writer.Write(16);
                    writer.Write((short)1);
                    writer.Write((short)CurrentSound.Channels);
                    writer.Write(CurrentSound.Rate);
                    writer.Write(CurrentSound.Rate * CurrentSound.Channels * 2);
                    writer.Write((short)(CurrentSound.Channels * 2));
                    writer.Write((short)16);

                    writer.Write(Encoding.ASCII.GetBytes("data"));
                    writer.Write(segmentSamples.Length * 2);

                    foreach (var sample in segmentSamples)
                    {
                        writer.Write(sample);
                    }
                }

                var asset = AssetSystem.RegisterFile(wavPath);
                if (asset != null)
                {
                    var soundFile = SoundFile.Load(asset.RelativePath);
                    if (soundFile != null)
                    {
                        onSoundCreated?.Invoke(soundFile);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Log.Error($"Error splitting sound: {ex.Message}");
        }
    }
}
using System;

namespace SboxMcp.Registry;

public enum ToolCategory
{
	Scene,
	GameObject,
	Component,
	Prefab,
	Asset,
	ModelDoc,
	AnimGraph,
	ShaderGraph,
	ActionGraph,
	Code,
	Editor,
	Retargeter,
	Cloud,
	Imported
}

/// <summary>
/// Marks a static method as an MCP tool. The registry reflects the method's
/// parameters into a JSON Schema and exposes it via tools/list.
/// </summary>
[AttributeUsage( AttributeTargets.Method )]
public sealed class McpToolAttribute : Attribute
{
	public string Name { get; }
	public string Description { get; }
	public ToolCategory Category { get; }

	/// <summary>Write tools are subject to the permission gate (approve-writes / read-only modes).</summary>
	public bool Writes { get; init; }

	/// <summary>
	/// Optional requirement key (e.g. an integration's library ident). The host
	/// resolves it via ToolRegistry.RequirementResolver; unresolved tools are
	/// hidden from clients and shown disabled in the tool browser.
	/// </summary>
	public string Requires { get; init; }

	/// <summary>
	/// Ships disabled; the user must enable it in the tool browser. Used for
	/// tools with external effects (e.g. downloading cloud assets).
	/// </summary>
	public bool DisabledByDefault { get; init; }

	public McpToolAttribute( string name, string description, ToolCategory category )
	{
		Name = name;
		Description = description;
		Category = category;
	}
}

/// <summary>
/// Optional description for a tool parameter, surfaced in the JSON Schema.
/// </summary>
[AttributeUsage( AttributeTargets.Parameter )]
public sealed class DescAttribute : Attribute
{
	public string Text { get; }
	public DescAttribute( string text ) { Text = text; }
}

/// <summary>
/// Thrown when tool arguments are missing or cannot be bound; surfaced to the
/// MCP client as an isError tool result.
/// </summary>
public sealed class ToolArgumentException : Exception
{
	public ToolArgumentException( string message, Exception inner = null ) : base( message, inner ) { }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using SboxMcp.Server;

namespace SboxMcp.Registry;

/// <summary>
/// A discovered [McpTool] method, with its generated descriptor and an
/// argument-binding invoker.
/// </summary>
public sealed class RegisteredTool
{
	public McpToolAttribute Meta { get; }
	public MethodInfo Method { get; }
	public McpToolDescriptor Descriptor { get; }

	/// <summary>
	/// Why this tool cannot run right now ("Disabled", "Not Installed", ...),
	/// or null when it is available. Evaluated live so user toggles and
	/// integrations installed mid-session apply without a restart.
	/// </summary>
	public string UnavailableReason
	{
		get
		{
			if ( ToolRegistry.DisabledResolver?.Invoke( this ) ?? Meta.DisabledByDefault )
				return "Disabled";

			return Meta.Requires is null ? null : ToolRegistry.RequirementResolver?.Invoke( Meta.Requires );
		}
	}

	public bool IsAvailable => UnavailableReason is null;

	internal RegisteredTool( McpToolAttribute meta, MethodInfo method )
	{
		Meta = meta;
		Method = method;
		Descriptor = new McpToolDescriptor( meta.Name, BuildDescription( meta ), SchemaGenerator.ForMethod( method ) );
	}

	static string BuildDescription( McpToolAttribute meta ) =>
		meta.Writes ? $"{meta.Description} (modifies project state)" : meta.Description;

	/// <summary>
	/// Binds JSON arguments to the method's parameters by name and invokes it.
	/// Throws ToolArgumentException on missing/unbindable arguments.
	/// </summary>
	public object Invoke( JsonElement? args )
	{
		var parameters = Method.GetParameters();
		var bound = new object[parameters.Length];

		for ( var i = 0; i < parameters.Length; i++ )
		{
			var p = parameters[i];

			// JsonElement params accept explicit null (e.g. to clear a reference
			// property); for typed params null falls through to the default
			if ( args is { ValueKind: JsonValueKind.Object } a && a.TryGetProperty( p.Name, out var value )
				&& (value.ValueKind != JsonValueKind.Null || p.ParameterType == typeof( JsonElement )) )
			{
				try
				{
					bound[i] = p.ParameterType == typeof( JsonElement )
						? value.Clone()
						: value.Deserialize( p.ParameterType, ToolRegistry.BindOptions );
				}
				catch ( Exception e ) when ( e is JsonException or NotSupportedException )
				{
					throw new ToolArgumentException(
						$"Argument '{p.Name}' could not be read as {p.ParameterType.Name}: {e.Message}", e );
				}
			}
			else if ( p.HasDefaultValue )
			{
				bound[i] = p.DefaultValue;
			}
			else
			{
				throw new ToolArgumentException( $"Missing required argument '{p.Name}'" );
			}
		}

		try
		{
			return Method.Invoke( null, bound );
		}
		catch ( TargetInvocationException e ) when ( e.InnerException is not null )
		{
			throw e.InnerException;
		}
	}
}

/// <summary>
/// Discovers [McpTool] static methods and serves them to the MCP server.
/// </summary>
public sealed class ToolRegistry
{
	/// <summary>
	/// Maps a tool's Requires key to an unavailability reason (short, e.g.
	/// "Not Installed") or null when the requirement is satisfied. Null
	/// resolver = everything available.
	/// </summary>
	public static Func<string, string> RequirementResolver { get; set; }

	/// <summary>
	/// Whether the user has disabled this tool. Null resolver = only
	/// DisabledByDefault applies.
	/// </summary>
	public static Func<RegisteredTool, bool> DisabledResolver { get; set; }

	internal static readonly JsonSerializerOptions BindOptions = new()
	{
		PropertyNameCaseInsensitive = true,
		Converters = { new JsonStringEnumConverter() }
	};

	static readonly JsonSerializerOptions ResultOptions = new()
	{
		WriteIndented = true,
		Converters = { new JsonStringEnumConverter() }
	};

	readonly List<RegisteredTool> _tools = new();
	readonly Dictionary<string, RegisteredTool> _byName = new( StringComparer.Ordinal );

	public IReadOnlyList<RegisteredTool> Tools => _tools;

	public void AddAssembly( Assembly assembly )
	{
		var methods = assembly.GetTypes()
			.Where( t => t.IsClass )
			.SelectMany( t => t.GetMethods( BindingFlags.Public | BindingFlags.Static ) )
			.Select( m => (Method: m, Meta: m.GetCustomAttribute<McpToolAttribute>()) )
			.Where( x => x.Meta is not null )
			.OrderBy( x => x.Meta.Name, StringComparer.Ordinal );

		foreach ( var (method, meta) in methods )
		{
			if ( _byName.ContainsKey( meta.Name ) )
				continue;

			var tool = new RegisteredTool( meta, method );
			_tools.Add( tool );
			_byName[meta.Name] = tool;
		}
	}

	public RegisteredTool Find( string name ) => _byName.GetValueOrDefault( name );

	/// <summary>
	/// Registers an arbitrary public static method (from another library) as a
	/// tool. Returns null when the name is already taken.
	/// </summary>
	public RegisteredTool AddImported( string name, string description, ToolCategory category, MethodInfo method )
	{
		if ( _byName.ContainsKey( name ) )
			return null;

		var meta = new McpToolAttribute( name, description, category ) { Writes = true };
		var tool = new RegisteredTool( meta, method );
		_tools.Add( tool );
		_byName[name] = tool;
		return tool;
	}

	public void Remove( string name )
	{
		if ( _byName.Remove( name, out var tool ) )
			_tools.Remove( tool );
	}

	/// <summary>
	/// Converts a tool's return value to the text sent back to the client.
	/// </summary>
	public static string FormatResult( object result ) => result switch
	{
		null => """{ "ok": true }""",
		string s => s,
		_ => JsonSerializer.Serialize( result, ResultOptions )
	};
}
using System;
using Editor;
using Sandbox;
using SboxMcp.Registry;
using static SboxMcp.Tools.ToolHelpers;

namespace SboxMcp.Tools;

public static class PrefabTools
{
	[McpTool( "prefab_instantiate", "Instantiates a prefab into the active scene.", ToolCategory.Prefab, Writes = true )]
	public static object Instantiate(
		[Desc( "Prefab asset path, e.g. 'prefabs/door.prefab'" )] string prefabPath,
		[Desc( "World position [x, y, z]" )] float[] position = null )
	{
		var session = RequireSession();

		var prefabFile = ResourceLibrary.Get<PrefabFile>( prefabPath )
			?? throw new InvalidOperationException( $"No prefab at '{prefabPath}' - use asset_search with assetType 'prefab'" );

		var prefabScene = SceneUtility.GetPrefabScene( prefabFile )
			?? throw new InvalidOperationException( $"Prefab '{prefabPath}' could not be loaded" );

		using var undo = session.UndoScope( $"MCP: instantiate {prefabPath}" ).WithGameObjectCreations().Push();

		var transform = position is null
			? global::Transform.Zero
			: new Transform( ToVector3( position, "position" ) );

		var instance = prefabScene.Clone( transform );
		return Describe( instance );
	}

	[McpTool( "prefab_create_from_gameobject", "Turns a GameObject (and its children) into a reusable .prefab asset; the original becomes an instance of it.", ToolCategory.Prefab, Writes = true )]
	public static object CreateFromGameObject(
		[Desc( "GameObject id or unique name" )] string gameObject,
		[Desc( "Output path ending in .prefab, e.g. 'prefabs/door.prefab'" )] string prefabPath )
	{
		if ( !prefabPath.EndsWith( ".prefab", StringComparison.OrdinalIgnoreCase ) )
			throw new ArgumentException( "prefabPath must end in .prefab" );

		var session = RequireSession();
		var go = FindGameObject( gameObject );
		var absolute = AssetTools.ResolveNewAssetPath( prefabPath );

		if ( System.IO.File.Exists( absolute ) )
			throw new InvalidOperationException( $"'{prefabPath}' already exists" );

		System.IO.Directory.CreateDirectory( System.IO.Path.GetDirectoryName( absolute ) );

		using var undo = session.UndoScope( $"MCP: create prefab {prefabPath}" )
			.WithGameObjectChanges( go, GameObjectUndoFlags.All ).Push();

		EditorUtility.Prefabs.ConvertGameObjectToPrefab( go, absolute );

		return new { created = prefabPath, instanceId = go.Id };
	}

	[McpTool( "prefab_break_instance", "Unlinks a prefab instance so it becomes plain GameObjects.", ToolCategory.Prefab, Writes = true )]
	public static object BreakInstance( [Desc( "GameObject id or unique name of the prefab instance root" )] string gameObject )
	{
		var session = RequireSession();
		var go = FindGameObject( gameObject );

		if ( !go.IsPrefabInstance )
			throw new InvalidOperationException( $"'{go.Name}' is not a prefab instance" );

		using var undo = session.UndoScope( "MCP: break prefab instance" )
			.WithGameObjectChanges( go, GameObjectUndoFlags.All ).Push();

		go.BreakFromPrefab();
		return Describe( go );
	}

	[McpTool( "prefab_update_from_prefab", "Re-syncs a prefab instance from its source prefab file.", ToolCategory.Prefab, Writes = true )]
	public static object UpdateFromPrefab( [Desc( "GameObject id or unique name of the prefab instance root" )] string gameObject )
	{
		var session = RequireSession();
		var go = FindGameObject( gameObject );

		if ( !go.IsPrefabInstance )
			throw new InvalidOperationException( $"'{go.Name}' is not a prefab instance" );

		using var undo = session.UndoScope( "MCP: update from prefab" )
			.WithGameObjectChanges( go, GameObjectUndoFlags.All ).Push();

		go.UpdateFromPrefab();
		return Describe( go );
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Editor;
using Sandbox;
using SboxMcp.Integration;

namespace SboxMcp.UI;

/// <summary>
/// Pick public static methods from installed libraries (and other loaded
/// code) to expose as MCP tools. Searchable; libraries are listed separately
/// from everything else. Choices apply immediately and persist.
/// </summary>
public class ImportToolsDialog : Dialog
{
	readonly LineEdit _search;
	readonly ScrollArea _scroll;

	public ImportToolsDialog( Widget parent ) : base( parent )
	{
		Window.WindowTitle = "Import Tools From Library";
		Window.SetWindowIcon( "library_add" );
		Window.SetModal( true, true );
		Window.MinimumWidth = 560;
		Window.MinimumHeight = 480;

		Layout = Layout.Column();
		Layout.Margin = 16;
		Layout.Spacing = 8;

		var hint = Layout.Add( new Label(
			"Expose public static methods from installed libraries as MCP tools. "
			+ "Imported tools persist, re-bind every session, and are write-gated by approvals.", this ) );
		hint.SetStyles( $"color: {Theme.TextLight.Hex}; font-size: 11px;" );
		hint.WordWrap = true;

		_search = Layout.Add( new LineEdit( this ) { PlaceholderText = "Search methods, types or libraries..." } );
		_search.TextEdited += _ => Rebuild();

		_scroll = new ScrollArea( this );
		_scroll.Canvas = new Widget( _scroll );
		_scroll.Canvas.Layout = Layout.Column();
		_scroll.Canvas.Layout.Spacing = 2;
		_scroll.Canvas.Layout.Margin = 4;
		_scroll.Canvas.VerticalSizeMode = SizeMode.CanGrow;
		_scroll.Canvas.HorizontalSizeMode = SizeMode.Flexible;
		Layout.Add( _scroll, 1 );

		var buttons = Layout.AddRow();
		buttons.AddStretchCell();
		var done = buttons.Add( new Button.Primary( "Done" ) { Icon = "check" } );
		done.Clicked = Close; // Dialog.Close closes the host window (Destroy leaves it black)

		Rebuild();
	}

	void Rebuild()
	{
		var canvas = _scroll.Canvas;
		canvas.Layout.Clear( true );

		var query = _search.Text;
		var candidates = ToolImporter.CandidateAssemblies().ToList();

		AddSection( canvas, "Libraries", "extension",
			candidates.Where( ToolImporter.IsLibraryAssembly ).ToList(), query );

		AddSection( canvas, "Project & Other", "folder",
			candidates.Where( a => !ToolImporter.IsLibraryAssembly( a ) ).ToList(), query );

		canvas.Layout.AddStretchCell();
	}

	void AddSection( Widget canvas, string title, string icon, List<Assembly> assemblies, string query )
	{
		var header = canvas.Layout.Add( new Label( title, canvas ) );
		header.SetStyles( $"color: {Theme.Blue.Hex}; font-size: 12px; font-weight: 700; margin-top: 8px;" );

		var any = false;

		foreach ( var assembly in assemblies )
		{
			var methods = ToolImporter.CandidateMethods( assembly )
				.Where( m => Matches( assembly, m, query ) )
				.Take( 60 )
				.ToList();

			if ( methods.Count == 0 )
				continue;

			any = true;

			var name = canvas.Layout.Add( new Label( ToolImporter.FriendlyName( assembly ), canvas ) );
			name.SetStyles( $"color: {Theme.Text.Hex}; font-size: 11px; font-weight: 600; margin-top: 4px; margin-left: 6px;" );

			foreach ( var method in methods )
			{
				var parameters = string.Join( ", ", method.GetParameters().Select( p => p.Name ) );
				var check = canvas.Layout.Add( new Checkbox( $"{method.DeclaringType?.Name}.{method.Name}({parameters})", canvas )
				{
					Value = ToolImporter.IsImported( method )
				} );
				check.ToolTip = method.DeclaringType?.FullName;

				var captured = method;
				check.Clicked = () =>
				{
					if ( check.Value )
						ToolImporter.Import( captured );
					else
						ToolImporter.Unimport( captured );
				};
			}
		}

		if ( !any )
		{
			var empty = canvas.Layout.Add( new Label(
				string.IsNullOrWhiteSpace( query ) ? "Nothing importable found." : "No matches.", canvas ) );
			empty.SetStyles( $"color: {Theme.TextLight.Hex}; font-size: 11px; margin-left: 6px;" );
		}
	}

	static bool Matches( Assembly assembly, MethodInfo method, string query )
	{
		if ( string.IsNullOrWhiteSpace( query ) )
			return true;

		return method.Name.Contains( query, StringComparison.OrdinalIgnoreCase )
			|| (method.DeclaringType?.Name.Contains( query, StringComparison.OrdinalIgnoreCase ) ?? false)
			|| ToolImporter.FriendlyName( assembly ).Contains( query, StringComparison.OrdinalIgnoreCase );
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
using SboxMcp.Integration;
using SboxMcp.Registry;

namespace SboxMcp.UI;

/// <summary>
/// Searchable, category-filterable browser of every tool the server exposes.
/// Doubles as documentation.
/// </summary>
public class ToolsPage : Widget
{
	readonly LineEdit _search;
	readonly List<CategoryChip> _chips = new();
	readonly ScrollArea _scroll;

	int _builtSignature = -1;

	public ToolsPage( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();
		Layout.Margin = 12;
		Layout.Spacing = 8;

		var searchRow = Layout.AddRow();
		searchRow.Spacing = 6;

		_search = searchRow.Add( new LineEdit( this ) { PlaceholderText = "Search tools..." }, 1 );
		_search.TextEdited += _ => Rebuild();

		var import = searchRow.Add( new Button( "Import Tools", "library_add" ) );
		import.ToolTip = "Expose public static methods from other installed libraries as MCP tools";
		import.Clicked = () => new ImportToolsDialog( this ).Show();

		// FlowRow wraps the chips to new lines on narrow docks instead of
		// letting them overlap
		var chipFlow = Layout.Add( new FlowRow( this ) );

		foreach ( var category in Enum.GetValues<ToolCategory>() )
		{
			var chip = new CategoryChip( category, chipFlow, clickable: true );
			chip.OnToggled = Rebuild;
			_chips.Add( chip );
			chipFlow.AddItem( chip );
		}

		_scroll = new ScrollArea( this );
		_scroll.Canvas = new Widget( _scroll );
		_scroll.Canvas.Layout = Layout.Column();
		_scroll.Canvas.Layout.Spacing = 2;
		_scroll.Canvas.VerticalSizeMode = SizeMode.CanGrow;
		_scroll.Canvas.HorizontalSizeMode = SizeMode.Flexible;
		Layout.Add( _scroll, 1 );

		Rebuild();
	}

	/// <summary>
	/// The dock restores before McpHost initializes, so the registry is empty
	/// at construction time - poll until tools appear.
	/// </summary>
	public void Tick()
	{
		var sig = Signature();
		if ( sig == _builtSignature )
			return;

		Rebuild();
	}

	static int Signature()
	{
		var tools = McpHost.Registry?.Tools;
		return tools is null ? 0 : tools.Count * 1000 + tools.Count( t => t.IsAvailable );
	}

	void Rebuild()
	{
		_builtSignature = Signature();

		var canvas = _scroll.Canvas;
		canvas.Layout.Clear( true );

		var query = _search.Text;
		var enabled = _chips.Where( c => c.Toggled ).Select( c => c.Category ).ToHashSet();

		var tools = (McpHost.Registry?.Tools ?? (IReadOnlyList<RegisteredTool>)Array.Empty<RegisteredTool>())
			.Where( t => enabled.Contains( t.Meta.Category ) )
			.Where( t => string.IsNullOrWhiteSpace( query )
				|| t.Meta.Name.Contains( query, StringComparison.OrdinalIgnoreCase )
				|| t.Meta.Description.Contains( query, StringComparison.OrdinalIgnoreCase ) )
			.ToList();

		var count = canvas.Layout.Add( new Label( $"{tools.Count} tools", canvas ) );
		count.SetStyles( $"color: {Palette.TextDim.Hex}; font-size: 10px;" );

		foreach ( var tool in tools )
			canvas.Layout.Add( new ToolRow( tool, canvas ) );

		canvas.Layout.AddStretchCell();
	}
}

/// <summary>
/// One tool entry: name (mono), write badge, wrapped description.
/// </summary>
public class ToolRow : Widget
{
	const float ToggleWidth = 40;

	readonly RegisteredTool _tool;

	public ToolRow( RegisteredTool tool, Widget parent ) : base( parent )
	{
		_tool = tool;
		FixedHeight = 40;
		ToolTip = tool.Meta.Description + "\n\nClick the toggle to enable/disable this tool.";
	}

	bool UserDisabled => McpSettings.GetToolDisabledOverride( _tool.Meta.Name ) ?? _tool.Meta.DisabledByDefault;

	protected override void OnMouseClick( MouseEvent e )
	{
		base.OnMouseClick( e );

		// the toggle lives in the right strip of the row
		if ( e.LocalPosition.x < LocalRect.Right - ToggleWidth )
			return;

		McpSettings.SetToolDisabled( _tool.Meta.Name, !UserDisabled );
		Update();
	}

	protected override void OnPaint()
	{
		Paint.Antialiasing = true;
		Paint.ClearPen();

		var unavailable = _tool.UnavailableReason;
		var disabled = unavailable is not null;
		var accent = Palette.For( _tool.Meta.Category );

		if ( disabled )
			accent = accent.WithAlpha( 0.35f );

		if ( Paint.HasMouseOver && !disabled )
		{
			Paint.SetBrush( Color.White.WithAlpha( 0.03f ) );
			Paint.DrawRect( LocalRect, 5 );
		}

		// category color tick
		Paint.SetBrush( accent );
		Paint.DrawRect( new Rect( LocalRect.Left + 2, LocalRect.Top + 8, 3, LocalRect.Height - 16 ), 1.5f );

		// name
		Paint.SetPen( disabled ? Palette.TextDim.WithAlpha( 0.6f ) : Palette.TextBright );
		Paint.SetFont( "Consolas", 8, 600 );
		var nameWidth = Paint.MeasureText( _tool.Meta.Name ).x;
		Paint.DrawText( new Rect( LocalRect.Left + 14, LocalRect.Top + 4, nameWidth + 4, 14 ), _tool.Meta.Name, TextFlag.LeftCenter );

		var badgeLeft = LocalRect.Left + 20 + nameWidth;

		// writes badge
		if ( _tool.Meta.Writes && !disabled )
		{
			var badge = new Rect( badgeLeft, LocalRect.Top + 5, 44, 13 );
			Paint.SetBrush( Palette.Error.WithAlpha( 0.18f ) );
			Paint.DrawRect( badge, 6 );
			Paint.SetPen( Palette.Error );
			Paint.SetDefaultFont( 6, 700 );
			Paint.DrawText( badge, "WRITES", TextFlag.Center );
		}

		// unavailable badge, e.g. "Not Installed"
		if ( disabled )
		{
			Paint.SetDefaultFont( 6, 700 );
			var badgeWidth = Paint.MeasureText( unavailable ).x + 12;
			var badge = new Rect( badgeLeft, LocalRect.Top + 5, badgeWidth, 13 );
			Paint.SetBrush( Palette.TextDim.WithAlpha( 0.15f ) );
			Paint.DrawRect( badge, 6 );
			Paint.SetPen( Palette.TextDim );
			Paint.DrawText( badge, unavailable, TextFlag.Center );
		}

		// description
		Paint.SetPen( disabled ? Palette.TextDim.WithAlpha( 0.5f ) : Palette.TextDim );
		Paint.SetDefaultFont( 7 );
		Paint.DrawText( new Rect( LocalRect.Left + 14, LocalRect.Top + 20, LocalRect.Width - ToggleWidth - 20, 14 ),
			_tool.Meta.Description, TextFlag.LeftCenter | TextFlag.SingleLine );

		// enable/disable toggle (persisted per tool)
		var off = UserDisabled;
		Paint.SetPen( off ? Palette.TextDim : Theme.Green );
		Paint.DrawIcon( new Rect( LocalRect.Right - ToggleWidth, LocalRect.Top, ToggleWidth - 8, LocalRect.Height ),
			off ? "toggle_off" : "toggle_on", 22, TextFlag.Center );
	}
}
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;
        }
    }
}