Search the source of every open source package.
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<FixedUpdateInputSystem>();
///
/// 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;
using Editor;
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 );
}
}
}