796 results

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 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!" );
	}
}

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

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

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

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

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

			pusher.Collect( position, ref vec );
		}

		return vec;
	}

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

		delta.z = 0; // ignore z

		var distanceDelta = (delta.Length / Radius);

		output += delta.Normal * (1.0f - distanceDelta);
	}
}

using System;

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

}
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.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 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
{
	
}
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 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 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}");
        }
    }
}
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 Sandbox;
using System.Collections.Generic;

namespace Coroutines;

/// <summary>
/// Represents an instance of a running coroutine.
/// </summary>
internal sealed class CoroutineInstance
{
	/// <summary>
	/// The coroutine that is being executed.
	/// </summary>
	internal IEnumerator<ICoroutineStaller> Coroutine { get; }
	/// <summary>
	/// Whether or not the coroutine has finished.
	/// </summary>
	internal bool IsFinished { get; private set; }

	/// <summary>
	/// Returns the current polling stage of the coroutine.
	/// </summary>
	internal GameObjectSystem.Stage CurrentPollingStage
	{
		get
		{
			if ( CurrentStall.PollingStage == Coroutines.Coroutine.PreservePollingStage )
				return LastPollingStage;

			return CurrentStall.PollingStage;
		}
	}
	/// <summary>
	/// The last valid polling stage that was used.
	/// </summary>
	private GameObjectSystem.Stage LastPollingStage { get; set; }
	/// <summary>
	/// Returns the current staller of the coroutine.
	/// </summary>
	private ICoroutineStaller CurrentStall => Coroutine.Current;

	/// <summary>
	/// Initializes a new instance of <see cref="CoroutineInstance"/>.
	/// </summary>
	/// <param name="coroutine">The coroutine to execute.</param>
	internal CoroutineInstance( IEnumerator<ICoroutineStaller> coroutine )
	{
		LastPollingStage = Coroutines.Coroutine.DefaultPollingStage;

		Coroutine = coroutine;
		IsFinished = !coroutine.MoveNext();
	}

	/// <summary>
	/// Updates the state of the coroutine.
	/// </summary>
	internal void Update()
	{
		if ( IsFinished )
			return;

		CurrentStall.Update();
		if ( !CurrentStall.IsComplete )
			return;

		if ( !Coroutine.MoveNext() || CurrentStall is null )
		{
			IsFinished = true;
			return;
		}

		if ( CurrentStall.PollingStage != Coroutines.Coroutine.PreservePollingStage )
			LastPollingStage = CurrentStall.PollingStage;
	}
}
using Sandbox;

namespace Mongo.Rest;

public static class SceneExtensions
{
	public static IMongoRepository<T>? GetRepositoryFrom<T>( this Scene scene ) where T : class
	{
		var system = scene.GetSystem<MongoRestSystem>();
		return system.GetRepositoryFrom<T>();
	}

	public static T? GetRepository<T>( this Scene scene ) where T : class, IMongoRepository
	{
		var system = scene.GetSystem<MongoRestSystem>();
		return system.GetRepository<T>();
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Mongo.Rest;

public sealed class MongoRestSystem : GameObjectSystem
{
	private bool _initialized;

	public readonly Dictionary<Type, IMongoRepository> Repositories = new();

	public IMongoRestOptions Options { get; private set; } = new MongoRestOptions
	{
		Url = "https://localhost:443",
		Database = "Orizon"
	};
	
	public MongoRestSystem( Scene scene ) : base( scene )
	{
		Listen( Stage.SceneLoaded, -1, Initialize, nameof(MongoRestSystem) );
	}

	public void Initialize()
	{
		if ( _initialized ) return;
		_initialized = true;

		Repositories.Clear();
		
		var repositories = MongoHelper.GetRepositories().ToList();
		Log.Info( $"Registered {repositories.Count} repositories" );
		
		foreach ( var repository in repositories )
			Repositories.Add( repository.GetInnerType(), repository );
	}

	public void Configure( Action<MongoRestOptions> options )
	{
		var opt = new MongoRestOptions();
		options( opt );

		Options = opt;
	}

	public IMongoRepository<T>? GetRepositoryFrom<T>() where T : class
	{
		Repositories.TryGetValue( typeof(T), out var repository );
		return repository as IMongoRepository<T>;
	}
	
	public T? GetRepository<T>() where T : class, IMongoRepository
	{
		return Repositories.Values.FirstOrDefault(x => x.GetType() == typeof(T)) as T;
	}
}
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; }

}
global using Microsoft.AspNetCore.Components; 
global using Microsoft.AspNetCore.Components.Rendering;
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 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();
	}
}
using System.Collections.Generic;

namespace Ink.Parsed
{
    // Used by the FlowBase when constructing the weave flow from
    // a flat list of content objects.
    public class Weave : Parsed.Object
    {
        // Containers can be chained as multiple gather points
        // get created as the same indentation level.
        // rootContainer is always the first in the chain, while
        // currentContainer is the latest.
        public Runtime.Container rootContainer { 
            get {
                if (_rootContainer == null) {
                    GenerateRuntimeObject ();
                }

                return _rootContainer;
            }
        }
        Runtime.Container currentContainer { get; set; }

		public int baseIndentIndex { get; private set; }

        // Loose ends are:
        //  - Choices or Gathers that need to be joined up
        //  - Explicit Divert to gather points (i.e. "->" without a target)
        public List<IWeavePoint> looseEnds;

        public List<GatherPointToResolve> gatherPointsToResolve;
        public class GatherPointToResolve
        {
            public Runtime.Divert divert;
            public Runtime.Object targetRuntimeObj;
        }

        public Parsed.Object lastParsedSignificantObject
        {
            get {
                if (content.Count == 0) return null;

                // Don't count extraneous newlines or VAR/CONST declarations,
                // since they're "empty" statements outside of the main flow.
                Parsed.Object lastObject = null;
                for (int i = content.Count - 1; i >= 0; --i) {
                    lastObject = content [i];

                    var lastText = lastObject as Parsed.Text;
                    if (lastText && lastText.text == "\n") {
                        continue;
                    }

                    if (IsGlobalDeclaration (lastObject))
                        continue;
                    
                    break;
                }

                var lastWeave = lastObject as Weave;
                if (lastWeave)
                    lastObject = lastWeave.lastParsedSignificantObject;
                
                return lastObject;
            }
        }
                        
        public Weave(List<Parsed.Object> cont, int indentIndex=-1) 
        {
            if (indentIndex == -1) {
                baseIndentIndex = DetermineBaseIndentationFromContent (cont);
            } else {
                baseIndentIndex = indentIndex;
            }

            AddContent (cont);

            ConstructWeaveHierarchyFromIndentation ();
        }

        public void ResolveWeavePointNaming ()
        {
            var namedWeavePoints = FindAll<IWeavePoint> (w => !string.IsNullOrEmpty (w.name));

            _namedWeavePoints = new Dictionary<string, IWeavePoint> ();

            foreach (var weavePoint in namedWeavePoints) {

                // Check for weave point naming collisions
                IWeavePoint existingWeavePoint;
                if (_namedWeavePoints.TryGetValue (weavePoint.name, out existingWeavePoint)) {
                    var typeName = existingWeavePoint is Gather ? "gather" : "choice";
                    var existingObj = (Parsed.Object)existingWeavePoint;

                    Error ("A " + typeName + " with the same label name '" + weavePoint.name + "' already exists in this context on line " + existingObj.debugMetadata.startLineNumber, (Parsed.Object)weavePoint);
                }

                _namedWeavePoints [weavePoint.name] = weavePoint;
            }
        }

        void ConstructWeaveHierarchyFromIndentation()
        {
            // Find nested indentation and convert to a proper object hierarchy
            // (i.e. indented content is replaced with a Weave object that contains
            // that nested content)
            int contentIdx = 0;
            while (contentIdx < content.Count) {

                Parsed.Object obj = content [contentIdx];

                // Choice or Gather
                if (obj is IWeavePoint) {
                    var weavePoint = (IWeavePoint)obj;
                    var weaveIndentIdx = weavePoint.indentationDepth - 1;

                    // Inner level indentation - recurse
                    if (weaveIndentIdx > baseIndentIndex) {

                        // Step through content until indent jumps out again
                        int innerWeaveStartIdx = contentIdx;
                        while (contentIdx < content.Count) {
                            var innerWeaveObj = content [contentIdx] as IWeavePoint;
                            if (innerWeaveObj != null) {
                                var innerIndentIdx = innerWeaveObj.indentationDepth - 1;
                                if (innerIndentIdx <= baseIndentIndex) {
                                    break;
                                }
                            }

                            contentIdx++;
                        }

                        int weaveContentCount = contentIdx - innerWeaveStartIdx;

                        var weaveContent = content.GetRange (innerWeaveStartIdx, weaveContentCount);
                        content.RemoveRange (innerWeaveStartIdx, weaveContentCount);

                        var weave = new Weave (weaveContent, weaveIndentIdx);
                        InsertContent (innerWeaveStartIdx, weave);

                        // Continue iteration from this point
                        contentIdx = innerWeaveStartIdx;
                    }

                } 

                contentIdx++;
            }
        }
            
        // When the indentation wasn't told to us at construction time using
        // a choice point with a known indentation level, we may be told to
        // determine the indentation level by incrementing from our closest ancestor.
        public int DetermineBaseIndentationFromContent(List<Parsed.Object> contentList)
        {
            foreach (var obj in contentList) {
                if (obj is IWeavePoint) {
                    return ((IWeavePoint)obj).indentationDepth - 1;
                }
            }

            // No weave points, so it doesn't matter
            return 0;
        }

        public override Runtime.Object GenerateRuntimeObject ()
        {
            _rootContainer = currentContainer = new Runtime.Container();
            looseEnds = new List<IWeavePoint> ();

            gatherPointsToResolve = new List<GatherPointToResolve> ();

            // Iterate through content for the block at this level of indentation
            //  - Normal content is nested under Choices and Gathers
            //  - Blocks that are further indented cause recursion
            //  - Keep track of loose ends so that they can be diverted to Gathers
            foreach(var obj in content) {

                // Choice or Gather
                if (obj is IWeavePoint) {
                    AddRuntimeForWeavePoint ((IWeavePoint)obj);
                } 

                // Non-weave point
                else {

                    // Nested weave
                    if (obj is Weave) {
                        var weave = (Weave)obj;
                        AddRuntimeForNestedWeave (weave);
                        gatherPointsToResolve.AddRange (weave.gatherPointsToResolve);
                    }

                    // Other object
                    // May be complex object that contains statements - e.g. a multi-line conditional
                    else {
                        AddGeneralRuntimeContent (obj.runtimeObject);
                    }
                }
            }

            // Pass any loose ends up the hierarhcy
            PassLooseEndsToAncestors();

            return _rootContainer;
        }

        // Found gather point:
        //  - gather any loose ends
        //  - set the gather as the main container to dump new content in
        void AddRuntimeForGather(Gather gather)
        {
            // Determine whether this Gather should be auto-entered:
            //  - It is auto-entered if there were no choices in the last section
            //  - A section is "since the previous gather" - so reset now
            bool autoEnter = !hasSeenChoiceInSection;
            hasSeenChoiceInSection = false;

            var gatherContainer = gather.runtimeContainer;

            if (gather.name == null) {
                // Use disallowed character so it's impossible to have a name collision
                gatherContainer.name = "g-" + _unnamedGatherCount;
                _unnamedGatherCount++;
            }
                
            // Auto-enter: include in main content
            if (autoEnter) {
                currentContainer.AddContent (gatherContainer);
            } 

            // Don't auto-enter:
            // Add this gather to the main content, but only accessible
            // by name so that it isn't stepped into automatically, but only via
            // a divert from a loose end.
            else {
                _rootContainer.AddToNamedContentOnly (gatherContainer);
            }

            // Consume loose ends: divert them to this gather
            foreach (IWeavePoint looseEndWeavePoint in looseEnds) {

                var looseEnd = (Parsed.Object)looseEndWeavePoint;

                // Skip gather loose ends that are at the same level
                // since they'll be handled by the auto-enter code below
                // that only jumps into the gather if (current runtime choices == 0)
                if (looseEnd is Gather) {
                    var prevGather = (Gather)looseEnd;
                    if (prevGather.indentationDepth == gather.indentationDepth) {
                        continue;
                    }
                }

                Runtime.Divert divert = null;

                if (looseEnd is Parsed.Divert) {
                    divert = (Runtime.Divert) looseEnd.runtimeObject;
                } else {
                    divert = new Runtime.Divert ();
                    var looseWeavePoint = looseEnd as IWeavePoint;
                    looseWeavePoint.runtimeContainer.AddContent (divert);
                }
                   
                // Pass back knowledge of this loose end being diverted
                // to the FlowBase so that it can maintain a list of them,
                // and resolve the divert references later
                gatherPointsToResolve.Add (new GatherPointToResolve{ divert = divert, targetRuntimeObj = gatherContainer });
            }
            looseEnds.Clear ();

            // Replace the current container itself
            currentContainer = gatherContainer;
        }

        void AddRuntimeForWeavePoint(IWeavePoint weavePoint)
        {
            // Current level Gather
            if (weavePoint is Gather) {
                AddRuntimeForGather ((Gather)weavePoint);
            } 

            // Current level choice
            else if (weavePoint is Choice) {

                // Gathers that contain choices are no longer loose ends
                // (same as when weave points get nested content)
                if (previousWeavePoint is Gather) {
                    looseEnds.Remove (previousWeavePoint);
                }

                // Add choice point content
                var choice = (Choice)weavePoint;
                currentContainer.AddContent (choice.runtimeObject);

                // Add choice's inner content to self
                choice.innerContentContainer.name = "c-" + _choiceCount;
                currentContainer.AddToNamedContentOnly (choice.innerContentContainer);
                _choiceCount++;

                hasSeenChoiceInSection = true;
            }

            // Keep track of loose ends
            addContentToPreviousWeavePoint = false; // default
            if (WeavePointHasLooseEnd (weavePoint)) {
                looseEnds.Add (weavePoint);


                var looseChoice = weavePoint as Choice;
                if (looseChoice) {
                    addContentToPreviousWeavePoint = true;
                }
            }
            previousWeavePoint = weavePoint;
        }

        // Add nested block at a greater indentation level
        public void AddRuntimeForNestedWeave(Weave nestedResult)
        {
            // Add this inner block to current container
            // (i.e. within the main container, or within the last defined Choice/Gather)
            AddGeneralRuntimeContent (nestedResult.rootContainer);

            // Now there's a deeper indentation level, the previous weave point doesn't
            // count as a loose end (since it will have content to go to)
            if (previousWeavePoint != null) {
                looseEnds.Remove (previousWeavePoint);
                addContentToPreviousWeavePoint = false;
            }
        }

        // Normal content gets added into the latest Choice or Gather by default,
        // unless there hasn't been one yet.
        void AddGeneralRuntimeContent(Runtime.Object content)
        {
            // Content is allowed to evaluate runtimeObject to null
            // (e.g. AuthorWarning, which doesn't make it into the runtime)
            if (content == null)
                return;
            
            if (addContentToPreviousWeavePoint) {
                previousWeavePoint.runtimeContainer.AddContent (content);
            } else {
                currentContainer.AddContent (content);
            }
        }

        void PassLooseEndsToAncestors()
        {
            if (looseEnds.Count == 0) return;

            // Search for Weave ancestor to pass loose ends to for gathering.
            // There are two types depending on whether the current weave
            // is separated by a conditional or sequence.
            //  - An "inner" weave is one that is directly connected to the current
            //    weave - i.e. you don't have to pass through a conditional or
            //    sequence to get to it. We're allowed to pass all loose ends to
            //    one of these.
            //  - An "outer" weave is one that is outside of a conditional/sequence
            //    that the current weave is nested within. We're only allowed to
            //    pass gathers (i.e. 'normal flow') loose ends up there, not normal
            //    choices. The rule is that choices have to be diverted explicitly
            //    by the author since it's ambiguous where flow should go otherwise.
            //
            // e.g.:
            //
            //   - top                       <- e.g. outer weave
            //   {true:
            //       * choice                <- e.g. inner weave
            //         * * choice 2
            //             more content      <- e.g. current weave
            //       * choice 2
            //   }
            //   - more of outer weave
            //
            Weave closestInnerWeaveAncestor = null;
            Weave closestOuterWeaveAncestor = null;

            // Find inner and outer ancestor weaves as defined above.
            bool nested = false;
            for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
            {

                // Found ancestor?
                var weaveAncestor = ancestor as Weave;
                if (weaveAncestor != null)
                {
                    if (!nested && closestInnerWeaveAncestor == null)
                        closestInnerWeaveAncestor = weaveAncestor;

                    if (nested && closestOuterWeaveAncestor == null)
                        closestOuterWeaveAncestor = weaveAncestor;
                }


                // Weaves nested within Sequences or Conditionals are
                // "sealed" - any loose ends require explicit diverts.
                if (ancestor is Sequence || ancestor is Conditional)
                    nested = true;
            }

            // No weave to pass loose ends to at all?
            if (closestInnerWeaveAncestor == null && closestOuterWeaveAncestor == null)
                return;

            // Follow loose end passing logic as defined above
            for (int i = looseEnds.Count - 1; i >= 0; i--) {
                var looseEnd = looseEnds[i];

                bool received = false;

                // This weave is nested within a conditional or sequence:
                //  - choices can only be passed up to direct ancestor ("inner") weaves
                //  - gathers can be passed up to either, but favour the closer (inner) weave
                //    if there is one
                if(nested) {
                    if( looseEnd is Choice && closestInnerWeaveAncestor != null) {
                        closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
                        received = true;
                    }

                    else if( !(looseEnd is Choice) ) {
                        var receivingWeave = closestInnerWeaveAncestor ?? closestOuterWeaveAncestor;
                        if(receivingWeave != null) {
                            receivingWeave.ReceiveLooseEnd(looseEnd);
                            received = true;
                        }
                    }
                }

                // No nesting, all loose ends can be safely passed up
                else {
                    closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
                    received = true;
                }

                if(received) looseEnds.RemoveAt(i);
            }
        }

        void ReceiveLooseEnd(IWeavePoint childWeaveLooseEnd)
        {
            looseEnds.Add(childWeaveLooseEnd);
        }

        public override void ResolveReferences(Story context)
        {
            base.ResolveReferences (context);

            // Check that choices nested within conditionals and sequences are terminated
            if( looseEnds != null && looseEnds.Count > 0 ) {
                var isNestedWeave = false;
                for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
                {
                    if (ancestor is Sequence || ancestor is Conditional)
                    {
                        isNestedWeave = true;
                        break;
                    }
                }
                if (isNestedWeave)
                {
                    ValidateTermination(BadNestedTerminationHandler);
                }
            }

            foreach(var gatherPoint in gatherPointsToResolve) {
                gatherPoint.divert.targetPath = gatherPoint.targetRuntimeObj.path;
            }
                
            CheckForWeavePointNamingCollisions ();
        }

        public IWeavePoint WeavePointNamed(string name)
        {
            if (_namedWeavePoints == null)
                return null;

            IWeavePoint weavePointResult = null;
            if (_namedWeavePoints.TryGetValue (name, out weavePointResult))
                return weavePointResult;

            return null;
        }

        // Global VARs and CONSTs are treated as "outside of the flow"
        // when iterating over content that follows loose ends
        bool IsGlobalDeclaration (Parsed.Object obj)
        {

            var varAss = obj as VariableAssignment;
            if (varAss && varAss.isGlobalDeclaration && varAss.isDeclaration)
                return true;
            
            var constDecl = obj as ConstantDeclaration;
            if (constDecl)
                return true;

            return false;
        }

        // While analysing final loose ends, we look to see whether there
        // are any diverts etc which choices etc divert from
        IEnumerable<Parsed.Object> ContentThatFollowsWeavePoint (IWeavePoint weavePoint)
        {
            var obj = (Parsed.Object)weavePoint;

            // Inner content first (e.g. for a choice)
            if (obj.content != null) {
                foreach (var contentObj in obj.content) {

                    // Global VARs and CONSTs are treated as "outside of the flow"
                    if (IsGlobalDeclaration (contentObj)) continue;

                    yield return contentObj;
                }
            }


            var parentWeave = obj.parent as Weave;
            if (parentWeave == null) {
                throw new System.Exception ("Expected weave point parent to be weave?");
            }

            var weavePointIdx = parentWeave.content.IndexOf (obj);

            for (int i = weavePointIdx+1; i < parentWeave.content.Count; i++) {
                var laterObj = parentWeave.content [i];

                // Global VARs and CONSTs are treated as "outside of the flow"
                if (IsGlobalDeclaration (laterObj)) continue;

                // End of the current flow
                if (laterObj is IWeavePoint) 
                    break;

                // Other weaves will be have their own loose ends
                if (laterObj is Weave)
                    break;

                yield return laterObj;
            }
        }

        public delegate void BadTerminationHandler (Parsed.Object terminatingObj);
        public void ValidateTermination (BadTerminationHandler badTerminationHandler)
        {
            // Don't worry if the last object in the flow is a "TODO",
            // even if there are other loose ends in other places
            if (lastParsedSignificantObject is AuthorWarning) {
                return;
            }

            // By now, any sub-weaves will have passed loose ends up to the root weave (this).
            // So there are 2 possible situations:
            //  - There are loose ends from somewhere in the flow.
            //    These aren't necessarily "real" loose ends - they're weave points
            //    that don't connect to any lower weave points, so we just
            //    have to check that they terminate properly.
            //  - This weave is just a list of content with no actual weave points,
            //    so we just need to check that the list of content terminates.

            bool hasLooseEnds = looseEnds != null && looseEnds.Count > 0;

            if (hasLooseEnds) {
                foreach (var looseEnd in looseEnds) {
                    var looseEndFlow = ContentThatFollowsWeavePoint (looseEnd);
                    ValidateFlowOfObjectsTerminates (looseEndFlow, (Parsed.Object)looseEnd, badTerminationHandler);
                }
            }

            // No loose ends... is there any inner weaving at all?
            // If not, make sure the single content stream is terminated correctly
            else {

                // If there's any actual weaving, assume that content is 
                // terminated correctly since we would've had a loose end otherwise
                foreach (var obj in content) {
                    if (obj is IWeavePoint) return;
                }

                // Straight linear flow? Check it terminates
                ValidateFlowOfObjectsTerminates (content, this, badTerminationHandler);
            }
        }

        void BadNestedTerminationHandler(Parsed.Object terminatingObj)
        {
            Conditional conditional = null;
            for (var ancestor = terminatingObj.parent; ancestor != null; ancestor = ancestor.parent) {
                if( ancestor is Sequence || ancestor is Conditional ) {
                    conditional = ancestor as Conditional;
                    break;
                }
            }

            var errorMsg = "Choices nested in conditionals or sequences need to explicitly divert afterwards.";

            // Tutorialise proper choice syntax if this looks like a single choice within a condition, e.g.
            // { condition:
            //      * choice
            // }
            if (conditional != null) {
                var numChoices = conditional.FindAll<Choice>().Count;
                if( numChoices == 1 ) {
                    errorMsg = "Choices with conditions should be written: '* {condition} choice'. Otherwise, "+ errorMsg.ToLower();
                }
            }

            Error(errorMsg, terminatingObj);
        }

        void ValidateFlowOfObjectsTerminates (IEnumerable<Parsed.Object> objFlow, Parsed.Object defaultObj, BadTerminationHandler badTerminationHandler)
        {
            bool terminated = false;
            Parsed.Object terminatingObj = defaultObj;
            foreach (var flowObj in objFlow) {

                var divert = flowObj.Find<Divert> (d => !d.isThread && !d.isTunnel && !d.isFunctionCall && !(d.parent is DivertTarget));
                if (divert != null) {
                    terminated = true;
                }

                if (flowObj.Find<TunnelOnwards> () != null) {
                    terminated = true;
                    break;
                }

                terminatingObj = flowObj;
            }


            if (!terminated) {

                // Author has left a note to self here - clearly we don't need
                // to leave them with another warning since they know what they're doing.
                if (terminatingObj is AuthorWarning) {
                    return;
                }

                badTerminationHandler (terminatingObj);
            }
        }
            
        bool WeavePointHasLooseEnd(IWeavePoint weavePoint)
        {
            // No content, must be a loose end.
            if (weavePoint.content == null) return true;

            // If a weave point is diverted from, it doesn't have a loose end.
            // Detect a divert object within a weavePoint's main content
            // Work backwards since we're really interested in the end,
            // although it doesn't actually make a difference!
            // (content after a divert will simply be inaccessible)
            for (int i = weavePoint.content.Count - 1; i >= 0; --i) {
                var innerDivert = weavePoint.content [i] as Divert;
                if (innerDivert) {
                    bool willReturn = innerDivert.isThread || innerDivert.isTunnel || innerDivert.isFunctionCall;
                    if (!willReturn) return false;
                }
            }

            return true;
        }

        // Enforce rule that weave points must not have the same
        // name as any stitches or knots upwards in the hierarchy
        void CheckForWeavePointNamingCollisions()
        {
            if (_namedWeavePoints == null)
                return;
            
            var ancestorFlows = new List<FlowBase> ();
            foreach (var obj in this.ancestry) {
                var flow = obj as FlowBase;
                if (flow)
                    ancestorFlows.Add (flow);
                else
                    break;
            }


            foreach (var namedWeavePointPair in _namedWeavePoints) {
                var weavePointName = namedWeavePointPair.Key;
                var weavePoint = (Parsed.Object) namedWeavePointPair.Value;

                foreach(var flow in ancestorFlows) {

                    // Shallow search
                    var otherContentWithName = flow.ContentWithNameAtLevel (weavePointName);

                    if (otherContentWithName && otherContentWithName != weavePoint) {
                        var errorMsg = string.Format ("{0} '{1}' has the same label name as a {2} (on {3})", 
                            weavePoint.GetType().Name, 
                            weavePointName, 
                            otherContentWithName.GetType().Name, 
                            otherContentWithName.debugMetadata);

                        Error(errorMsg, (Parsed.Object) weavePoint);
                    }

                }
            }
        }

        // Keep track of previous weave point (Choice or Gather)
        // at the current indentation level:
        //  - to add ordinary content to be nested under it
        //  - to add nested content under it when it's indented
        //  - to remove it from the list of loose ends when
        //     - it has indented content since it's no longer a loose end
        //     - it's a gather and it has a choice added to it
        IWeavePoint previousWeavePoint = null;
        bool addContentToPreviousWeavePoint = false;

        // Used for determining whether the next Gather should auto-enter
        bool hasSeenChoiceInSection = false;

        int _unnamedGatherCount;

        int _choiceCount;


        Runtime.Container _rootContainer;
        Dictionary<string, IWeavePoint> _namedWeavePoints;
    }
}


namespace Ink.Parsed
{
    public class Wrap<T> : Parsed.Object where T : Runtime.Object
    {
        public Wrap (T objToWrap)
        {
            _objToWrap = objToWrap;
        }

        public override Runtime.Object GenerateRuntimeObject ()
        {
            return _objToWrap;
        }

        T _objToWrap;
    }

    // Shorthand for writing Parsed.Wrap<Runtime.Glue> and Parsed.Wrap<Runtime.Tag>
    public class Glue : Wrap<Runtime.Glue> {
        public Glue (Runtime.Glue glue) : base(glue) {}
    }
    public class LegacyTag : Wrap<Runtime.Tag> {
        public LegacyTag (Runtime.Tag tag) : base (tag) { }
    }
    
}

using Duccsoft.ImGui.Rendering;
using System;

namespace Duccsoft.ImGui.Elements;

public class Window : Element
{
	public Window( string name, ref bool open, Vector2 screenPos, Vector2 pivot, Vector2 size, ImGuiWindowFlags flags )
		: base( null )
	{
		Name = name;
		DrawList = new ImDrawList( Name );
		Id = ImGui.GetID( Name );
		WindowFlags = flags;
		Position = screenPos;
		if ( System.CustomWindowPositions.TryGetValue( Id, out var customPos ) )
		{
			// Window positions are stored unscaled in case screen size changes,
			// so we need to scale them back up here.
			Position = customPos * ImGuiStyle.UIScale;
		}
		Pivot = pivot;
		Padding = ImGui.GetStyle().WindowPadding;
		CustomSize = size;
		ImGuiSystem.Current.IdStack.Push( Id );
		ImGuiSystem.Current.WindowStack.Push( this );
		CursorPosition = ImGui.GetStyle().WindowPadding;
		CursorStartPosition = CursorPosition;

		OnBegin();

		open = true;
	}

	public string Name { get; init; }
	public ImDrawList DrawList { get; set; }
	public ImGuiWindowFlags WindowFlags { get; init; }

	public Action OnClose { get; set; }

	internal WindowTitleBar TitleBar { get; set; }

	public Vector2 CursorStartPosition { get; set; }
	public Vector2 CursorPosition { get; set; }

	public static Color32 BackgroundColor => ImGui.GetColorU32( ImGuiCol.WindowBg );
	public static Color32 BorderColor => ImGui.GetColorU32( ImGuiCol.Border );

	public override void OnEnd()
	{
		base.OnEnd();

		TitleBar?.OnEnd();
		if ( System.TryGetDrawList( Id, out var drawList ) )
		{
			DrawList = drawList;
			DrawList.CommandList.Reset();
		}
		else
		{
			DrawList = new ImDrawList( $"ImGui DrawList {Name}" );
			System.AddDrawList( Id, DrawList );
		}
	}

	protected override void OnDrawSelf( ImDrawList drawList )
	{
		DrawList.AddRect( ScreenRect.TopLeft, ScreenRect.BottomRight, BorderColor, rounding: 0, flags: ImDrawFlags.None, thickness: 1 );
		DrawList.AddRectFilled( ScreenRect.TopLeft, ScreenRect.BottomRight, BackgroundColor );
	}
}
namespace Duccsoft.ImGui;

public static partial class ImGui
{
	public static ImGuiIO GetIO() => System.InputState;
	public static Vector2 GetMousePos() => MouseState.Position;
	public static Vector2 GetMouseDragDelta( ImGuiMouseButton button, float lockThreshold = -1.0f )
	{
		if ( lockThreshold < 0f )
		{
			// TODO: Use io.MouseDraggingThreshold
			lockThreshold = 1.0f;
		}
		var mouseDelta = button switch
		{
			ImGuiMouseButton.Left	=> MouseState.LeftClickDragTotalDelta,
			ImGuiMouseButton.Right	=> MouseState.RightClickDragTotalDelta,
			ImGuiMouseButton.Middle	=> MouseState.MiddleClickDragTotalDelta,
			_						=> Vector2.Zero
		};
		if ( mouseDelta.Length < lockThreshold )
			return Vector2.Zero;

		return mouseDelta;
	}
}
using System;

namespace Duccsoft.ImGui;

public static partial class ImGui
{
	public static float GetFontSize() => (int)(18 * ImGuiStyle.UIScale);

	public static ImGuiStyle GetStyle()
	{
		return ImGuiSystem.Current.Style;
	}

	public static Color32 GetColorU32( ImGuiCol color, float alphaMul = 1.0f )
	{
		var colors = ImGuiSystem.Current.Style.Colors;

		if ( colors is null || !colors.TryGetValue( color, out Color32 styleColor ) )
			return new Color32( 0xFF, 0x00, 0xFF, (byte)(0xFF * alphaMul) );

		return styleColor with { a = (byte)(styleColor.a * alphaMul) };
	}

	#region Style Colors
	public static void StyleColorsDark( ImGuiStyle style )
	{
		if ( style is null )
			return;

		style.Colors ??= new();
		style.Colors[ImGuiCol.WindowBg]						= new( 0x0F, 0x0F, 0x0F, 240 );
		style.Colors[ImGuiCol.Border]						= new( 0x42, 0x42, 0x4C, 128 );
		style.Colors[ImGuiCol.Text]							= new( 0xFF, 0xFF, 0xFF );
		style.Colors[ImGuiCol.TitleBg]						= new( 0x0A, 0x0A, 0x0A );
		style.Colors[ImGuiCol.TitleBgActive]				= new( 0x29, 0x4A, 0x7A );
		style.Colors[ImGuiCol.Button]						= new( 66, 150, 250, 102 );
		style.Colors[ImGuiCol.ImGuiColButtonHovered]		= new( 66, 150, 250 );
		style.Colors[ImGuiCol.ButtonActive]					= new( 15, 135, 250 );
		style.Colors[ImGuiCol.FrameBg]						= new( 41, 74, 122, 138 );
		style.Colors[ImGuiCol.FrameBgHovered]				= new( 66, 150, 250, 102 );
		style.Colors[ImGuiCol.FrameBgActive]				= new( 66, 150, 250, 171 );
		style.Colors[ImGuiCol.SliderGrab]					= new( 61, 133, 244 );
		style.Colors[ImGuiCol.SliderGrabActive]				= new( 66, 150, 250, 255 );
		style.Colors[ImGuiCol.CheckMark]					= new( 66, 150, 250, 255 );
	}
	#endregion
}
using Duccsoft.ImGui.Elements;

namespace Duccsoft.ImGui;

public static partial class ImGui
{
	public static bool IsItemClicked( ImGuiMouseButton button = ImGuiMouseButton.Left )
	{
		return System.ClickedElementId == CurrentItemRecursive?.Id;
	}

	public static void Text( string formatString, params object[] args )
	{
		var text = string.Format( formatString, args );
		_ = new TextWidget( CurrentWindow, text );
	}

	public static bool Button( string label, Vector2 size = default )
	{
		var button = new ButtonWidget( CurrentWindow, label );
		return button.IsReleased;
	}

	public static bool Checkbox( string label, ref bool value )
	{
		var checkbox = new Checkbox( CurrentWindow, label, ref value );
		return checkbox.IsReleased;
	}

	public static bool DragInt( string label, ref int value, float speed = 1.0f, int min = 0, int max = 0, string format = null, ImGuiSliderFlags flags = 0 )
	{
		_ = new DragInt( CurrentWindow, label, ref value, speed, min, max, format, flags );
		// TODO: Is returning true correct?
		return true;
	}

	public static bool SliderFloat( string label, ref float value, float min, float max, string format = "F3", ImGuiSliderFlags flags = 0 )
	{
		var components = new float[1] { value };
		_ = new Slider<float>( CurrentWindow, label, ref components, min, max, format );
		value = components[0];
		return true;
	}

	public static bool SliderFloat2( string label, ref Vector2 value, float min, float max, string format = "F3", ImGuiSliderFlags flags = 0 )
	{
		var components = new float[2] { value.x, value.y };
		_ = new Slider<float>( CurrentWindow, label, ref components, min, max, format );
		value.x = components[0];
		value.y = components[1];
		return true;
	}

	public static bool SliderFloat3( string label, ref Vector3 value, float min, float max, string format = "F3", ImGuiSliderFlags flags = 0 )
	{
		var components = new float[3] { value.x, value.y, value.z };
		_ = new Slider<float>( CurrentWindow, label, ref components, min, max, format );
		value.x = components[0];
		value.y = components[1];
		value.z = components[2];
		return true;
	}

	public static bool SliderFloat4( string label, ref Vector4 value, float min, float max, string format = "F3", ImGuiSliderFlags flags = 0 )
	{
		var components = new float[4] { value.x, value.y, value.z, value.w };
		_ = new Slider<float>( CurrentWindow, label, ref components, min, max, format );
		value.x = components[0];
		value.y = components[1];
		value.z = components[2];
		value.w = components[3];
		return true;
	}

	public static bool SliderInt( string label, ref int value, int min, int max, string format = null, ImGuiSliderFlags flags = 0 )
	{
		var components = new int[1] { value };
		_ = new Slider<int>( CurrentWindow, label, ref components, min, max, format );
		value = components[0];
		return true;
	}

	public static void Image( Texture texture, Vector2 size, Vector2 uv0, Vector2 uv1, Color tintColor, Color borderColor )
	{
		_ = new ImageWidget( CurrentWindow, texture, size, uv0, uv1, tintColor, borderColor );
	}

	public static void Image( Texture texture, Vector2 size, Color tintColor, Color borderColor )
	{
		Image( texture, size, Vector2.Zero, Vector2.One, tintColor, borderColor );
	}
}
using System;
using System.Collections.Generic;

namespace Duccsoft.ImGui;

public class IdStack
{
	private struct HashData
	{
		public HashData( string id )
		{
			StringSource = id;
		}

		public HashData( int id )
		{
			IntSource = id;
		}

		public string StringSource { get; set; }
		public int IntSource { get; set; }
		
		public override int GetHashCode()
		{
			return HashCode.Combine( StringSource, IntSource );
		}
	}

	private Stack<HashData> _data = new();
	private Stack<int> _hashes = new();
	private int GetSeed()
	{
		if ( _hashes.Count == 0 )
		{
			return 0;
		}
		else
		{
			return _hashes.Peek();
		}
	}

	public void Clear()
	{
		_data.Clear();
		_hashes.Clear();
	}

	public int GetHash( string id ) => HashCode.Combine( GetSeed(), id );
	public int GetHash( int id ) => HashCode.Combine( GetSeed(), id );
	private int GetHash( HashData id ) => HashCode.Combine( GetSeed(), id.GetHashCode() );

	public void Push( string id ) => Push( new HashData( id ) );
	public void Push( int id ) => Push( new HashData( id ) );
	private void Push( HashData data )
	{
		_data.Push( data );
		var hash = GetHash( data );
		_hashes.Push( hash );
	}

	public void Pop()
	{
		_data.Pop();
		_hashes.Pop();
	}

}
using System;
using System.Collections.Generic;
using System.Linq;

namespace Duccsoft.ImGui;

internal class ReflectionCache : IHotloadManaged
{
	private Dictionary<Type, TypeDescription> _typeCache { get; set; } = new();
	private Dictionary<Type, List<PropertyDescription>> _propertyCache { get; set; } = new();
	public TypeDescription GetTypeDescription( Type type )
	{
		ArgumentNullException.ThrowIfNull( type );
		if ( !_typeCache.TryGetValue( type, out var typeDesc ) )
		{
			typeDesc = TypeLibrary.GetType( type );
			if ( typeDesc is null )
				throw new Exception( $"Type {type?.FullName} not found in {nameof( TypeLibrary )}" );
			_typeCache[type] = typeDesc;
		}
		return _typeCache[type];
	}

	public List<PropertyDescription> GetProperties( Type type )
	{
		ArgumentNullException.ThrowIfNull( type );
		if ( !_propertyCache.TryGetValue( type, out var properties ) )
		{
			var typeDesc = GetTypeDescription( type );
			properties = typeDesc.Properties
				.Where( p => p.HasAttribute<PropertyAttribute>() )
				.ToList();
			_propertyCache[type] = properties;
		}
		return _propertyCache[type];
	}

	private void Clear()
	{
		_typeCache?.Clear();
		_propertyCache?.Clear();
		_typeCache ??= new();
		_propertyCache ??= new();
	}

	public void Created( IReadOnlyDictionary<string, object> state ) => Clear();
	public void Persisted() => Clear();
}
using Sandbox.Rendering;
using System;

namespace Duccsoft.ImGui.Rendering;

public class ImDrawList
{
	public ImDrawList( string name )
	{
		CommandList = new CommandList( $"ImGui DrawList {name}" )
		{
			Flags = CommandList.Flag.Hud
		};
	}

	public CommandList CommandList { get; private set; }

	#region Rect
	public void AddRect( Vector2 upperLeft, Vector2 lowerRight, Color32 color, float rounding = 0f, ImDrawFlags flags = ImDrawFlags.None, float thickness = 1.0f )
	{
		DrawRect( upperLeft, lowerRight, Color.Transparent, color, rounding, flags, thickness );
	}
	public void AddRectFilled( Vector2 upperLeft, Vector2 lowerRight, Color32 color, float rounding = 0f, ImDrawFlags flags = ImDrawFlags.None )
		=> DrawRect( upperLeft, lowerRight, color, Color.Transparent, rounding, flags, borderThickness: 0f );

	private void DrawRect( Vector2 upperLeft, Vector2 lowerRight, Color fillColor, Color borderColor, float rounding, ImDrawFlags flags, float borderThickness )
	{
		// Transform
		CommandList.Set( "BoxPosition", upperLeft );
		CommandList.Set( "BoxSize", lowerRight - upperLeft );

		// Background
		CommandList.SetCombo( "D_BACKGROUND_IMAGE", 0 );
		if ( borderThickness >= 1f )
		{
			// Border
			CommandList.Set( "HasBorder", 1 );
			// TODO: Use ImDrawFlags to determine which borders are rounded.
			CommandList.Set( "BorderSize", borderThickness );
			CommandList.Set( "BorderRadius", rounding );
			CommandList.Set( "BorderColorL", borderColor );
			CommandList.Set( "BorderColorT", borderColor );
			CommandList.Set( "BorderColorR", borderColor );
			CommandList.Set( "BorderColorB", borderColor );
			CommandList.SetCombo( "D_BORDER_IMAGE", 0 );
		}

		CommandList.DrawQuad( new Rect( upperLeft, lowerRight - upperLeft ), Material.UI.Box, fillColor );
	}
	#endregion

	#region Triangle
	//public void AddTriangleFilled( Vector2 p1, Vector2 p2, Vector3 p3, Color32 color )
	//{
	//	throw new NotImplementedException();
	//}
	#endregion

	#region Text
	private static TextRendering.Scope TextScope( string text, Color color ) 
		=> new( text, color, ImGui.GetTextLineHeight(), "Consolas" );

	public void AddText( Vector2 pos, Color32 color, string text, TextFlag flags = TextFlag.LeftTop )
		=> DrawText( new Rect( pos, 1f ), TextScope( text, color ), flags );

	private void DrawText( Rect rect, TextRendering.Scope scope, TextFlag flags )
	{
		var textTexture = TextRendering.GetOrCreateTexture( in scope, clip: default, flags );
		if ( !textTexture.IsValid() )
			return;

		CommandList.Set( "TextureIndex", textTexture.Index );
		var size = textTexture.Size;
		rect = rect.Align( size, flags );
		CommandList.DrawQuad( rect, Material.FromShader( "shaders/ui_text.shader" ), Color.White );
	}
	#endregion

	#region Image
	public void AddImage( Texture texture, Vector2 upperLeft, Vector2 lowerRight, Vector2 uv0, Vector2 uv1, Color32 tintColor )
		=> DrawImage( texture, upperLeft, lowerRight, uv0, uv1, tintColor );

	public void AddImage( Texture texture, Vector2 upperLeft, Vector2 lowerRight )
		=> AddImage( texture, upperLeft, lowerRight, uv0: new Vector2( 0, 0 ), uv1: new Vector2( 1, 1 ), tintColor: Color.White );

	private void DrawImage( Texture texture, Vector2 upperLeft, Vector2 lowerRight, Vector2 uv0, Vector2 uv1, Color32 tintColor )
	{
		if ( !texture.IsValid() )
			return;

		// Transform
		CommandList.Set( "BoxPosition", upperLeft );
		CommandList.Set( "BoxSize", lowerRight - upperLeft );

		// Background
		CommandList.SetCombo( "D_BACKGROUND_IMAGE", 1 );
		CommandList.Set( "BgRepeat", -1 );
		CommandList.Set( "TextureIndex", texture.Index );
		var texToRectScale = 1f / (texture.Size / (lowerRight - upperLeft));
		var offset = uv0 * texture.Size * texToRectScale;
		var size = uv1 * texture.Size * texToRectScale - offset;
		var bgPos = new Vector4( offset.x, offset.y, size.x, size.y );
		CommandList.Set( "BgPos", bgPos );

		// Border
		CommandList.Set( "HasBorder", 0 );

		CommandList.DrawQuad( new Rect( upperLeft, lowerRight - upperLeft ), Material.FromShader( "shaders/imgui_rect.shader"), tintColor );
	}
	#endregion Image
}
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;

namespace Ink.Runtime
{
    public class CallStack
    {
        public class Element
        {
            public Pointer currentPointer;

            public bool inExpressionEvaluation;
            public Dictionary<string, Runtime.Object> temporaryVariables;
            public PushPopType type;

            // When this callstack element is actually a function evaluation called from the game,
            // we need to keep track of the size of the evaluation stack when it was called
            // so that we know whether there was any return value.
            public int evaluationStackHeightWhenPushed;

            // When functions are called, we trim whitespace from the start and end of what
            // they generate, so we make sure know where the function's start and end are.
            public int functionStartInOuputStream;

            public Element(PushPopType type, Pointer pointer, bool inExpressionEvaluation = false) {
                this.currentPointer = pointer;
                this.inExpressionEvaluation = inExpressionEvaluation;
                this.temporaryVariables = new Dictionary<string, Object>();
                this.type = type;
            }

            public Element Copy()
            {
                var copy = new Element (this.type, currentPointer, this.inExpressionEvaluation);
                copy.temporaryVariables = new Dictionary<string,Object>(this.temporaryVariables);
                copy.evaluationStackHeightWhenPushed = evaluationStackHeightWhenPushed;
                copy.functionStartInOuputStream = functionStartInOuputStream;
                return copy;
            }
        }

        public class Thread
        {
            public List<Element> callstack;
            public int threadIndex;
            public Pointer previousPointer;

            public Thread() {
                callstack = new List<Element>();
            }

			public Thread(Dictionary<string, object> jThreadObj, Story storyContext) : this() {
                threadIndex = (int) jThreadObj ["threadIndex"];

				List<object> jThreadCallstack = (List<object>) jThreadObj ["callstack"];
				foreach (object jElTok in jThreadCallstack) {

					var jElementObj = (Dictionary<string, object>)jElTok;

                    PushPopType pushPopType = (PushPopType)(int)jElementObj ["type"];

                    Pointer pointer = Pointer.Null;

					string currentContainerPathStr = null;
					object currentContainerPathStrToken;
					if (jElementObj.TryGetValue ("cPath", out currentContainerPathStrToken)) {
						currentContainerPathStr = currentContainerPathStrToken.ToString ();

                        var threadPointerResult = storyContext.ContentAtPath (new Path (currentContainerPathStr));
                        pointer.container = threadPointerResult.container;
                        pointer.index = (int)jElementObj ["idx"];

                        if (threadPointerResult.obj == null) {
                            throw new System.Exception ("When loading state, internal story location couldn't be found: " + currentContainerPathStr + ". Has the story changed since this save data was created?");
                        } else if (threadPointerResult.approximate) {
                            if (pointer.container != null) {
                                storyContext.Warning ("When loading state, exact internal story location couldn't be found: '" + currentContainerPathStr + "', so it was approximated to '" + pointer.container.path.ToString() + "' to recover. Has the story changed since this save data was created?");
                            } else {
                                storyContext.Warning ("When loading state, exact internal story location couldn't be found: '" + currentContainerPathStr + "' and it may not be recoverable. Has the story changed since this save data was created?");
                            }
                        }
					}

                    bool inExpressionEvaluation = (bool)jElementObj ["exp"];

					var el = new Element (pushPopType, pointer, inExpressionEvaluation);

                    object temps;
                    if ( jElementObj.TryGetValue("temp", out temps) ) {
                        el.temporaryVariables = Json.JObjectToDictionaryRuntimeObjs((Dictionary<string, object>)temps);
                    } else {
                        el.temporaryVariables.Clear();
                    }					

					callstack.Add (el);
				}

				object prevContentObjPath;
				if( jThreadObj.TryGetValue("previousContentObject", out prevContentObjPath) ) {
					var prevPath = new Path((string)prevContentObjPath);
                    previousPointer = storyContext.PointerAtPath(prevPath);
                }
			}

            public Thread Copy() {
                var copy = new Thread ();
                copy.threadIndex = threadIndex;
                foreach(var e in callstack) {
                    copy.callstack.Add(e.Copy());
                }
                copy.previousPointer = previousPointer;
                return copy;
            }

            public void WriteJson(SimpleJson.Writer writer)
            {
                writer.WriteObjectStart();

                // callstack
                writer.WritePropertyStart("callstack");
                writer.WriteArrayStart();
                foreach (CallStack.Element el in callstack)
                {
                    writer.WriteObjectStart();
                    if(!el.currentPointer.isNull) {
                        writer.WriteProperty("cPath", el.currentPointer.container.path.componentsString);
                        writer.WriteProperty("idx", el.currentPointer.index);
                    }

                    writer.WriteProperty("exp", el.inExpressionEvaluation);
                    writer.WriteProperty("type", (int)el.type);

                    if(el.temporaryVariables.Count > 0) {
                        writer.WritePropertyStart("temp");
                        Json.WriteDictionaryRuntimeObjs(writer, el.temporaryVariables);
                        writer.WritePropertyEnd();
                    }

                    writer.WriteObjectEnd();
                }
                writer.WriteArrayEnd();
                writer.WritePropertyEnd();

                // threadIndex
                writer.WriteProperty("threadIndex", threadIndex);

                if (!previousPointer.isNull)
                {
                    writer.WriteProperty("previousContentObject", previousPointer.Resolve().path.ToString());
                }

                writer.WriteObjectEnd();
            }
        }

        public List<Element> elements {
            get {
                return callStack;
            }
        }

		public int depth {
			get {
				return elements.Count;
			}
		}

        public Element currentElement { 
            get {
                var thread = _threads [_threads.Count - 1];
                var cs = thread.callstack;
                return cs [cs.Count - 1];
            } 
        }

        public int currentElementIndex {
            get {
                return callStack.Count - 1;
            }
        }

        public Thread currentThread
        {
            get {
                return _threads [_threads.Count - 1];
            }
            set {
                SboxDebug.Assert (_threads.Count == 1, "Shouldn't be directly setting the current thread when we have a stack of them");
                _threads.Clear ();
                _threads.Add (value);
            }
        }

        public bool canPop {
            get {
                return callStack.Count > 1;
            }
        }

        public CallStack (Story storyContext)
        {
            _startOfRoot = Pointer.StartOf(storyContext.rootContentContainer);
            Reset();
        }


        public CallStack(CallStack toCopy)
        {
            _threads = new List<Thread> ();
            foreach (var otherThread in toCopy._threads) {
                _threads.Add (otherThread.Copy ());
            }
            _threadCounter = toCopy._threadCounter;
            _startOfRoot = toCopy._startOfRoot;
        }

        public void Reset() 
        {
            _threads = new List<Thread>();
            _threads.Add(new Thread());

            _threads[0].callstack.Add(new Element(PushPopType.Tunnel, _startOfRoot));
        }


        // Unfortunately it's not possible to implement jsonToken since
        // the setter needs to take a Story as a context in order to
        // look up objects from paths for currentContainer within elements.
        public void SetJsonToken(Dictionary<string, object> jObject, Story storyContext)
        {
            _threads.Clear ();

            var jThreads = (List<object>) jObject ["threads"];

            foreach (object jThreadTok in jThreads) {
                var jThreadObj = (Dictionary<string, object>)jThreadTok;
                var thread = new Thread (jThreadObj, storyContext);
                _threads.Add (thread);
            }

            _threadCounter = (int)jObject ["threadCounter"];
            _startOfRoot = Pointer.StartOf(storyContext.rootContentContainer);
        }

        public void WriteJson(SimpleJson.Writer w)
        {
            w.WriteObject(writer =>
            {
                writer.WritePropertyStart("threads");
                {
                    writer.WriteArrayStart();

                    foreach (CallStack.Thread thread in _threads)
                    {
                        thread.WriteJson(writer);
                    }

                    writer.WriteArrayEnd();
                }
                writer.WritePropertyEnd();

                writer.WritePropertyStart("threadCounter");
                {
                    writer.Write(_threadCounter);
                }
                writer.WritePropertyEnd();
            });
        
        }

        public void PushThread()
        {
            var newThread = currentThread.Copy ();
            _threadCounter++;
            newThread.threadIndex = _threadCounter;
            _threads.Add (newThread);
        }

        public Thread ForkThread()
        {
            var forkedThread = currentThread.Copy();
            _threadCounter++;
            forkedThread.threadIndex = _threadCounter;
            return forkedThread;
        }

        public void PopThread()
        {
            if (canPopThread) {
                _threads.Remove (currentThread);
            } else {
				throw new System.Exception("Can't pop thread");
            }
        }

        public bool canPopThread
        {
            get {
				return _threads.Count > 1 && !elementIsEvaluateFromGame;
            }
        }

		public bool elementIsEvaluateFromGame
		{
			get {
				return currentElement.type == PushPopType.FunctionEvaluationFromGame;
			}
		}

        public void Push(PushPopType type, int externalEvaluationStackHeight = 0, int outputStreamLengthWithPushed = 0)
        {
            // When pushing to callstack, maintain the current content path, but jump out of expressions by default
            var element = new Element (
                type, 
                currentElement.currentPointer,
                inExpressionEvaluation: false
            );

            element.evaluationStackHeightWhenPushed = externalEvaluationStackHeight;
            element.functionStartInOuputStream = outputStreamLengthWithPushed;

            callStack.Add (element);
        }

        public bool CanPop(PushPopType? type = null) {

            if (!canPop)
                return false;
            
            if (type == null)
                return true;
            
            return currentElement.type == type;
        }
            
        public void Pop(PushPopType? type = null)
        {
            if (CanPop (type)) {
                callStack.RemoveAt (callStack.Count - 1);
                return;
            } else {
				throw new System.Exception("Mismatched push/pop in Callstack");
            }
        }

        // Get variable value, dereferencing a variable pointer if necessary
        public Runtime.Object GetTemporaryVariableWithName(string name, int contextIndex = -1)
        {
            // contextIndex 0 means global, so index is actually 1-based
            if (contextIndex == -1)
                contextIndex = currentElementIndex+1;
            
            Runtime.Object varValue = null;

            var contextElement = callStack [contextIndex-1];

            if (contextElement.temporaryVariables.TryGetValue (name, out varValue)) {
                return varValue;
            } else {
                return null;
            }
        }
            
        public void SetTemporaryVariable(string name, Runtime.Object value, bool declareNew, int contextIndex = -1)
        {
            if (contextIndex == -1)
                contextIndex = currentElementIndex+1;

            var contextElement = callStack [contextIndex-1];
            
            if (!declareNew && !contextElement.temporaryVariables.ContainsKey(name)) {
                throw new System.Exception ("Could not find temporary variable to set: " + name);
            }

            Runtime.Object oldValue;
            if( contextElement.temporaryVariables.TryGetValue(name, out oldValue) )
                ListValue.RetainListOriginsForAssignment (oldValue, value);

            contextElement.temporaryVariables [name] = value;
        }

        // Find the most appropriate context for this variable.
        // Are we referencing a temporary or global variable?
        // Note that the compiler will have warned us about possible conflicts,
        // so anything that happens here should be safe!
        public int ContextForVariableNamed(string name)
        {
            // Current temporary context?
            // (Shouldn't attempt to access contexts higher in the callstack.)
            if (currentElement.temporaryVariables.ContainsKey (name)) {
                return currentElementIndex+1;
            } 

            // Global
            else {
                return 0;
            }
        }
            
        public Thread ThreadWithIndex(int index)
        {
            return _threads.Find (t => t.threadIndex == index);
        }

        private List<Element> callStack
        {
            get {
                return currentThread.callstack;
            }
        }

		public string callStackTrace {
			get {
				var sb = new System.Text.StringBuilder();

				for(int t=0; t<_threads.Count; t++) {

					var thread = _threads[t];
					var isCurrent = (t == _threads.Count-1);
					sb.AppendFormat("=== THREAD {0}/{1} {2}===\n", (t+1), _threads.Count, (isCurrent ? "(current) ":""));

					for(int i=0; i<thread.callstack.Count; i++) {

						if( thread.callstack[i].type == PushPopType.Function )
							sb.Append("  [FUNCTION] ");
						else
							sb.Append("  [TUNNEL] ");

						var pointer = thread.callstack[i].currentPointer;
						if( !pointer.isNull ) {
							sb.Append("<SOMEWHERE IN ");
							sb.Append(pointer.container.path.ToString());
							sb.AppendLine(">");
						}
					}
				}


				return sb.ToString();
			}
		}

        List<Thread> _threads;
        int _threadCounter;
        Pointer _startOfRoot;
    }
}

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace Ink.Runtime
{
	/// <summary>
	/// Simple ink profiler that logs every instruction in the story and counts frequency and timing.
	/// To use:
	///  
	///   var profiler = story.StartProfiling(), 
	/// 
	///   (play your story for a bit)
	/// 
	///   var reportStr = profiler.Report();
	/// 
	///   story.EndProfiling();
	/// 
	/// </summary>
	public class Profiler
	{
        /// <summary>
        /// The root node in the hierarchical tree of recorded ink timings.
        /// </summary>
		public ProfileNode rootNode {
			get {
				return _rootNode;
			}
		}

		public Profiler() {
			_rootNode = new ProfileNode();
		}

        /// <summary>
        /// Generate a printable report based on the data recording during profiling.
        /// </summary>
		public string Report() {
			var sb = new StringBuilder();
			sb.AppendFormat("{0} CONTINUES / LINES:\n", _numContinues);
			sb.AppendFormat("TOTAL TIME: {0}\n", FormatMillisecs(_continueTotal));
			sb.AppendFormat("SNAPSHOTTING: {0}\n", FormatMillisecs(_snapTotal));
			sb.AppendFormat("OTHER: {0}\n", FormatMillisecs(_continueTotal - (_stepTotal + _snapTotal)));
			sb.Append(_rootNode.ToString());
			return sb.ToString();
		}

		public void PreContinue() {
			_continueWatch.Reset();
			_continueWatch.Start();
		}

		public void PostContinue() {
			_continueWatch.Stop();
			_continueTotal += Millisecs(_continueWatch);
			_numContinues++;
		}

		public void PreStep() {
			_currStepStack = null;
			_stepWatch.Reset();
			_stepWatch.Start();
		}

		public void Step(CallStack callstack) 
		{
			_stepWatch.Stop();

			var stack = new string[callstack.elements.Count];
			for(int i=0; i<stack.Length; i++) {
                string stackElementName = "";
                if(!callstack.elements[i].currentPointer.isNull) {
                    var objPath = callstack.elements[i].currentPointer.path;

                    for(int c=0; c<objPath.length; c++) {
                        var comp = objPath.GetComponent(c);
                        if( !comp.isIndex ) {
                            stackElementName = comp.name;
                            break;
                        }
                    }

                }
                stack[i] = stackElementName;
			}
				
			_currStepStack = stack;

			var currObj = callstack.currentElement.currentPointer.Resolve();

			string stepType = null;
			var controlCommandStep = currObj as ControlCommand;
			if( controlCommandStep )
				stepType = controlCommandStep.commandType.ToString() + " CC";
			else
				stepType = currObj.GetType().Name;

			_currStepDetails = new StepDetails {
				type = stepType,
				obj = currObj
			};

			_stepWatch.Start();
		}

		public void PostStep() {
			_stepWatch.Stop();

			var duration = Millisecs(_stepWatch);
			_stepTotal += duration;

			_rootNode.AddSample(_currStepStack, duration);

			_currStepDetails.time = duration;
			_stepDetails.Add(_currStepDetails);
		}

        /// <summary>
        /// Generate a printable report specifying the average and maximum times spent
        /// stepping over different internal ink instruction types.
        /// This report type is primarily used to profile the ink engine itself rather
        /// than your own specific ink.
        /// </summary>
		public string StepLengthReport()
		{
			var sb = new StringBuilder();

			sb.AppendLine("TOTAL: "+_rootNode.totalMillisecs+"ms");

			var averageStepTimes = _stepDetails
				.GroupBy(s => s.type)
				.Select(typeToDetails => new KeyValuePair<string, double>(typeToDetails.Key, typeToDetails.Average(d => d.time)))
				.OrderByDescending(stepTypeToAverage => stepTypeToAverage.Value)
				.Select(stepTypeToAverage => {
					var typeName = stepTypeToAverage.Key;
					var time = stepTypeToAverage.Value;
					return typeName + ": " + time + "ms";
				})
				.ToArray();

			sb.AppendLine("AVERAGE STEP TIMES: "+string.Join(", ", averageStepTimes));

			var accumStepTimes = _stepDetails
				.GroupBy(s => s.type)
				.Select(typeToDetails => new KeyValuePair<string, double>(typeToDetails.Key + " (x"+typeToDetails.Count()+")", typeToDetails.Sum(d => d.time)))
				.OrderByDescending(stepTypeToAccum => stepTypeToAccum.Value)
				.Select(stepTypeToAccum => {
					var typeName = stepTypeToAccum.Key;
					var time = stepTypeToAccum.Value;
					return typeName + ": " + time;
				})
				.ToArray();

			sb.AppendLine("ACCUMULATED STEP TIMES: "+string.Join(", ", accumStepTimes));

			return sb.ToString();
		}

        /// <summary>
        /// Create a large log of all the internal instructions that were evaluated while profiling was active.
        /// Log is in a tab-separated format, for easy loading into a spreadsheet application.
        /// </summary>
		public string Megalog()
		{
			var sb = new StringBuilder();

			sb.AppendLine("Step type\tDescription\tPath\tTime");

			foreach(var step in _stepDetails) {
				sb.Append(step.type);
				sb.Append("\t");
				sb.Append(step.obj.ToString());
				sb.Append("\t");
				sb.Append(step.obj.path);
				sb.Append("\t");
				sb.AppendLine(step.time.ToString("F8"));
			}

			return sb.ToString();
		}

		public void PreSnapshot() {
			_snapWatch.Reset();
			_snapWatch.Start();
		}

		public void PostSnapshot() {
			_snapWatch.Stop();
			_snapTotal += Millisecs(_snapWatch);
		}

		double Millisecs(Stopwatch watch)
		{
			var ticks = watch.ElapsedTicks;
			return ticks * _millisecsPerTick;
		}

		public static string FormatMillisecs(double num) {
			if( num > 5000 ) {
				return string.Format("{0:N1} secs", num / 1000.0);
			} if( num > 1000 ) {
				return string.Format("{0:N2} secs", num / 1000.0);
			} else if( num > 100 ) {
				return string.Format("{0:N0} ms", num);
			} else if( num > 1 ) {
				return string.Format("{0:N1} ms", num);
			} else if( num > 0.01 ) {
				return string.Format("{0:N3} ms", num);
			} else {
				return string.Format("{0:N} ms", num);
			}
		}

		Stopwatch _continueWatch = new Stopwatch();
		Stopwatch _stepWatch = new Stopwatch();
		Stopwatch _snapWatch = new Stopwatch();

		double _continueTotal;
		double _snapTotal;
		double _stepTotal;

		string[] _currStepStack;
		StepDetails _currStepDetails;
		ProfileNode _rootNode;
		int _numContinues;

		struct StepDetails {
			public string type;
			public Runtime.Object obj;
			public double time;
		}
		List<StepDetails> _stepDetails = new List<StepDetails>();

		static double _millisecsPerTick = 1000.0 / Stopwatch.Frequency;
	}


    /// <summary>
    /// Node used in the hierarchical tree of timings used by the Profiler.
    /// Each node corresponds to a single line viewable in a UI-based representation.
    /// </summary>
	public class ProfileNode {

        /// <summary>
        /// The key for the node corresponds to the printable name of the callstack element.
        /// </summary>		
        public readonly string key;


        #pragma warning disable 0649
        /// <summary>
        /// Horribly hacky field only used by ink unity integration,
        /// but saves constructing an entire data structure that mirrors
        /// the one in here purely to store the state of whether each
        /// node in the UI has been opened or not  /// </summary>
        public bool openInUI;
        #pragma warning restore 0649

        /// <summary>
        /// Whether this node contains any sub-nodes - i.e. does it call anything else
        /// that has been recorded?
        /// </summary>
        /// <value><c>true</c> if has children; otherwise, <c>false</c>.</value>
		public bool hasChildren {
			get {
				return _nodes != null && _nodes.Count > 0;
			}
		}

        /// <summary>
        /// Total number of milliseconds this node has been active for.
        /// </summary>
		public int totalMillisecs {
			get {
				return (int)_totalMillisecs;
			}
		}

		public ProfileNode() {

		}

		public ProfileNode(string key) {
			this.key = key;
		}

		public void AddSample(string[] stack, double duration) {
			AddSample(stack, -1, duration);
		}

		void AddSample(string[] stack, int stackIdx, double duration) {

			_totalSampleCount++;
			_totalMillisecs += duration;

			if( stackIdx == stack.Length-1 ) {
				_selfSampleCount++;
				_selfMillisecs += duration;
			}

			if( stackIdx+1 < stack.Length )
				AddSampleToNode(stack, stackIdx+1, duration);
		}

		void AddSampleToNode(string[] stack, int stackIdx, double duration)
		{
			var nodeKey = stack[stackIdx];
			if( _nodes == null ) _nodes = new Dictionary<string, ProfileNode>();

			ProfileNode node;
			if( !_nodes.TryGetValue(nodeKey, out node) ) {
				node = new ProfileNode(nodeKey);
				_nodes[nodeKey] = node;
			}

			node.AddSample(stack, stackIdx, duration);
		}

        /// <summary>
        /// Returns a sorted enumerable of the nodes in descending order of
        /// how long they took to run.
        /// </summary>
		public IEnumerable<KeyValuePair<string, ProfileNode>> descendingOrderedNodes {
			get {
				if( _nodes == null ) return null;
				return _nodes.OrderByDescending(keyNode => keyNode.Value._totalMillisecs);
			}
		}

		void PrintHierarchy(StringBuilder sb, int indent)
		{
			Pad(sb, indent);

			sb.Append(key);
			sb.Append(": ");
			sb.AppendLine(ownReport);

			if( _nodes == null ) return;

			foreach(var keyNode in descendingOrderedNodes) {
				keyNode.Value.PrintHierarchy(sb, indent+1);
			}
		}

        /// <summary>
        /// Generates a string giving timing information for this single node, including
        /// total milliseconds spent on the piece of ink, the time spent within itself
        /// (v.s. spent in children), as well as the number of samples (instruction steps)
        /// recorded for both too.
        /// </summary>
        /// <value>The own report.</value>
		public string ownReport {
			get {
				var sb = new StringBuilder();
				sb.Append("total ");
				sb.Append(Profiler.FormatMillisecs(_totalMillisecs));
				sb.Append(", self ");
				sb.Append(Profiler.FormatMillisecs(_selfMillisecs));
				sb.Append(" (");
				sb.Append(_selfSampleCount);
				sb.Append(" self samples, ");
				sb.Append(_totalSampleCount);
				sb.Append(" total)");
				return sb.ToString();
			}
			
		}

		void Pad(StringBuilder sb, int spaces)
		{
			for(int i=0; i<spaces; i++) sb.Append("   ");
		}

        /// <summary>
        /// String is a report of the sub-tree from this node, but without any of the header information
        /// that's prepended by the Profiler in its Report() method.
        /// </summary>
		public override string ToString ()
		{
			var sb = new StringBuilder();
			PrintHierarchy(sb, 0);
			return sb.ToString();
		}

		Dictionary<string, ProfileNode> _nodes;
		double _selfMillisecs;
		double _totalMillisecs;
		int _selfSampleCount;
		int _totalSampleCount;
	}
}

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

}