Search the source of every open source package.
3301 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 System.Collections.Generic;
using System.Linq;
namespace Sandbox.Events;
/// <summary>
/// Generate an ordering based on a set of first-most and last-most items, and
/// individual constraints between pairs of items. All first-most items will be
/// ordered before all last-most items, and any other items will be put in the
/// middle unless forced to be elsewhere by a constraint.
/// </summary>
internal class SortingHelper
{
public record struct SortConstraint( int EarlierIndex, int LaterIndex )
{
public SortConstraint Complement => new ( LaterIndex, EarlierIndex );
}
private readonly int _itemCount;
private readonly HashSet<SortConstraint> _initialConstraints = new HashSet<SortConstraint>();
private readonly HashSet<int> _first = new HashSet<int>();
private readonly HashSet<int> _last = new HashSet<int>();
public SortingHelper( int itemCount )
{
_itemCount = itemCount;
}
public void AddConstraint( int earlierIndex, int laterIndex )
{
_initialConstraints.Add( new SortConstraint( earlierIndex, laterIndex ) );
}
public void AddFirst( int earlierIndex )
{
_first.Add( earlierIndex );
}
public void AddLast( int laterIndex )
{
_last.Add( laterIndex );
}
public bool Sort( List<int> result, out SortConstraint invalidConstraint )
{
var middle = new HashSet<int>();
for ( var index = 0; index < _itemCount; ++index )
{
if ( !_first.Contains( index ) && !_last.Contains( index ) )
middle.Add( index );
}
var allConstraints = new HashSet<SortConstraint>();
var newConstraints = new Queue<SortConstraint>();
var beforeDict = new Dictionary<int, HashSet<int>>();
var afterDict = new Dictionary<int, HashSet<int>>();
bool AddWorkingConstraint( int earlierIndex, int laterIndex, out SortConstraint constraint )
{
constraint = new SortConstraint( earlierIndex, laterIndex );
if ( allConstraints.Contains( constraint.Complement ) )
return false;
if ( !allConstraints.Add( constraint ) )
return true;
newConstraints.Enqueue( constraint );
if ( !beforeDict.TryGetValue( earlierIndex, out var before ) )
beforeDict.Add( earlierIndex, before = new HashSet<int>() );
if ( !afterDict.TryGetValue( laterIndex, out var after ) )
afterDict.Add( laterIndex, after = new HashSet<int>() );
before.Add( laterIndex );
after.Add( earlierIndex );
return true;
}
// Add initial constraints
foreach ( var initialConstraint in _initialConstraints )
{
if ( !AddWorkingConstraint( initialConstraint.EarlierIndex, initialConstraint.LaterIndex, out invalidConstraint ) )
return false;
}
// Everything in _first should be before everything in _last
foreach ( var earlierIndex in _first )
{
foreach ( var laterIndex in _last )
{
if ( !AddWorkingConstraint( earlierIndex, laterIndex, out invalidConstraint ) )
return false;
}
}
// Keep propagating constraints until nothing changes
while ( newConstraints.TryDequeue( out var nextConstraint ) )
{
// if a < b, and b < c, then a < c etc
if ( beforeDict.TryGetValue( nextConstraint.LaterIndex, out var before ) )
{
foreach ( var laterIndex in before )
{
if ( !AddWorkingConstraint( nextConstraint.EarlierIndex, laterIndex, out invalidConstraint ) )
return false;
}
}
if ( afterDict.TryGetValue( nextConstraint.EarlierIndex, out var after ) )
{
foreach ( var earlierIndex in after )
{
if ( !AddWorkingConstraint( earlierIndex, nextConstraint.LaterIndex, out invalidConstraint ) )
{
return false;
}
}
}
}
// Now if we have any items that aren't using GroupOrder.First, and haven't
// determined that they are ordered before another item with GroupOrder.First,
// we can safely order them after all GroupOrder.First items. And vice versa.
foreach ( var middleIndex in middle )
{
var isBeforeAnyFirst = beforeDict.TryGetValue( middleIndex, out var before )
&& before.Any( x => _first.Contains( x ) );
var isAfterAnyLast = afterDict.TryGetValue( middleIndex, out var after )
&& after.Any( x => _last.Contains( x ) );
if ( !isBeforeAnyFirst )
{
foreach ( var earlierIndex in _first )
AddWorkingConstraint( earlierIndex, middleIndex, out invalidConstraint );
}
if ( !isAfterAnyLast )
{
foreach ( var laterIndex in _last )
AddWorkingConstraint( middleIndex, laterIndex, out invalidConstraint );
}
}
// Now lets add items to the final ordering if all items that should be sorted
// before them are already added to that ordering. We'll implement this by choosing
// items that have an empty list / don't appear in afterDict, and update that
// dictionary as we go.
var earliestRemaining = new Queue<int>();
// First, seed the queue with everything that's already not ordered after anything
for ( var index = 0; index < _itemCount; ++index )
{
if ( !afterDict.ContainsKey( index ) )
{
earliestRemaining.Enqueue( index );
}
}
result.Clear();
while ( earliestRemaining.TryDequeue( out var nextIndex ) )
{
result.Add( nextIndex );
foreach ( var laterIndex in beforeDict.TryGetValue( nextIndex, out var laterIndices )
? laterIndices : Enumerable.Empty<int>() )
{
var beforeLater = afterDict[laterIndex];
beforeLater.Remove( nextIndex );
if ( beforeLater.Count == 0 )
earliestRemaining.Enqueue( laterIndex );
}
}
invalidConstraint = default;
return result.Count == _itemCount;
}
}
using Sandbox;
using System.Collections.Generic;
namespace EZCameraShake
{
public class CameraShaker : Component
{
/// <summary>
/// The single instance of the CameraShaker in the current scene. Do not use if you have multiple instances.
/// </summary>
public static CameraShaker Instance;
static Dictionary<string, CameraShaker> instanceList = new Dictionary<string, CameraShaker>();
/// <summary>
/// The default position influcence of all shakes created by this shaker.
/// </summary>
[Property] public Vector3 DefaultPosInfluence = new Vector3(0.15f, 0.15f, 0.15f);
/// <summary>
/// The default rotation influcence of all shakes created by this shaker.
/// </summary>
[Property] public Vector3 DefaultRotInfluence = new Vector3(1, 1, 1);
/// <summary>
/// Offset that will be applied to the camera's default (0,0,0) rest position
/// </summary>
[Property] public Vector3 RestPositionOffset = new Vector3(0, 0, 0);
/// <summary>
/// Offset that will be applied to the camera's default (0,0,0) rest rotation
/// </summary>
[Property] public Vector3 RestRotationOffset = new Vector3(0, 0, 0);
Vector3 posAddShake, rotAddShake;
List<CameraShakeInstance> cameraShakeInstances = new List<CameraShakeInstance>();
protected override void OnAwake()
{
Instance = this;
instanceList.Add(GameObject.Name, this);
}
protected override void OnUpdate()
{
posAddShake = Vector3.Zero;
rotAddShake = Vector3.Zero;
for (int i = 0; i < cameraShakeInstances.Count; i++)
{
if (i >= cameraShakeInstances.Count)
break;
CameraShakeInstance c = cameraShakeInstances[i];
if (c.CurrentState == CameraShakeState.Inactive && c.DeleteOnInactive)
{
cameraShakeInstances.RemoveAt(i);
i--;
}
else if (c.CurrentState != CameraShakeState.Inactive)
{
posAddShake += CameraUtilities.MultiplyVectors(c.UpdateShake(), c.PositionInfluence);
rotAddShake += CameraUtilities.MultiplyVectors(c.UpdateShake(), c.RotationInfluence);
}
}
Transform.LocalPosition = (posAddShake) + RestPositionOffset;
Vector3 thing = (rotAddShake / 100) + RestRotationOffset;
Transform.LocalRotation = new Angles(thing.x, thing.y, thing.z);
}
/// <summary>
/// Gets the CameraShaker with the given name, if it exists.
/// </summary>
/// <param name="name">The name of the camera shaker instance.</param>
/// <returns></returns>
public static CameraShaker GetInstance(string name)
{
CameraShaker c;
if (instanceList.TryGetValue(name, out c))
return c;
Log.Error("CameraShake " + name + " not found!");
return null;
}
/// <summary>
/// Starts a shake using the given preset.
/// </summary>
/// <param name="shake">The preset to use.</param>
/// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
public CameraShakeInstance Shake(CameraShakeInstance shake)
{
cameraShakeInstances.Add(shake);
return shake;
}
/// <summary>
/// Shake the camera once, fading in and out over a specified durations.
/// </summary>
/// <param name="magnitude">The intensity of the shake.</param>
/// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
/// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
/// <param name="fadeOutTime">How long to fade out the shake, in seconds.</param>
/// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
public CameraShakeInstance ShakeOnce(float magnitude, float roughness, float fadeInTime, float fadeOutTime)
{
CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness, fadeInTime, fadeOutTime);
shake.PositionInfluence = DefaultPosInfluence;
shake.RotationInfluence = DefaultRotInfluence;
cameraShakeInstances.Add(shake);
return shake;
}
/// <summary>
/// Shake the camera once, fading in and out over a specified durations.
/// </summary>
/// <param name="magnitude">The intensity of the shake.</param>
/// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
/// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
/// <param name="fadeOutTime">How long to fade out the shake, in seconds.</param>
/// <param name="posInfluence">How much this shake influences position.</param>
/// <param name="rotInfluence">How much this shake influences rotation.</param>
/// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
public CameraShakeInstance ShakeOnce(float magnitude, float roughness, float fadeInTime, float fadeOutTime, Vector3 posInfluence, Vector3 rotInfluence)
{
CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness, fadeInTime, fadeOutTime);
shake.PositionInfluence = posInfluence;
shake.RotationInfluence = rotInfluence;
cameraShakeInstances.Add(shake);
return shake;
}
/// <summary>
/// Start shaking the camera.
/// </summary>
/// <param name="magnitude">The intensity of the shake.</param>
/// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
/// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
/// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
public CameraShakeInstance StartShake(float magnitude, float roughness, float fadeInTime)
{
CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness);
shake.PositionInfluence = DefaultPosInfluence;
shake.RotationInfluence = DefaultRotInfluence;
shake.StartFadeIn(fadeInTime);
cameraShakeInstances.Add(shake);
return shake;
}
/// <summary>
/// Start shaking the camera.
/// </summary>
/// <param name="magnitude">The intensity of the shake.</param>
/// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
/// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
/// <param name="posInfluence">How much this shake influences position.</param>
/// <param name="rotInfluence">How much this shake influences rotation.</param>
/// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
public CameraShakeInstance StartShake(float magnitude, float roughness, float fadeInTime, Vector3 posInfluence, Vector3 rotInfluence)
{
CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness);
shake.PositionInfluence = posInfluence;
shake.RotationInfluence = rotInfluence;
shake.StartFadeIn(fadeInTime);
cameraShakeInstances.Add(shake);
return shake;
}
/// <summary>
/// Gets a copy of the list of current camera shake instances.
/// </summary>
public List<CameraShakeInstance> ShakeInstances
{ get { return new List<CameraShakeInstance>(cameraShakeInstances); } }
protected override void OnDestroy()
{
instanceList.Remove(GameObject.Name);
}
}
}
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 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; }
}
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 Editor;
public static class MyEditorMenu
{
[Menu( "Editor", "CrosshairBuilder/My Menu Option" )]
public static void OpenMyMenu()
{
EditorUtility.DisplayDialog( "It worked!", "This is being called from your library's editor code!" );
}
}
using Braxnet;
using Sandbox;
[TestClass]
public partial class LibraryTests
{
[TestMethod]
public void SceneTest()
{
var scene = new Scene();
using ( scene.Push() )
{
// var go = new GameObject();
Assert.AreEqual( 1, scene.Directory.GameObjectCount );
Assert.IsTrue( scene.Directory.FindByName( "LibraryTestComponent" ) != null );
}
}
}
[Autoload]
public class LibraryTestComponent : Component
{
}
public sealed class JiggleBone : TransformProxyComponent
{
JiggleBoneState state = new JiggleBoneState();
[Property]
public Vector3 StartPoint = new Vector3( 0, 0, 0 );
[Property]
public Vector3 EndPoint = new Vector3( 32, 0, 0 );
[Property, Range( 0, 2 )]
public float Speed { get; set; } = 1.0f;
[Property, Range( 0, 2 )]
public float Stiffness { get; set; } = 1.0f;
[Property, Range( 0, 2 )]
public float Damping { get; set; } = 1.0f;
[Property, Range( 0, 100 )]
public float Radius { get; set; } = 40.0f;
[Property, Range( 0, 100 )]
public float Mass { get; set; } = 1.0f;
Transform LocalJigglePosition;
protected override void OnEnabled()
{
LocalJigglePosition = Transform.Local;
base.OnEnabled();
state = new JiggleBoneState();
}
protected override void OnUpdate()
{
var oldPos = LocalJigglePosition;
using ( Transform.DisableProxy() )
{
var worldTx = Transform.World;
var startPoint = worldTx.PointToWorld( StartPoint );
var endPoint = worldTx.PointToWorld( EndPoint );
//Gizmo.Draw.LineSphere( startPoint, 1 );
//Gizmo.Draw.LineSphere( endPoint, 1 );
state.Extent = (endPoint - startPoint);
state.Stiffness = Stiffness;
state.Damping = Damping;
state.Radius = Radius;
state.Mass = Mass;
state.Update( startPoint, Time.Delta * Speed * 16.0f );
var tx = worldTx.RotateAround( startPoint, state.Rotation );
LocalJigglePosition = GameObject.Parent.Transform.World.ToLocal( tx );
}
if ( oldPos != LocalJigglePosition )
{
MarkTransformChanged();
}
}
protected override void DrawGizmos()
{
base.DrawGizmos();
if ( !Gizmo.IsSelected )
return;
using ( Transform.DisableProxy() )
{
Gizmo.Transform = Transform.World;
Gizmo.Draw.IgnoreDepth = false;
Gizmo.Draw.Color = Gizmo.Colors.Yaw.WithAlpha( 0.5f );
Gizmo.Draw.Line( StartPoint, EndPoint );
Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( StartPoint, 5 ) );
Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( EndPoint, 5 ) );
Gizmo.Draw.LineSphere( EndPoint, Radius * 2.0f, 4 );
}
}
public override Transform GetLocalTransform()
{
return LocalJigglePosition;
}
}
class JiggleBoneState
{
public Vector3 Extent = new Vector3( 32, 0, 0 );
public Vector3 Position { get; set; }
public Rotation Rotation { get; set; }
public float Stiffness { get; set; } = 1.0f;
public float Damping { get; set; } = 1.0f;
public float Radius { get; set; } = 10.0f;
public float Gravity { get; set; } = 1.0f;
public float Mass { get; set; } = 1.0f;
Vector3 basePosition;
Vector3 velocity;
public JiggleBoneState()
{
}
internal void Update( Vector3 position, float timeDelta )
{
basePosition = position + Extent;
// initialization
if ( Position == default )
{
Position = basePosition;
}
// Calculate spring force based on displacement from the cube
Vector3 displacement = Position - basePosition;
Vector3 springForce = -Stiffness * displacement;
// Calculate acceleration (Newton's second law)
Vector3 acceleration = springForce / Mass;
// Update velocity (integrate acceleration)
velocity += acceleration * timeDelta;
// Apply exponential damping
velocity *= (float)Math.Exp( -Damping * timeDelta );
// Update position (integrate velocity)
Position += velocity * timeDelta;
{
var diff = Position - basePosition;
var diffLen = diff.Length;
if ( diffLen > Radius )
{
Position = basePosition + diff.Normal * Radius;
//velocity = velocity.AddClamped( -diff * 2.0f, diff.Length );
}
}
// Store the rotation offset result
Rotation = Rotation.FromToRotation( basePosition - position, Position - position );
//Gizmo.Draw.IgnoreDepth = true;
//Gizmo.Draw.Line( position, Position );
//Gizmo.Draw.Line( basePosition, Position );
}
}
using Sandbox;
/// <summary>
/// This is a component - in your library!
/// </summary>
[Title( "LibraryImporter - My Component" )]
public class MyLibraryComponent : Component
{
}
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
using Editor;
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;
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();
}
}
}OptionsScreen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
justify-content: flex-start;
align-items: flex-start;
flex-direction: column;
font-weight: bold;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transform: scale( 1 );
transition: none;
pointer-events: all;
background-color: #111111;
.title {
color: white;
text-shadow: none;
box-shadow: none;
align-self: flex-end;
font-size: 75px;
text-shadow: 0px 0px 2px black;
flex-shrink: 0;
font-family: Consolas;
margin: 20px 0px;
}
.button {
//flex-direction: row;
justify-content: flex-start;
align-items: center;
flex-shrink: 0;
flex-grow: 0;
color: rgba(255,255,255, 0.9);
width: auto;
padding: 15px 32px;
text-align: center;
align-self: center;
font-size: 24px;
font-weight: 600;
margin: 20px;
gap: 0;
transition: all 50ms linear;
cursor: pointer;
}
> .container {
position: relative;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 100%;
.title {
align-self: center;
font-size: 60px;
flex-shrink: 0;
}
> .buttons {
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
flex-shrink: 0;
// gap: 8px;
}
.content {
padding: 5px;
flex-grow: 1;
flex-direction: column;
align-self: center;
//border-radius: 20px;
&.bg {
//background-color: rgba(220,220,220,0.5);
}
}
}
// Buttons on the left side
.container .buttons {
.button-bg .button {
width: 100%;
min-width: 400px;
max-width: 600px;
}
}
.content {
height: auto;
// Buttons inside the content box
.buttons {
.button {
padding: 5px;
font-size: 30px;
}
}
&.tabs-container {
flex-grow: 0;
flex-shrink: 0;
overflow: hidden;
}
.tabs-group {
flex-direction: row;
align-items: center;
flex-grow: 0;
.button {
margin: 0 15px;
background-color: #242424;
// Can't remove this sound once it's set
// Use .inactive instead
&:hover {
//sound-in: ui.button.over;
}
&:hover {
background-color: #3B3B3B; //#EC594F;
}
&:active {
background-color: #6E6E6E33; //#D32F2F;
}
&.active {
background-color: #F44336;
}
&.inactive:hover {
sound-in: ui.button.over;
}
}
}
}
.content.tab-content {
// CONTENT ADD HERE
.button {
margin: 0;
}
}
.content.tab-content .button, .menu.buttons .button {
&:hover {
sound-in: ui.button.over;
}
&.green {
background-color: #4CAF50;
&:hover {
background-color: #57BB5A;
}
&:active {
background-color: #45A049;
}
}
&.red {
background-color: #F44336;
&:hover {
background-color: #EC594F;
}
&:active {
background-color: #D32F2F;
}
}
&.gray, &.grey {
background-color: #666;
color: white;
&:hover {
background-color: #777;
}
&:active {
background-color: #444;
}
}
}
.table {
position: relative;
flex-shrink: 1;
flex-grow: 0;
justify-content: center;
flex-wrap: wrap;
color: #9b9fa8;
align-self: center;
max-width: 1500px;
width: 85%;
font-size: 30px;
// TABLE ADD HERE
overflow-y: scroll;
.row {
flex-direction: row;
width: 100%;
align-items: center;
justify-content: center;
flex-grow: 0;
flex-shrink: 0;
font-weight: 600;
margin: 10px 0;
}
.row.title {
color: white;
margin-top: 20px;
padding: 5px 0;
justify-content: flex-start;
font-size: 32px;
font-weight: 800;
border-bottom: 0.2em solid rgb(30, 30, 30, 0.25);
&:first-child {
margin-top: 0;
}
}
.column {
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.column:last-child {
align-items: flex-end;
justify-content: center;
//height: 100%;
}
.column.value {
.button {
width: 100px;
justify-content: center;
}
}
SliderControl {
width: 100%;
}
}
.cycling-selector {
display: flex;
flex-direction: column;
align-items: center;
width: 100%; /* Adjust width as needed */
max-width: 500px; /* Set max width */
background-color: #111; /* Dark background similar to your image */
padding: 10px;
border-radius: 5px;
color: white;
font-family: Arial, sans-serif;
}
.cycling-label {
text-align: center;
margin-bottom: 10px; /* Spacing between label and controls */
font-size: 14px;
width: 100%;
}
.cycling-controls {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.button {
width: auto;
padding: 5px;
}
}
.cycling-controls .value {
align-items: center;
font-weight: bold;
font-size: 22px;
padding: 5px;
}
.cycling-controls .arrow {
border: none;
color: white;
font-size: 40px;
line-height: 10px;
cursor: pointer;
padding: 5px;
&.left {
padding-left: 0;
}
&.right {
padding-right: 0;
}
}
.cycling-controls .arrow:hover {
color: #ccc;
}
}
global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
[AssemblyInitialize]
public static void ClassInitialize( TestContext context )
{
Sandbox.Application.InitUnitTest();
}
}
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Duccsoft;
/// <summary>
/// Provides a handy asynchronous wrapper for loading a VideoPlayer and waiting
/// until its video and audio are both loaded.
/// </summary>
public class AsyncVideoLoader
{
public AsyncVideoLoader()
{
_videoPlayer = new VideoPlayer();
}
public AsyncVideoLoader( VideoPlayer player )
{
_videoPlayer = player ?? new VideoPlayer();
}
public bool IsLoading { get; private set; }
private VideoPlayer _videoPlayer;
private Action _onLoaded;
private Action _onAudioReady;
public async Task<VideoPlayer> LoadFromUrl( string url, CancellationToken cancelToken = default )
{
void Play( VideoPlayer player ) => player.Play( url );
await Load( Play, cancelToken );
return _videoPlayer;
}
public async Task<VideoPlayer> LoadFromFile( BaseFileSystem fileSystem, string path, CancellationToken cancelToken )
{
void Play( VideoPlayer player ) => player.Play( fileSystem, path );
await Load( Play, cancelToken );
return _videoPlayer;
}
private async Task Load( Action<VideoPlayer> playAction, CancellationToken cancelToken = default )
{
// Attempting to play a video from a thread would throw an exception.
await GameTask.MainThread( cancelToken );
if ( IsLoading )
{
throw new InvalidOperationException( "Another video was already being loaded. Check IsLoading or create a new instance of AsyncVideoLoader." );
}
IsLoading = true;
bool videoLoaded = false;
bool audioLoaded = false;
// Assign private members instead of named methods to the invocation lists of the
// VideoPlayer delegates to break reference equality between runs.
_onLoaded = () => videoLoaded = true;
_onAudioReady = () => audioLoaded = true;
_videoPlayer.OnLoaded = _onLoaded;
_videoPlayer.OnAudioReady = _onAudioReady;
playAction?.Invoke( _videoPlayer );
// Non-blocking spin until video and audio are loaded.
while ( !videoLoaded || !audioLoaded )
{
// If OnLoaded or OnAudioReady are changed externally before we're finished
// loading, the video will likely never load. Abort to avoid spinning forever.
var callbacksChanged = _onLoaded != _videoPlayer.OnLoaded || _onAudioReady != _videoPlayer.OnAudioReady;
if ( callbacksChanged || cancelToken.IsCancellationRequested )
{
IsLoading = false;
return;
}
await GameTask.Yield();
}
IsLoading = false;
}
}
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using Editor;
using Sandbox;
using System.Text;
class SpectogramWidget : Widget
{
private short[] samples;
private int sampleRate;
private List<int> splitPoints = new List<int>();
private int? dragPoint = null;
private Label loadingLabel;
private Label dropLabel;
private bool isLoading = true;
public SoundFile CurrentSound { get; private set; }
public SpectogramWidget(SoundFile soundFile) : base(null)
{
MinimumSize = 100;
MouseTracking = true;
AcceptDrops = true;
loadingLabel = new Label(this);
loadingLabel.Text = "Loading audio data...";
loadingLabel.Visible = false;
dropLabel = new Label(this);
dropLabel.Text = "Drop a sound file here";
dropLabel.SetStyles("font-size: 18px; color: #aaa; text-align: center;");
if (soundFile != null)
{
LoadSound(soundFile);
}
}
public async void LoadSound(SoundFile soundFile)
{
CurrentSound = soundFile;
isLoading = true;
samples = null;
splitPoints.Clear();
loadingLabel.Visible = true;
dropLabel.Visible = false;
await LoadAudioDataAsync(soundFile);
}
private async Task LoadAudioDataAsync(SoundFile soundFile)
{
try
{
await soundFile.LoadAsync();
samples = await soundFile.GetSamplesAsync();
if (samples == null)
{
loadingLabel.Text = "Failed to load audio data";
return;
}
sampleRate = soundFile.Rate;
splitPoints.Add(0);
splitPoints.Add(samples.Length - 1);
loadingLabel.Visible = false;
isLoading = false;
Update();
}
catch (Exception ex)
{
loadingLabel.Text = $"Error loading audio: {ex.Message}";
}
}
protected override void DoLayout()
{
base.DoLayout();
if (loadingLabel != null)
{
loadingLabel.Position = new Vector2(10, Height / 2 - 10);
loadingLabel.Size = new Vector2(Width - 20, 20);
}
if (dropLabel != null)
{
dropLabel.Position = new Vector2(10, Height / 2 - 10);
dropLabel.Size = new Vector2(Width - 20, 20);
}
}
public override void OnDragDrop(DragEvent e)
{
base.OnDragDrop(e);
if (!e.Data.HasFileOrFolder) return;
var asset = AssetSystem.FindByPath(e.Data.FileOrFolder);
if (asset?.AssetType != AssetType.SoundFile) return;
var soundFile = SoundFile.Load(asset.Path);
if (soundFile != null)
{
LoadSound(soundFile);
}
}
public override void OnDragHover(DragEvent e)
{
base.OnDragHover(e);
if (!e.Data.HasFileOrFolder) return;
var asset = AssetSystem.FindByPath(e.Data.FileOrFolder);
if (asset?.AssetType != AssetType.SoundFile) return;
e.Action = DropAction.Link;
}
protected override void OnMouseClick(MouseEvent e)
{
if (isLoading) return;
base.OnMouseClick(e);
if (e.Button == MouseButtons.Left)
{
var samplePos = (int)(e.LocalPosition.x / Width * samples.Length);
var nearPoint = splitPoints.FirstOrDefault(p => Math.Abs(p - samplePos) < (samples.Length / Width * 5));
if (nearPoint != default)
{
dragPoint = splitPoints.IndexOf(nearPoint);
}
else
{
splitPoints.Add(samplePos);
splitPoints.Sort();
Update();
}
}
}
protected override void OnMouseMove(MouseEvent e)
{
if (isLoading) return;
base.OnMouseMove(e);
if (dragPoint.HasValue)
{
var samplePos = (int)(e.LocalPosition.x / Width * samples.Length);
splitPoints[dragPoint.Value] = samplePos;
splitPoints.Sort();
Update();
}
}
protected override void OnMouseReleased(MouseEvent e)
{
if (isLoading) return;
base.OnMouseReleased(e);
dragPoint = null;
}
protected override void OnPaint()
{
base.OnPaint();
if (isLoading || samples == null)
{
return;
}
Paint.ClearPen();
Paint.SetBrush(Theme.Grey.WithAlpha(0.1f));
Paint.DrawRect(LocalRect);
Paint.SetPen(Theme.Blue);
var samplesPerPixel = samples.Length / Width;
for (int x = 0; x < Width; x++)
{
var startSample = (int)(x * samplesPerPixel);
var endSample = Math.Min(startSample + samplesPerPixel, samples.Length);
var max = short.MinValue;
var min = short.MaxValue;
for (int i = startSample; i < endSample; i++)
{
max = Math.Max(max, samples[i]);
min = Math.Min(min, samples[i]);
}
var y1 = Height / 2 + (min / (float)short.MaxValue * Height / 2);
var y2 = Height / 2 + (max / (float)short.MaxValue * Height / 2);
Paint.DrawLine(new Vector2(x, y1), new Vector2(x, y2));
}
Paint.SetPen(Theme.Red);
foreach (var point in splitPoints)
{
var x = point / (float)samples.Length * Width;
Paint.DrawLine(new Vector2(x, 0), new Vector2(x, Height));
}
}
public List<int> GetSplitPoints()
{
return new List<int>(splitPoints);
}
public void SplitCurrentSound(Action<SoundFile> onSoundCreated)
{
if (CurrentSound == null || samples == null) return;
var splitPoints = GetSplitPoints();
if (splitPoints.Count < 2) return;
try
{
var baseFileName = Path.GetFileNameWithoutExtension(CurrentSound.ResourcePath);
var outputDir = Path.Combine(
Project.Current.GetAssetsPath(),
"generated",
$"{baseFileName}_splits"
);
Directory.CreateDirectory(outputDir);
for (int i = 0; i < splitPoints.Count - 1; i++)
{
var start = splitPoints[i];
var end = splitPoints[i + 1];
var length = end - start;
var segmentSamples = new short[length];
Array.Copy(samples, start, segmentSamples, 0, length);
var wavPath = Path.Combine(outputDir, $"{baseFileName}_part_{i + 1}.wav");
using (var writer = new BinaryWriter(File.Create(wavPath)))
{
writer.Write(Encoding.ASCII.GetBytes("RIFF"));
writer.Write(36 + (segmentSamples.Length * 2));
writer.Write(Encoding.ASCII.GetBytes("WAVE"));
writer.Write(Encoding.ASCII.GetBytes("fmt "));
writer.Write(16);
writer.Write((short)1);
writer.Write((short)CurrentSound.Channels);
writer.Write(CurrentSound.Rate);
writer.Write(CurrentSound.Rate * CurrentSound.Channels * 2);
writer.Write((short)(CurrentSound.Channels * 2));
writer.Write((short)16);
writer.Write(Encoding.ASCII.GetBytes("data"));
writer.Write(segmentSamples.Length * 2);
foreach (var sample in segmentSamples)
{
writer.Write(sample);
}
}
var asset = AssetSystem.RegisterFile(wavPath);
if (asset != null)
{
var soundFile = SoundFile.Load(asset.RelativePath);
if (soundFile != null)
{
onSoundCreated?.Invoke(soundFile);
}
}
}
}
catch (Exception ex)
{
Log.Error($"Error splitting sound: {ex.Message}");
}
}
}namespace LobbySystem;
/// <summary>
/// Auto-hosts a lobby so Steam friends can join, and keeps one networked pawn per connection plus optional
/// bots by cloning <see cref="PlayerPrefab"/>. The pawn only has to implement <see cref="ILobbyAgent"/>.
/// Spawning is de-duped and runs in OnUpdate so a join can't fire mid-enumeration.
/// </summary>
public sealed class LobbyNetworkManager : Component, Component.INetworkListener
{
[Property] public GameObject PlayerPrefab { get; set; }
[Property] public int BotCount { get; set; } = 1;
/// <summary>When true, bots only exist during an active round.</summary>
[Property] public bool BotsOnlyDuringRound { get; set; } = true;
[Property] public Color BotTint { get; set; } = new Color( 1f, 0.35f, 0.3f );
// Lobby spawn ring, used before a round map loads.
readonly Vector3[] _spawns =
{
new Vector3( 0f, -300f, 40f ), new Vector3( 300f, 0f, 40f ),
new Vector3( 0f, 300f, 40f ), new Vector3( -300f, 0f, 40f ),
new Vector3( 250f, 250f, 40f ), new Vector3( -250f, -250f, 40f ),
};
int _spawnIndex;
readonly Dictionary<Guid, GameObject> _pawns = new();
readonly List<GameObject> _bots = new();
bool _reconcileNow;
TimeUntil _nextReconcile;
TimeUntil _nextSweep;
protected override async Task OnLoad()
{
// When joining a friend the engine is mid-connect and IsActive is briefly false, so poll for a
// moment before hosting. Otherwise a joiner would spin up its own solo lobby.
if ( Networking.IsActive ) return;
for ( int i = 0; i < 6 && !Networking.IsActive; i++ )
await Task.DelayRealtimeSeconds( 0.1f );
if ( !Networking.IsActive )
Networking.CreateLobby( new() );
}
void INetworkListener.OnActive( Connection channel ) => _reconcileNow = true;
protected override void OnUpdate()
{
if ( !Networking.IsHost || PlayerPrefab is null ) return;
if ( !_reconcileNow && _nextReconcile > 0f ) return;
_reconcileNow = false;
_nextReconcile = 0.25f;
try
{
bool wantBots = !BotsOnlyDuringRound || (LobbyDirector.Current?.State == LobbyState.Active);
ReconcileBots( wantBots ? Math.Max( 0, BotCount ) : 0 );
foreach ( var conn in Connection.All.ToList() )
{
if ( conn is null || !conn.IsActive ) continue;
if ( _pawns.TryGetValue( conn.Id, out var pawn ) && pawn.IsValid() ) continue;
var id = conn.Id;
_pawns[id] = FindConnectionPawn( id ) ?? SpawnPawn( false, conn.DisplayName, conn );
}
Sweep();
}
catch
{
// Connection or scene list changed during the pass; retry next frame.
}
}
void ReconcileBots( int target )
{
_bots.RemoveAll( b => !b.IsValid() );
while ( _bots.Count > target )
{
var b = _bots[_bots.Count - 1];
_bots.RemoveAt( _bots.Count - 1 );
if ( b.IsValid() ) b.Destroy();
}
while ( _bots.Count < target )
_bots.Add( SpawnPawn( true, "Bot", null ) );
}
GameObject FindConnectionPawn( Guid id )
{
foreach ( var a in Scene.GetAllComponents<ILobbyAgent>() )
{
if ( !a.IsValid() || a.IsBot ) continue;
if ( a is Component c && c.Network.OwnerId == id ) return c.GameObject;
}
return null;
}
void Sweep()
{
if ( _nextSweep > 0f ) return;
_nextSweep = 1f;
foreach ( var key in _pawns.Where( kv => !kv.Value.IsValid() ).Select( kv => kv.Key ).ToList() )
_pawns.Remove( key );
}
GameObject SpawnPawn( bool isBot, string displayName, Connection owner )
{
var go = PlayerPrefab.Clone( NextSpawn() );
go.Name = isBot ? "Bot" : $"Player - {displayName}";
go.Enabled = true;
var agent = go.Components.Get<ILobbyAgent>() ?? go.Components.GetInChildren<ILobbyAgent>();
agent?.InitAgent( isBot, displayName );
if ( isBot )
{
var rend = go.Components.GetInChildren<SkinnedModelRenderer>();
if ( rend is not null ) rend.Tint = BotTint;
}
if ( owner is not null ) go.NetworkSpawn( owner );
else go.NetworkSpawn();
return go;
}
Vector3 NextSpawn()
{
int idx = _spawnIndex++;
var dir = LobbyDirector.Current;
if ( dir is not null && dir.UseRoundMap && dir.MapReady )
return dir.RoundSpawnPoint( idx );
return _spawns[idx % _spawns.Length];
}
}
namespace LobbySystem;
/// <summary>Lifecycle of the lobby: Lobby, then Active, then Ended before looping back.</summary>
public enum LobbyState
{
/// <summary>Free roam before and after a round. The mode button works here.</summary>
Lobby,
/// <summary>A round is running.</summary>
Active,
/// <summary>Round finished; results show before returning to the lobby.</summary>
Ended
}
namespace LobbySystem;
/// <summary>
/// In-world button that opens the mode menu for the host, or a local suggestion menu for a client. It needs
/// a ModelRenderer to be visible and is hidden while a round is live. When the local player is within
/// <see cref="UseRange"/> and presses Use, the menu opens.
/// </summary>
public sealed class LobbyModeButton : Component
{
[Property] public float UseRange { get; set; } = 130f;
[Property] public bool GlowWhenInRange { get; set; } = true;
[Property] public Color IdleTint { get; set; } = new Color( 0.85f, 0.4f, 0.15f );
[Property] public Color ActiveTint { get; set; } = new Color( 1f, 0.85f, 0.3f );
ModelRenderer _renderer;
ILobbyAgent _me;
protected override void OnStart()
{
_renderer = Components.Get<ModelRenderer>() ?? Components.GetInChildren<ModelRenderer>();
if ( _renderer is not null ) _renderer.Tint = IdleTint;
}
protected override void OnUpdate()
{
var dir = LobbyDirector.Current;
bool inLobby = dir is null || !dir.RoundLive;
if ( _renderer is not null && _renderer.Enabled != inLobby )
_renderer.Enabled = inLobby;
if ( !inLobby ) return;
var me = LocalPlayer();
bool inRange = me is not null && WorldPosition.Distance( me.WorldPosition ) <= UseRange;
if ( GlowWhenInRange && _renderer is not null )
_renderer.Tint = inRange ? ActiveTint : IdleTint;
if ( inRange && Input.Pressed( "Use" ) )
dir?.RequestModeMenu();
}
ILobbyAgent LocalPlayer()
{
if ( _me is not null && _me.IsValid() && !_me.IsBot && !_me.IsProxy ) return _me;
try { _me = Scene.GetAllComponents<ILobbyAgent>().FirstOrDefault( c => c.IsValid() && !c.IsBot && !c.IsProxy ); }
catch { _me = null; }
return _me;
}
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "MC Clouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "trend" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "trend.mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-16T17:04:05.4666731Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.124.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.124.0")]using Sandbox;
using Sandbox.UI;
using System;
namespace SbTween;
public static class LightExtensions
{
public static BaseTween TweenLightColor( this Light Light, Color target, float duration )
{
Color start = Light.LightColor;
var tween = new BaseTween( duration );
tween.Target = Light.GameObject;
return TweenManager.Instance.AddTween( tween
.OnStart( () => start = Light.LightColor )
.OnUpdate( p => Light.LightColor = Color.Lerp( start, target, p ) ) );
}
public static BaseTween TweenRadius( this SpotLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.Radius;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.Radius )
.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenConeOuter( this SpotLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.ConeOuter;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.ConeOuter )
.OnUpdate( p => light.ConeOuter = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenInnerCone( this SpotLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.ConeInner;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.ConeInner )
.OnUpdate( p => light.ConeInner = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenAttenuation( this PointLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.Attenuation;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.Attenuation )
.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenRadius( this PointLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.Radius;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.Radius )
.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenAttenuation( this SpotLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.Attenuation;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.Attenuation )
.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
}
//FLICKERING LIGHT
public static BaseTween TweenFlickerLight( this PointLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
{
if ( !light.IsValid() ) return null;
float time = 0f;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnUpdate( _ =>
{
time += Time.Delta * speed;
float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
float t = (noise + 1f) * 0.5f;
light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
} ) );
}
public static BaseTween TweenFlickerLight( this SpotLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
{
if ( !light.IsValid() ) return null;
float time = 0f;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnUpdate( _ =>
{
time += Time.Delta * speed;
float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
float t = (noise + 1f) * 0.5f;
light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
} ) );
}
//FLICKERING Color
public static BaseTween TweenFlickerColor( this PointLight light, Color colorA, Color colorB, float duration, float speed = 10f )
{
if ( !light.IsValid() ) return null;
float time = 0f;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnUpdate( _ =>
{
time += Time.Delta * speed;
float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
float t = (noise + 1f) * 0.5f;
light.LightColor = Color.Lerp( colorA, colorB, t );
} ) );
}
public static BaseTween TweenFlickerColor( this SpotLight light, Color colorA, Color colorB, float duration, float speed = 10f )
{
if ( !light.IsValid() ) return null;
float time = 0f;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnUpdate( _ =>
{
time += Time.Delta * speed;
float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
float t = (noise + 1f) * 0.5f;
light.LightColor = Color.Lerp( colorA, colorB, t );
} ) );
}
}
using Sandbox;
using System;
namespace SbTween;
public static class AudioExtensions
{
public static BaseTween TweenVolume( this SoundPointComponent sound, float targetVolume, float duration )
{
float startVolume = sound.Volume;
var tween = new BaseTween( duration );
tween.Target = sound.GameObject;
return TweenManager.Instance.AddTween( tween
.OnStart( () => startVolume = sound.Volume )
.OnUpdate( p =>
{
if ( !sound.IsValid() ) return;
sound.Volume = MathX.Lerp( startVolume, targetVolume, p );
} )
);
}
public static BaseTween TweenPitch( this SoundPointComponent sound, float targetPitch, float duration )
{
float startPitch = sound.Pitch;
var tween = new BaseTween( duration );
tween.Target = sound.GameObject;
return TweenManager.Instance.AddTween( tween
.OnStart( () => startPitch = sound.Pitch )
.OnUpdate( p =>
{
if ( !sound.IsValid() ) return;
sound.Pitch = MathX.Lerp( startPitch, targetPitch, p );
} )
);
}
}
using Sandbox;
using System;
namespace SbTween;
public static class MathTweenExtensions
{
public static BaseTween TweenInCircle( this GameObject obj, float duration, Vector3 axis, float range, float speed, bool snapping = false )
{
Vector3 centerPos = obj.WorldPosition;
var tween = new BaseTween( duration );
tween.Target = obj;
Vector3 normal = axis.Normal;
Vector3 v1 = Vector3.Cross( normal, MathF.Abs( normal.z ) < 0.9f ? Vector3.Up : Vector3.Forward ).Normal;
Vector3 v2 = Vector3.Cross( normal, v1 ).Normal;
return TweenManager.Instance.AddTween( tween
.OnUpdate( p =>
{
if ( !obj.IsValid() ) return;
float angleDegrees = p * 360f * speed;
float angleRadians = angleDegrees * (MathF.PI / 180f);
float cos = MathF.Cos( angleRadians ) * range;
float sin = MathF.Sin( angleRadians ) * range;
Vector3 rotatedOffset = (v1 * cos) + (v2 * sin);
obj.WorldPosition = centerPos + rotatedOffset;
} )
);
}
public static BaseTween TweenSpiral( this GameObject obj, float duration, Vector3 axis, float speed, float frequency )
{
Vector3 startPos = obj.WorldPosition;
var tween = new BaseTween( duration );
tween.Target = obj;
return TweenManager.Instance.AddTween( tween
.OnStart( () => startPos = obj.WorldPosition )
.OnUpdate( p =>
{
if ( !obj.IsValid() ) return;
float angle = p * MathF.PI * 2f * frequency;
float currentRadius = p * speed;
float x = MathF.Cos( angle ) * currentRadius;
float y = MathF.Sin( angle ) * currentRadius;
Vector3 axisOffset = axis * p;
Vector3 circleOffset = new Vector3( x, y, 0 );
obj.WorldPosition = startPos + axisOffset + circleOffset;
} )
);
}
public static BaseTween TweenPunchFloat( this GameObject obj, float v, float amplitude, float duration, int vibrations = 5, float elasticity = 1f, Action<float> setter = null )
{
var tween = new BaseTween( duration );
tween.Target = obj;
return TweenManager.Instance.AddTween( tween
.OnUpdate( p =>
{
if ( !obj.IsValid() ) return;
if ( p >= 1.0f )
{
setter?.Invoke( v );
return;
}
float decay = MathF.Pow( 1f - p, elasticity * 3f );
float omega = vibrations * MathF.PI * 2f;
float oscillation = MathF.Sin( p * omega );
float currentOffset = amplitude * oscillation * decay;
setter?.Invoke( v + currentOffset );
} )
.OnComplete( () => setter?.Invoke( v ) )
);
}
public static BaseTween TweenShakeFloat( this GameObject obj, float baseline, float strength, float duration, Action<float> setter = null )
{
var tween = new BaseTween( duration );
tween.Target = obj;
return TweenManager.Instance.AddTween( tween
.OnUpdate( p =>
{
if ( !obj.IsValid() ) return;
if ( p >= 1.0f )
{
setter?.Invoke( baseline );
return;
}
float currentStrength = strength * (1.0f - p);
float randomOffset = Game.Random.Float( -currentStrength, currentStrength );
setter?.Invoke( baseline + randomOffset );
} )
.OnComplete( () => setter?.Invoke( baseline ) )
);
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Editor;
using Sandbox;
namespace HammerTextureBrowser;
[Dock( "Editor", "OG Texture Browser", "texture" )]
public sealed class HammerTextureBrowserDock : Widget
{
private static readonly Regex QuotedMaterialProperty = new( "\"(?<key>[^\"]+)\"\\s+\"(?<value>[^\"]+)\"", RegexOptions.Compiled | RegexOptions.CultureInvariant );
private static readonly string[] PrimaryTextureKeys =
[
"TextureColor",
"TextureBaseColor",
"TextureAlbedo",
"AlbedoTexture",
"BaseTexture",
"BaseColorTexture",
"g_tColor",
"g_tBaseColor",
"g_tAlbedo"
];
private readonly HammerTextureList TextureList;
private readonly ComboBox SizeCombo;
private readonly LineEdit FilterEdit;
private readonly ComboBox KeywordsCombo;
private readonly Checkbox OnlyUsedTextures;
private readonly Label SelectedTextureLabel;
private readonly Label TextureSizeLabel;
private readonly Label CountLabel;
private readonly Button MarkButton;
private readonly Button ReplaceButton;
private readonly Button ReloadButton;
private readonly Button OpenSourceButton;
private List<Asset> AllMaterials = new();
private Asset SelectedMaterial;
private string KeywordFilter = string.Empty;
private int PreviewSize = 128;
public HammerTextureBrowserDock( Widget parent ) : base( parent )
{
MinimumSize = new( 420, 320 );
WindowTitle = "OG Texture Browser";
SetWindowIcon( "texture" );
Layout = Layout.Column();
Layout.Margin = 0;
Layout.Spacing = 0;
TextureList = Layout.Add( new HammerTextureList( this ), 1 );
TextureList.OnTextureSelected = SelectMaterial;
TextureList.OnTextureActivated = SelectMaterial;
TextureList.OnOpenInEditor = OpenMaterialSource;
TextureList.OnOpenInAssetBrowser = OpenInAssetBrowser;
var bottom = Layout.Add( new Widget( this ) );
bottom.Layout = Layout.Column();
bottom.Layout.Margin = 4;
bottom.Layout.Spacing = 3;
bottom.FixedHeight = Theme.RowHeight * 2 + 12;
bottom.OnPaintOverride = () =>
{
Paint.ClearPen();
Paint.SetBrush( Theme.ControlBackground );
Paint.DrawRect( bottom.LocalRect );
return false;
};
var topRow = bottom.Layout.AddRow();
topRow.Spacing = 5;
var sizeBlock = topRow.Add( new Widget( this ) );
sizeBlock.FixedWidth = 138;
sizeBlock.MinimumWidth = 138;
sizeBlock.MaximumWidth = 138;
sizeBlock.Layout = Layout.Row();
sizeBlock.Layout.Margin = 0;
sizeBlock.Layout.Spacing = 5;
AddSmallLabel( sizeBlock.Layout, "Size:" );
SizeCombo = sizeBlock.Layout.Add( new ComboBox( sizeBlock ) );
SizeCombo.FixedWidth = 88;
SizeCombo.MinimumWidth = 88;
SizeCombo.MaximumWidth = 88;
SizeCombo.AddItem( "32x32", null, () => SetPreviewSize( 32 ) );
SizeCombo.AddItem( "64x64", null, () => SetPreviewSize( 64 ) );
SizeCombo.AddItem( "128x128", null, () => SetPreviewSize( 128 ) );
SizeCombo.AddItem( "256x256", null, () => SetPreviewSize( 256 ) );
SizeCombo.AddItem( "1:1", null, () => SetPreviewSize( 128 ) );
SizeCombo.CurrentIndex = 2;
StyleBottomInput( SizeCombo, 24 );
AddSmallLabel( topRow, "Filter:", 58 );
FilterEdit = topRow.Add( new LineEdit( this ) );
FilterEdit.FixedWidth = 176;
FilterEdit.MinimumWidth = 176;
FilterEdit.MaximumWidth = 176;
FilterEdit.PlaceholderText = "texture name";
FilterEdit.TextChanged += _ => RefreshList();
StyleBottomInput( FilterEdit );
SelectedTextureLabel = topRow.Add( new Label( this ) );
SelectedTextureLabel.MinimumWidth = 180;
SelectedTextureLabel.Alignment = TextFlag.LeftCenter;
SelectedTextureLabel.Text = "";
OpenSourceButton = topRow.Add( new Button( "Open Source" ) );
OpenSourceButton.FixedWidth = 96;
OpenSourceButton.Clicked = OpenSelectedSource;
var bottomRow = bottom.Layout.AddRow();
bottomRow.Spacing = 5;
OnlyUsedTextures = bottomRow.Add( new Checkbox( "Only used textures" ) );
OnlyUsedTextures.FixedWidth = 138;
OnlyUsedTextures.StateChanged += _ => RefreshList();
AddSmallLabel( bottomRow, "Keywords:", 58 );
KeywordsCombo = bottomRow.Add( new ComboBox( this ) );
KeywordsCombo.FixedWidth = 176;
KeywordsCombo.MinimumWidth = 176;
KeywordsCombo.MaximumWidth = 176;
KeywordsCombo.AddItem( "All Keywords", null, () => SetKeywordFilter( string.Empty ) );
StyleBottomInput( KeywordsCombo, 24 );
MarkButton = bottomRow.Add( new Button( "Mark" ) );
MarkButton.FixedWidth = 76;
MarkButton.Clicked = MarkSelectedMaterial;
ReplaceButton = bottomRow.Add( new Button( "Replace" ) );
ReplaceButton.FixedWidth = 76;
ReplaceButton.Clicked = ReplaceSelectedMaterial;
ReloadButton = bottomRow.Add( new Button( "Reload" ) );
ReloadButton.FixedWidth = 76;
ReloadButton.Clicked = Reload;
TextureSizeLabel = bottomRow.Add( new Label( this ) );
TextureSizeLabel.MinimumWidth = 0;
TextureSizeLabel.MaximumWidth = 68;
TextureSizeLabel.Alignment = TextFlag.RightCenter;
bottomRow.AddStretchCell( 1 );
CountLabel = bottomRow.Add( new Label( this ) );
CountLabel.MinimumWidth = 0;
CountLabel.MaximumWidth = 96;
CountLabel.Alignment = TextFlag.RightCenter;
ReloadMaterials();
SetPreviewSize( PreviewSize );
RefreshList();
UpdateSelectedMaterialUi();
}
private void Reload()
{
ReloadMaterials();
RefreshList();
UpdateSelectedMaterialUi();
}
private static void AddSmallLabel( Layout row, string text, int fixedWidth = 0 )
{
var label = row.Add( new Label( text ) );
label.Alignment = fixedWidth > 0 ? TextFlag.RightCenter : TextFlag.LeftCenter;
label.FixedWidth = fixedWidth > 0 ? fixedWidth : text.Length * 6 + 4;
}
private static void StyleBottomInput( Widget widget, int fixedHeight = 22 )
{
widget.FixedHeight = fixedHeight;
widget.SetStyles(
"background-color: #2d3136; " +
"border: 1px solid #555c64; " +
"border-radius: 2px; " +
"color: #f2f2f2; " +
"padding-top: 0px; " +
"padding-bottom: 0px; " +
"padding-left: 5px; " +
"padding-right: 5px;" );
}
private void ReloadMaterials()
{
var menuPath = EditorUtility.Projects.GetAll()
.FirstOrDefault( x => x.Config.Ident == "menu" )
?.GetAssetsPath()
.NormalizeFilename( false );
AllMaterials = AssetSystem.All
.Where( IsBrowsableMaterial )
.Where( x => !IsMenuAsset( x, menuPath ) )
.OrderBy( TextureDisplayName, StringComparer.OrdinalIgnoreCase )
.ToList();
RebuildKeywords();
}
private static bool IsBrowsableMaterial( Asset asset )
{
if ( asset is null )
return false;
if ( asset.AssetType != AssetType.Material )
return false;
if ( asset.AbsolutePath?.Contains( ".sbox/cloud/", StringComparison.OrdinalIgnoreCase ) ?? false )
return false;
return true;
}
private static bool IsMenuAsset( Asset asset, string menuPath )
{
if ( string.IsNullOrEmpty( menuPath ) )
return false;
return asset.AbsolutePath?.NormalizeFilename( false ).StartsWith( menuPath, StringComparison.OrdinalIgnoreCase ) ?? false;
}
private void RebuildKeywords()
{
var tags = AllMaterials
.SelectMany( x => x.Tags )
.Where( x => !string.IsNullOrWhiteSpace( x ) )
.Distinct( StringComparer.OrdinalIgnoreCase )
.OrderBy( x => x, StringComparer.OrdinalIgnoreCase )
.ToList();
KeywordsCombo.Clear();
KeywordsCombo.AddItem( "All Keywords", null, () => SetKeywordFilter( string.Empty ) );
foreach ( var tag in tags )
{
var tagValue = tag;
KeywordsCombo.AddItem( tagValue, null, () => SetKeywordFilter( tagValue ) );
}
KeywordsCombo.CurrentIndex = 0;
}
private void SetKeywordFilter( string tag )
{
KeywordFilter = tag ?? string.Empty;
RefreshList();
}
private void SetPreviewSize( int size )
{
PreviewSize = size;
TextureList.SetPreviewSize( PreviewSize );
RefreshList();
}
private void RefreshList()
{
if ( TextureList is null )
return;
IEnumerable<Asset> materials = AllMaterials;
var query = FilterEdit?.Text ?? string.Empty;
if ( !string.IsNullOrWhiteSpace( query ) )
{
var parts = query.Split( ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries );
materials = materials.Where( x => MatchesQuery( x, parts ) );
}
if ( !string.IsNullOrWhiteSpace( KeywordFilter ) )
{
materials = materials.Where( x => x.Tags.Any( tag => tag.Equals( KeywordFilter, StringComparison.OrdinalIgnoreCase ) ) );
}
if ( OnlyUsedTextures?.Value ?? false )
{
var used = HammerMaterialSelection.GetUsedMaterialKeys();
materials = materials.Where( x => used.Contains( x.RelativePath ) || used.Contains( x.AbsolutePath ) || used.Contains( TextureDisplayName( x ) ) );
}
var entries = materials
.OrderBy( TextureDisplayName, StringComparer.OrdinalIgnoreCase )
.Select( x => new HammerTextureEntry( x ) )
.ToList();
TextureList.SetItems( entries );
if ( CountLabel is not null )
{
CountLabel.Text = $"{entries.Count:n0} textures";
CountLabel.Update();
}
if ( SelectedMaterial is not null && entries.All( x => x.Asset != SelectedMaterial ) )
TextureList.UnselectAll();
}
private static bool MatchesQuery( Asset asset, IEnumerable<string> parts )
{
var name = TextureDisplayName( asset );
var type = asset.AssetType?.FriendlyName ?? string.Empty;
IEnumerable<string> tags = asset.Tags;
foreach ( var part in parts )
{
var search = part;
var negated = false;
if ( search.StartsWith( "-" ) )
{
search = search[1..];
negated = true;
}
var matched =
name.Contains( search, StringComparison.OrdinalIgnoreCase ) ||
type.Contains( search, StringComparison.OrdinalIgnoreCase ) ||
tags.Any( x => x.Contains( search, StringComparison.OrdinalIgnoreCase ) );
if ( !negated && !matched )
return false;
if ( negated && matched )
return false;
}
return true;
}
private void SelectMaterial( Asset material )
{
if ( material?.AssetType != AssetType.Material )
return;
SelectedMaterial = material;
EditorUtility.InspectorObject = material;
HammerMaterialSelection.SetCurrentMaterial( material );
UpdateSelectedMaterialUi();
}
private void UpdateSelectedMaterialUi()
{
var hasMaterial = SelectedMaterial is not null;
SelectedTextureLabel.Text = hasMaterial ? TextureDisplayName( SelectedMaterial ) : "";
SelectedTextureLabel.ToolTip = SelectedMaterial?.RelativePath ?? string.Empty;
TextureSizeLabel.Text = GetTextureSizeText( SelectedMaterial );
TextureSizeLabel.ToolTip = SelectedMaterial?.AbsolutePath ?? string.Empty;
MarkButton.Enabled = hasMaterial;
ReplaceButton.Enabled = hasMaterial;
OpenSourceButton.Enabled = hasMaterial;
SelectedTextureLabel.Update();
TextureSizeLabel.Update();
MarkButton.Update();
ReplaceButton.Update();
OpenSourceButton.Update();
}
private static string GetTextureSizeText( Asset asset )
{
if ( asset is null )
return "";
var texturePath = GetPrimaryMaterialTexturePath( asset );
if ( string.IsNullOrWhiteSpace( texturePath ) )
return "";
try
{
var texture = Texture.Load( texturePath );
if ( texture is null || !texture.IsValid() || texture.Width <= 0 || texture.Height <= 0 )
return "";
return $"{texture.Width}x{texture.Height}";
}
catch
{
return "";
}
}
private static string GetPrimaryMaterialTexturePath( Asset asset )
{
if ( string.IsNullOrWhiteSpace( asset?.AbsolutePath ) || !File.Exists( asset.AbsolutePath ) )
return null;
try
{
var properties = QuotedMaterialProperty.Matches( File.ReadAllText( asset.AbsolutePath ) )
.Select( x => (Key: x.Groups["key"].Value, Value: NormalizeTexturePath( x.Groups["value"].Value )) )
.Where( x => IsTexturePath( x.Value ) )
.ToList();
foreach ( var key in PrimaryTextureKeys )
{
var match = properties.FirstOrDefault( x => x.Key.Equals( key, StringComparison.OrdinalIgnoreCase ) );
if ( !string.IsNullOrWhiteSpace( match.Value ) )
return match.Value;
}
var colorMatch = properties.FirstOrDefault( x =>
(x.Key.Contains( "color", StringComparison.OrdinalIgnoreCase ) ||
x.Key.Contains( "albedo", StringComparison.OrdinalIgnoreCase ) ||
x.Key.Contains( "base", StringComparison.OrdinalIgnoreCase )) &&
!x.Key.Contains( "tint", StringComparison.OrdinalIgnoreCase ) );
if ( !string.IsNullOrWhiteSpace( colorMatch.Value ) )
return colorMatch.Value;
return properties.FirstOrDefault().Value;
}
catch
{
return null;
}
}
private static string NormalizeTexturePath( string path )
{
return path?.Trim().Replace( '\\', '/' );
}
private static bool IsTexturePath( string path )
{
var extension = Path.GetExtension( path );
return extension.Equals( ".vtex", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".tga", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".png", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".jpg", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".jpeg", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".tif", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".tiff", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".psd", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".exr", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".hdr", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".bmp", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".webp", StringComparison.OrdinalIgnoreCase );
}
private void MarkSelectedMaterial()
{
if ( SelectedMaterial is null )
return;
HammerMaterialSelection.SelectFacesUsingMaterial( SelectedMaterial );
}
private void ReplaceSelectedMaterial()
{
if ( SelectedMaterial is null )
return;
HammerMaterialSelection.AssignAssetToSelection( SelectedMaterial );
}
private void OpenSelectedSource()
{
OpenMaterialSource( SelectedMaterial );
}
private void OpenMaterialSource( Asset asset )
{
if ( asset is null )
return;
if ( asset.CanOpenInEditor )
{
asset.OpenInEditor();
return;
}
EditorUtility.OpenFileFolder( asset.AbsolutePath );
}
private void OpenInAssetBrowser( Asset asset )
{
if ( asset is null )
return;
LocalAssetBrowser.OpenTo( asset, true );
}
internal static string TextureDisplayName( Asset asset )
{
var path = asset?.RelativePath ?? asset?.Name ?? "";
path = path.NormalizeFilename( false, false );
if ( path.StartsWith( "materials/", StringComparison.OrdinalIgnoreCase ) )
path = path["materials/".Length..];
var extension = Path.GetExtension( path );
if ( !string.IsNullOrEmpty( extension ) )
path = path[..^extension.Length];
return path;
}
}
internal sealed class HammerTextureList : ListView
{
private int PreviewSize = 128;
public Action<Asset> OnTextureSelected;
public Action<Asset> OnTextureActivated;
public Action<Asset> OnOpenInEditor;
public Action<Asset> OnOpenInAssetBrowser;
public HammerTextureList( Widget parent ) : base( parent )
{
MultiSelect = false;
FocusMode = FocusMode.TabOrClick;
Margin = 2;
ItemSpacing = 2;
ItemPaint = PaintTextureItem;
ItemSelected = item => SelectTexture( item, false );
ItemActivated = item => SelectTexture( item, true );
ItemDrag = StartTextureDrag;
ItemContextMenu = OpenTextureContextMenu;
ItemScrollEnter = item => (item as HammerTextureEntry)?.OnScrollEnter();
ItemScrollExit = item => (item as HammerTextureEntry)?.OnScrollExit();
OnPaintOverride = PaintBackground;
}
public void SetPreviewSize( int previewSize )
{
PreviewSize = previewSize;
ItemSize = new Vector2( PreviewSize + 4, PreviewSize + 18 );
Update();
}
public void SetItems( IEnumerable<HammerTextureEntry> entries )
{
base.SetItems( entries.Cast<object>() );
Update();
}
private bool PaintBackground()
{
Paint.ClearPen();
Paint.SetBrush( Color.Black );
Paint.DrawRect( LocalRect );
return false;
}
private void SelectTexture( object item, bool activated )
{
if ( item is not HammerTextureEntry entry )
return;
if ( activated )
OnTextureActivated?.Invoke( entry.Asset );
else
OnTextureSelected?.Invoke( entry.Asset );
}
private bool StartTextureDrag( object item )
{
if ( item is not HammerTextureEntry entry || entry.Asset is null )
return false;
SelectItem( entry );
SelectTexture( entry, false );
var drag = new Drag( this );
drag.Data.Object = entry.Asset;
drag.Data.Text = entry.Asset.RelativePath;
drag.Data.Url = new Uri( "file:///" + entry.Asset.AbsolutePath );
foreach ( var selected in SelectedItems.OfType<HammerTextureEntry>() )
{
if ( selected == entry || selected.Asset is null )
continue;
drag.Data.Text += "\n" + selected.Asset.RelativePath;
}
drag.Execute();
return true;
}
private void OpenTextureContextMenu( object item )
{
if ( item is not HammerTextureEntry entry )
return;
SelectItem( entry );
SelectTexture( entry, false );
var menu = new ContextMenu( this );
menu.AddOption( "Open in Editor", "edit", () => OnOpenInEditor?.Invoke( entry.Asset ) )
.Enabled = entry.Asset is not null;
menu.AddOption( "Open in Asset Browser", "search", () => OnOpenInAssetBrowser?.Invoke( entry.Asset ) )
.Enabled = entry.Asset is not null;
menu.OpenAtCursor( false );
}
private void PaintTextureItem( VirtualWidget item )
{
if ( item.Object is not HammerTextureEntry entry )
return;
var rect = item.Rect.Shrink( 1 );
var active = Paint.HasPressed;
var selected = Paint.HasSelected || Paint.HasPressed;
var hover = !selected && Paint.HasMouseOver;
Paint.ClearPen();
Paint.SetBrush( Color.Black );
Paint.DrawRect( rect );
if ( selected )
{
Paint.SetPen( Theme.Primary, 2 );
Paint.ClearBrush();
Paint.DrawRect( rect.Grow( -1 ) );
}
else if ( hover )
{
Paint.SetPen( Theme.ControlBackground.Lighten( 0.35f ), 1 );
Paint.ClearBrush();
Paint.DrawRect( rect.Grow( -1 ) );
}
var previewRect = rect;
previewRect.Width = PreviewSize;
previewRect.Height = PreviewSize;
previewRect.Left += MathF.Max( 0, (rect.Width - PreviewSize) * 0.5f );
entry.DrawPreview( previewRect, active );
var nameRect = previewRect;
nameRect.Top = previewRect.Bottom;
nameRect.Height = 12;
Paint.ClearPen();
Paint.SetBrush( selected ? Theme.Primary.Lighten( active ? -0.1f : 0.05f ) : new Color( 0.0f, 0.05f, 0.85f ) );
Paint.DrawRect( nameRect );
Paint.SetDefaultFont( 6 );
Paint.SetPen( Color.White );
var label = Paint.GetElidedText( entry.DisplayName, nameRect.Width - 4, ElideMode.Middle );
Paint.DrawText( nameRect.Shrink( 2, 0 ), label, TextFlag.LeftCenter | TextFlag.SingleLine );
}
}
internal sealed class HammerTextureEntry
{
public Asset Asset { get; }
public string DisplayName { get; }
public HammerTextureEntry( Asset asset )
{
Asset = asset;
DisplayName = HammerTextureBrowserDock.TextureDisplayName( asset );
}
public void OnScrollEnter()
{
if ( Asset is null || Asset.HasCachedThumbnail )
return;
EditorEvent.Register( this );
Asset.GetAssetThumb( true );
}
public void OnScrollExit()
{
if ( Asset is null )
return;
Asset.CancelThumbBuild();
EditorEvent.Unregister( this );
}
public void DrawPreview( Rect rect, bool active )
{
Paint.ClearPen();
Paint.SetBrush( active ? Color.Black.Lighten( 0.08f ) : Color.Black );
Paint.DrawRect( rect );
var thumb = Asset?.GetAssetThumb( true ) ?? AssetType.Material?.Icon128;
if ( thumb is null )
return;
var drawRect = rect;
var scale = MathF.Min( rect.Width / thumb.Width, rect.Height / thumb.Height );
if ( scale > 0 && float.IsFinite( scale ) )
{
drawRect.Width = MathF.Min( rect.Width, thumb.Width * scale );
drawRect.Height = MathF.Min( rect.Height, thumb.Height * scale );
drawRect.Left = rect.Left + (rect.Width - drawRect.Width) * 0.5f;
drawRect.Top = rect.Top + (rect.Height - drawRect.Height) * 0.5f;
}
Paint.BilinearFiltering = true;
Paint.Draw( drawRect, thumb );
Paint.BilinearFiltering = false;
}
}
internal static class HammerMaterialSelection
{
private const BindingFlags StaticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private static Type HammerType;
private static bool HasResolvedHammerType;
public static void SetCurrentMaterial( Asset asset ) => InvokeHammerAssetMethod( "SetCurrentMaterial", asset );
public static void SelectFacesUsingMaterial( Asset asset ) => InvokeHammerAssetMethod( "SelectFacesUsingMaterial", asset );
public static void AssignAssetToSelection( Asset asset ) => InvokeHammerAssetMethod( "AssignAssetToSelection", asset );
public static HashSet<string> GetUsedMaterialKeys()
{
var keys = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
try
{
var activeMap = GetHammerType()?.GetProperty( "ActiveMap", StaticFlags )?.GetValue( null );
if ( activeMap is null )
return keys;
var world = activeMap.GetType().GetProperty( "World", InstanceFlags )?.GetValue( activeMap );
if ( world is null )
return keys;
CollectUsedMaterialKeys( world, keys );
}
catch
{
// Hammer throws when no map is open; in that case "Only used textures" should simply show none.
return keys;
}
return keys;
}
private static void CollectUsedMaterialKeys( object node, HashSet<string> keys )
{
if ( node is null )
return;
var materialMethod = node.GetType().GetMethod( "GetFaceMaterialAssets", InstanceFlags );
if ( materialMethod is not null && materialMethod.Invoke( node, null ) is IEnumerable materials )
{
foreach ( var item in materials )
{
if ( item is not Asset asset )
continue;
keys.Add( asset.RelativePath );
keys.Add( asset.AbsolutePath );
keys.Add( HammerTextureBrowserDock.TextureDisplayName( asset ) );
}
}
var children = node.GetType().GetProperty( "Children", InstanceFlags )?.GetValue( node ) as IEnumerable;
if ( children is null )
return;
foreach ( var child in children )
{
CollectUsedMaterialKeys( child, keys );
}
}
private static void InvokeHammerAssetMethod( string methodName, Asset asset )
{
if ( asset is null )
return;
var method = GetHammerType()?.GetMethod( methodName, StaticFlags, binder: null, types: new[] { typeof( Asset ) }, modifiers: null );
if ( method is null )
return;
try
{
method.Invoke( null, new object[] { asset } );
}
catch
{
// This dock still works as a browser if Hammer is not the active editor surface.
}
}
private static Type GetHammerType()
{
if ( HasResolvedHammerType )
return HammerType;
HasResolvedHammerType = true;
foreach ( var assembly in AppDomain.CurrentDomain.GetAssemblies() )
{
HammerType = assembly.GetType( "Editor.MapEditor.Hammer" );
if ( HammerType is not null )
break;
}
return HammerType;
}
}
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 );
}
}
}
namespace Sandbox.UiPro;
public enum HorizontalAlignment
{
Left,
Center,
Right
}
public enum VerticalAlignment
{
Top,
Center,
Bottom
}
[Title( "Text Node - UI Pro" ), Category( "UI Pro" ), Icon( "text_fields" )]
public class TextNode : NodeComponent
{
[Property, InlineEditor, Group( "Layout Settings" ), Order( -999 )] public override NodeStyle Style { get; set; } = GetDefaultStyle();
[Property, Group("Text Settings")] public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Center;
[Property, Group( "Text Settings" )] public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Center;
[Property, InlineEditor, Group( "Text Settings" )] public TextRendering.Scope TextScope { get; set; } = TextRendering.Scope.Default;
private static NodeStyle GetDefaultStyle()
{
return new NodeStyle()
{
Anchor = NodePoint.CenterMiddle,
Pivot = NodePoint.CenterMiddle,
Offset = Vector2.Zero,
Size = new Vector2( 100, 100 ),
ChildPadding = Vector2.Zero,
ClipChildren = false,
StretchHorizontal = false,
StretchVertical = false,
CornerRadius = 0,
BorderWidth = 0,
BorderColor = Color.Black,
Texture = Texture.White,
Tint = Color.White,
UvScale = Vector2.One,
UvOffset = Vector2.Zero
};
}
protected override void UpdateStyle(float scaleFactor)
{
Style.BorderWidth = 0; // for debugging
Style.BorderColor = Color.White;
TextRendering.Scope scope = TextScope;
scope.FontSize *= scaleFactor;
Texture texture = TextRendering.GetOrCreateTexture( scope );
Style.Texture = texture;
Vector2 scale = (Layout.Outer.Size * scaleFactor) / texture.Size;
Style.UvScale = scale;
float alignX = HorizontalAlignment switch
{
HorizontalAlignment.Left => 0f,
HorizontalAlignment.Center => 0.5f,
HorizontalAlignment.Right => 1f,
_ => 0.5f,
};
float alignY = VerticalAlignment switch
{
VerticalAlignment.Top => 0f,
VerticalAlignment.Center => 0.5f,
VerticalAlignment.Bottom => 1f,
_ => 0.5f,
};
float offsetX = alignX * (1f / scale.x - 1f);
float offsetY = alignY * (1f / scale.y - 1f);
Style.UvOffset = new Vector2( offsetX, offsetY );
}
}
using Sandbox.UiPro;
namespace Sandbox;
// Hooks up the Button's OnClick event and responds
// by updating the TextNode
public class ExampleButtonController : Component
{
[Property] public Button MyButton { get; set; }
[Property] public TextNode MyText { get; set; }
[Property, ReadOnly] public int ClickCount { get; set; } = 0;
protected override void OnStart()
{
if ( !MyButton.IsValid() ) return;
MyButton.OnClick = OnButtonClicked;
}
private void OnButtonClicked()
{
ClickCount++;
if ( !MyText.IsValid() ) return;
TextRendering.Scope scope = MyText.TextScope;
scope.Text = $"Clicked {ClickCount} Times";
MyText.TextScope = scope;
}
}
using System;
namespace Sandbox.UiPro;
public enum NodePoint
{
TopLeft, TopMiddle, TopRight,
CenterLeft, CenterMiddle, CenterRight,
BottomLeft, BottomMiddle, BottomRight,
}
public class NodeStyle
{
[Property] public NodePoint Anchor { get; set; }
[Property] public NodePoint Pivot { get; set; }
[Property] public Vector2 Offset { get; set; }
[Property] public Vector2 Size { get; set; }
[Property] public Vector4 ChildPadding { get; set; }
[Property] public bool ClipChildren { get; set; }
[Property] public bool StretchHorizontal { get; set; }
[Property] public bool StretchVertical { get; set; }
[Property, Hide] public float CornerRadius { get; set; }
[Property, Hide] public float BorderWidth { get; set; }
[Property, Hide] public Color BorderColor { get; set; }
[Property, Hide] public Texture Texture { get; set; }
[Property, Hide] public Color Tint { get; set; }
[Property, Hide] public Vector2 UvScale { get; set; }
[Property, Hide] public Vector2 UvOffset { get; set; }
}
public class NodeLayout
{
public Rect Outer { get; private set; }
public Rect Inner { get; private set; }
public Rect ClipRect { get; private set; }
public float ClipRadius { get; private set; }
public Rect ChildClipRect { get; private set; }
public float ChildClipRadius { get; private set; }
public static NodeLayout GetRootLayout( Vector2 size )
{
Rect rootRect = new Rect( Vector2.Zero, size );
NodeLayout layout = new NodeLayout()
{
Outer = rootRect,
Inner = rootRect,
ClipRect = rootRect,
ClipRadius = 0,
ChildClipRect = rootRect,
ChildClipRadius = 0
};
return layout;
}
public void Compute( NodeLayout parentLayout, NodeStyle style )
{
Vector2 anchor = AnchorFraction( style.Anchor );
Vector2 pivot = AnchorFraction( style.Pivot );
float width = style.StretchHorizontal ? parentLayout.Inner.Size.x : style.Size.x;
float height = style.StretchVertical ? parentLayout.Inner.Size.y : style.Size.y;
float x = style.StretchHorizontal
? parentLayout.Inner.Position.x + style.Offset.x
: parentLayout.Inner.Position.x + anchor.x * parentLayout.Inner.Size.x - pivot.x * width + style.Offset.x;
float y = style.StretchVertical
? parentLayout.Inner.Position.y + style.Offset.y
: parentLayout.Inner.Position.y + anchor.y * parentLayout.Inner.Size.y - pivot.y * height + style.Offset.y;
Outer = new Rect( new Vector2( x, y ), new Vector2( width, height ) );
float innerW = Math.Max( 0f, width - style.ChildPadding.x - style.ChildPadding.z );
float innerH = Math.Max( 0f, height - style.ChildPadding.y - style.ChildPadding.w );
Inner = new Rect( new Vector2( x + style.ChildPadding.x, y + style.ChildPadding.y ), new Vector2( innerW, innerH ) );
ClipRect = parentLayout.ChildClipRect;
ClipRadius = parentLayout.ChildClipRadius;
if ( style.ClipChildren )
{
float bw = Math.Max( 0f, style.BorderWidth );
float clipW = Math.Max( 0f, Outer.Size.x - bw * 2f );
float clipH = Math.Max( 0f, Outer.Size.y - bw * 2f );
ChildClipRect = new Rect( new Vector2( Outer.Position.x + bw, Outer.Position.y + bw ), new Vector2( clipW, clipH ) );
ChildClipRadius = Math.Max( 0f, style.CornerRadius - bw );
}
else
{
ChildClipRect = parentLayout.ChildClipRect;
ChildClipRadius = parentLayout.ChildClipRadius;
}
}
private static Vector2 AnchorFraction( NodePoint point )
{
float fx = point switch
{
NodePoint.TopLeft or NodePoint.CenterLeft or NodePoint.BottomLeft => 0f,
NodePoint.TopMiddle or NodePoint.CenterMiddle or NodePoint.BottomMiddle => 0.5f,
_ => 1f,
};
float fy = point switch
{
NodePoint.TopLeft or NodePoint.TopMiddle or NodePoint.TopRight => 0f,
NodePoint.CenterLeft or NodePoint.CenterMiddle or NodePoint.CenterRight => 0.5f,
_ => 1f,
};
return new Vector2( fx, fy );
}
}
global using Sandbox;
global using Editor;
global using System.Collections.Generic;
global using System.Linq;
global using Dreams.UltimateLightManager;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Dreams.UltimateLightManager;
[Library( "UltimateLightManager" )]
[Title( "Ultimate Light Manager" )]
[Description( "Advanced light component with presets, runtime controls, grouping, and an integrated S&box editor workflow." )]
[Category( "Light" )]
[Icon( "tungsten" )]
public class UltimateLightManager : Component, Component.ExecuteInEditor
{
public enum LightTypeEnum
{
Point,
Spot
}
public enum LightPreset
{
Custom,
Candle,
Torch,
Neon,
Alarm,
BrokenLamp,
SciFi,
StreetLight
}
public static void SetGroupState( string groupName, bool isEnabled )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.SetEnabledState( isEnabled );
}
}
public static void SetGroupBrightness( string groupName, float brightness )
{
brightness = Math.Max( brightness, 0f );
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.SetBrightnessLevel( brightness );
}
}
public static void SetGroupColor( string groupName, Color color )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.SetLightColorValue( color );
}
}
public static void ApplyPresetToGroup( string groupName, LightPreset preset )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.ApplyPreset( preset );
}
}
public static void TriggerGroupFlash( string groupName, float duration = 0.15f, float brightnessMultiplier = 2f )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.TriggerFlash( duration, brightnessMultiplier );
}
}
public static void TriggerGroupAlarm( string groupName, float duration = 2f )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.TriggerAlarm( duration );
}
}
public static void SetPowerGridState( string powerGridTag, bool isPowered )
{
foreach ( var light in GetLightsInPowerGrid( powerGridTag ) )
{
light.SetPowered( isPowered );
}
}
private static IEnumerable<UltimateLightManager> GetLightsInGroup( string groupName )
{
return EnumerateAllLights().Where( light => string.Equals( light.LightGroup, groupName, StringComparison.OrdinalIgnoreCase ) );
}
private static IEnumerable<UltimateLightManager> GetLightsInPowerGrid( string powerGridTag )
{
return EnumerateAllLights().Where( light => string.Equals( light.PowerGridTag, powerGridTag, StringComparison.OrdinalIgnoreCase ) );
}
private static IEnumerable<UltimateLightManager> EnumerateAllLights()
{
var visitedScenes = new HashSet<Scene>();
var visitedLights = new HashSet<UltimateLightManager>();
foreach ( var scene in EnumerateCandidateScenes() )
{
if ( scene == null || !visitedScenes.Add( scene ) )
{
continue;
}
foreach ( var light in scene.GetAllComponents<UltimateLightManager>() )
{
if ( light != null && visitedLights.Add( light ) )
{
yield return light;
}
}
}
}
private static IEnumerable<Scene> EnumerateCandidateScenes()
{
if ( Game.ActiveScene != null )
{
yield return Game.ActiveScene;
}
foreach ( var scene in Scene.All )
{
if ( scene != null )
{
yield return scene;
}
}
}
[Property, Order( -1 ), Group( "Management" )]
public string LightGroup { get; set; } = "Default";
[Property, Group( "Management" )]
public string PowerGridTag { get; set; } = string.Empty;
[Property, Group( "Management" )]
public float StartDelay { get; set; } = 0.0f;
[Property, Group( "Management" )]
public bool AutoDesync { get; set; } = true;
[Property, Group( "Management" )]
public bool ShowDebugGizmos { get; set; } = false;
[Property, Group( "Management" )]
public bool ForceNetworkObjectMode { get; set; } = true;
[Property, Group( "General" ), Sync( SyncFlags.FromHost )]
public LightTypeEnum TargetLightType { get; set; } = LightTypeEnum.Point;
[Property, Group( "General" ), Sync( SyncFlags.FromHost )]
public bool IsEnabled { get; set; } = true;
[Property, Group( "General" ), Sync( SyncFlags.FromHost )]
public Color LightColor { get; set; } = Color.White;
[Property, Group( "General" ), Range( 0, 100 ), Sync( SyncFlags.FromHost | SyncFlags.Interpolate )]
public float Brightness { get; set; } = 1.0f;
[Property, Group( "General" ), Range( 0, 10 )]
public float VolumetricBoost { get; set; } = 1.0f;
[Property, Group( "General" )]
public bool CastShadows { get; set; } = true;
[Property, Group( "Presets" ), Sync( SyncFlags.FromHost )]
public LightPreset SelectedPreset { get; set; } = LightPreset.Custom;
[Property, Group( "Presets" ), Sync( SyncFlags.FromHost )]
public bool AutoApplyPreset { get; set; } = true;
[Property, Group( "Transitions" )]
public bool EnableFade { get; set; } = false;
[Property, Group( "Transitions" )]
public float FadeInDuration { get; set; } = 0.2f;
[Property, Group( "Transitions" )]
public float FadeOutDuration { get; set; } = 0.2f;
[Property, Group( "Audio" )]
public SoundEvent AmbientSound { get; set; }
[Property, Group( "Audio" )]
public SoundEvent ToggleOnSound { get; set; }
[Property, Group( "Audio" )]
public SoundEvent ToggleOffSound { get; set; }
[Property, Group( "Audio" )]
public bool ModulateVolumeWithLight { get; set; } = true;
[Property, Group( "Audio" )]
public bool ModulatePitchWithLight { get; set; } = false;
[Property, Group( "Audio" ), Range( 0, 5 )]
public float BaseVolume { get; set; } = 1.0f;
[Property, Group( "Audio" ), Range( 0.5f, 2f )]
public float MinPitch { get; set; } = 0.9f;
[Property, Group( "Audio" ), Range( 0.5f, 2f )]
public float MaxPitch { get; set; } = 1.1f;
[Property, Group( "Optimization" )]
public float MaxDistance { get; set; } = 2500.0f;
[Property, Group( "Optimization" )]
public float ShadowMaxDistance { get; set; } = 800.0f;
[Property, Group( "Optimization" )]
public bool EnableCulling { get; set; } = true;
[Property, Group( "Optimization" )]
public bool EnableAdaptiveUpdates { get; set; } = false;
[Property, Group( "Optimization" ), Range( 1, 120 )]
public float NearUpdateRate { get; set; } = 60.0f;
[Property, Group( "Optimization" ), Range( 1, 120 )]
public float FarUpdateRate { get; set; } = 12.0f;
[Property, Group( "Gameplay" )]
public float DefaultAlarmDuration { get; set; } = 2.0f;
[Property, Group( "Gameplay" ), Sync( SyncFlags.FromHost )]
public Color AlarmColor { get; set; } = new Color( 1.0f, 0.15f, 0.1f );
[Property, Group( "Gameplay" )]
public float AlarmStrobeSpeed { get; set; } = 8.0f;
[Property, Group( "Gameplay" )]
public float AlarmBrightnessMultiplier { get; set; } = 1.5f;
[Property, FeatureEnabled( "Fire & Candle" )]
public bool EnableFire { get; set; } = false;
[Property, Feature( "Fire & Candle" )]
public float FireSpeed { get; set; } = 12.0f;
[Property, Feature( "Fire & Candle" ), Range( 0, 1 )]
public float FireIntensity { get; set; } = 0.3f;
[Property, Feature( "Fire & Candle" ), Range( 0, 2 )]
public float FireChaos { get; set; } = 1.0f;
[Property, FeatureEnabled( "Horror Mode" )]
public bool EnableHorror { get; set; } = false;
[Property, Feature( "Horror Mode" )]
public float MinFlickerDelay { get; set; } = 0.05f;
[Property, Feature( "Horror Mode" )]
public float MaxFlickerDelay { get; set; } = 0.4f;
[Property, Feature( "Horror Mode" ), Range( 0, 1 )]
public float DamageSeverity { get; set; } = 0.8f;
[Property, Feature( "Horror Mode" )]
public SoundEvent SparkSound { get; set; }
[Property, FeatureEnabled( "Disco Mode" )]
public bool EnableDisco { get; set; } = false;
[Property, Feature( "Disco Mode" )]
public float DiscoSpeed { get; set; } = 20.0f;
[Property, Feature( "Disco Mode" ), Range( 0, 1 )]
public float DiscoSaturation { get; set; } = 1.0f;
[Property, Feature( "Disco Mode" ), Range( 0, 1 )]
public float DiscoValue { get; set; } = 1.0f;
[Property, FeatureEnabled( "Color Transition" )]
public bool EnableColorTransition { get; set; } = false;
[Property, Feature( "Color Transition" ), Sync( SyncFlags.FromHost )]
public Color SecondaryColor { get; set; } = new Color( 0.2f, 0.85f, 1.0f );
[Property, Feature( "Color Transition" )]
public float ColorTransitionSpeed { get; set; } = 1.0f;
[Property, FeatureEnabled( "Proximity Sensor" )]
public bool EnableSensor { get; set; } = false;
[Property, Feature( "Proximity Sensor" )]
public float SensorRange { get; set; } = 300.0f;
[Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
public float SensorMinBrightness { get; set; } = 0.0f;
[Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
public float SensorMaxBrightness { get; set; } = 1.0f;
[Property, Feature( "Proximity Sensor" ), Range( 1, 20 )]
public float SensorSmoothness { get; set; } = 5.0f;
[Property, Feature( "Proximity Sensor" )]
public bool InvertSensor { get; set; } = false;
[Property, FeatureEnabled( "Motion Sway" )]
public bool EnableSway { get; set; } = false;
[Property, Feature( "Motion Sway" )]
public float SwaySpeedPitch { get; set; } = 1.0f;
[Property, Feature( "Motion Sway" )]
public float SwayAmountPitch { get; set; } = 5.0f;
[Property, Feature( "Motion Sway" )]
public float SwaySpeedRoll { get; set; } = 0.7f;
[Property, Feature( "Motion Sway" )]
public float SwayAmountRoll { get; set; } = 3.0f;
[Property, FeatureEnabled( "Flicker Pattern" )]
public bool EnablePattern { get; set; } = false;
[Property, Feature( "Flicker Pattern" )]
public string Pattern { get; set; } = "mmnmmommommnonmmonqnmmo";
[Property, Feature( "Flicker Pattern" )]
public float PatternSpeed { get; set; } = 10.0f;
[Property, FeatureEnabled( "Pulse" )]
public bool EnablePulse { get; set; } = false;
[Property, Feature( "Pulse" )]
public float PulseSpeed { get; set; } = 1.0f;
[Property, Feature( "Pulse" ), Range( 0, 1 )]
public float PulseMin { get; set; } = 0.2f;
[Property, FeatureEnabled( "Strobe" )]
public bool EnableStrobe { get; set; } = false;
[Property, Feature( "Strobe" )]
public float StrobeSpeed { get; set; } = 10.0f;
[Property, Feature( "Strobe" ), Range( 0.1f, 0.9f )]
public float StrobeDutyCycle { get; set; } = 0.5f;
[Property, FeatureEnabled( "Kelvin" )]
public bool EnableKelvin { get; set; } = false;
[Property, Feature( "Kelvin" ), Range( 1000, 12000 )]
public int KelvinTemperature { get; set; } = 4500;
[Property, FeatureEnabled( "Power Surge" )]
public bool EnablePowerSurge { get; set; } = false;
[Property, Feature( "Power Surge" )]
public float SurgeMinInterval { get; set; } = 4.0f;
[Property, Feature( "Power Surge" )]
public float SurgeMaxInterval { get; set; } = 10.0f;
[Property, Feature( "Power Surge" )]
public float SurgeDuration { get; set; } = 0.15f;
[Property, Feature( "Power Surge" )]
public float SurgeBrightnessMultiplier { get; set; } = 1.8f;
public bool Powered => PoweredState;
public float ExternalBrightnessMultiplier => ExternalBrightnessMultiplierState;
public bool HasColorOverride => HasExternalColorOverrideState;
public bool AlarmActive => AlarmEndTimeState > RealTime.Now;
private PointLight _pointLight;
private SpotLight _spotLight;
private Light ActiveLight => TargetLightType == LightTypeEnum.Point ? (Light)_pointLight : _spotLight;
private int _lastKelvin = -1;
private Color _cachedKelvinColor = Color.White;
private Rotation _baseRotation;
private bool _isInitialized;
private bool _hasAppliedPreset;
private LightPreset _lastAppliedPreset = LightPreset.Custom;
private LightPreset _lastObservedPreset = LightPreset.Custom;
private bool _lastObservedAutoApplyPreset = true;
private LightTypeEnum _lastSyncedLightType = LightTypeEnum.Point;
private float _brokenMultiplier = 1.0f;
private float _nextFlicker;
private float _sensorWeightTarget = 1.0f;
private float _sensorWeightCurrent = 1.0f;
private float _randomTimeOffset;
private float _creationTime;
private float _lastUpdateTimestamp;
private float _enabledBlend = 1.0f;
private float _nextAdaptiveUpdateTime;
private float _nextViewerCameraRefreshTime;
private float _nextSurgeTime;
private float _surgeEndTime;
private bool _hasOutputState;
private bool _lastOutputEnabled;
private CameraComponent _cachedViewerCamera;
private SoundHandle _ambientSoundHandle;
[Sync( SyncFlags.FromHost )]
private bool PoweredState { get; set; } = true;
[Sync( SyncFlags.FromHost | SyncFlags.Interpolate )]
private float ExternalBrightnessMultiplierState { get; set; } = 1.0f;
[Sync( SyncFlags.FromHost )]
private bool HasExternalColorOverrideState { get; set; }
[Sync( SyncFlags.FromHost )]
private Color ExternalColorOverrideState { get; set; } = Color.White;
[Sync( SyncFlags.FromHost )]
private float FlashEndTimeState { get; set; }
[Sync( SyncFlags.FromHost )]
private float FlashBrightnessMultiplierState { get; set; } = 1.0f;
[Sync( SyncFlags.FromHost )]
private float AlarmEndTimeState { get; set; }
protected override void OnAwake()
{
EnsureNetworkMode();
}
protected override void OnStart()
{
EnsureNetworkMode();
_baseRotation = LocalRotation;
_creationTime = RealTime.Now;
_lastUpdateTimestamp = _creationTime;
_enabledBlend = IsEnabled ? 1.0f : 0.0f;
if ( AutoDesync )
{
_randomTimeOffset = Game.Random.Float( 0f, 100f );
}
SyncComponentsIfNeeded( force: true );
if ( AutoApplyPreset )
{
ApplyPresetInternal( SelectedPreset );
}
ScheduleNextSurge( _creationTime );
_lastObservedPreset = SelectedPreset;
_lastObservedAutoApplyPreset = AutoApplyPreset;
_isInitialized = true;
}
protected override void OnUpdate()
{
if ( !_isInitialized )
{
return;
}
SyncComponentsIfNeeded();
SyncPresetSelectionIfNeeded();
var light = ActiveLight;
if ( light == null )
{
return;
}
bool isPlaying = Game.IsPlaying;
float absoluteTime = RealTime.Now;
float effectTime = (isPlaying ? Time.Now : absoluteTime) + _randomTimeOffset;
float deltaTime = GetFrameDelta( absoluteTime );
if ( isPlaying && StartDelay > 0f && (absoluteTime - _creationTime) < StartDelay )
{
DisableOutput( light );
return;
}
var viewerCam = GetViewerCamera( absoluteTime );
float distSq = viewerCam != null ? WorldPosition.DistanceSquared( viewerCam.WorldPosition ) : 0f;
if ( ShouldSkipAdaptiveUpdate( isPlaying, viewerCam, distSq, absoluteTime ) )
{
UpdateAmbientSoundPosition();
return;
}
UpdateSensorWeight( viewerCam, distSq, deltaTime );
UpdateEnabledBlend( deltaTime );
UpdateSway( effectTime );
if ( isPlaying && EnableCulling && viewerCam != null && distSq > MaxDistance * MaxDistance )
{
DisableOutput( light );
return;
}
float fx = 1.0f;
fx *= EvaluatePulseAndStrobe( effectTime );
fx *= EvaluatePattern( effectTime );
fx *= EvaluateFire( effectTime );
fx *= EvaluateHorror( effectTime, isPlaying );
fx *= EvaluatePowerSurge( absoluteTime );
fx *= EvaluateAlarm( effectTime, absoluteTime );
if ( absoluteTime < FlashEndTimeState )
{
fx *= FlashBrightnessMultiplierState;
}
float finalBrightness = Brightness * fx * _sensorWeightCurrent * _enabledBlend * ExternalBrightnessMultiplierState;
finalBrightness = Math.Max( finalBrightness, 0f );
bool shouldBeEnabled = finalBrightness > 0.001f;
light.Enabled = shouldBeEnabled;
Color resolvedColor = ResolveLightColor( effectTime, absoluteTime );
light.LightColor = resolvedColor * finalBrightness;
light.Shadows = CastShadows && ( !isPlaying || distSq < ShadowMaxDistance * ShadowMaxDistance );
light.FogStrength = VolumetricBoost;
UpdateOutputState( shouldBeEnabled, true );
ManageAudio( shouldBeEnabled, finalBrightness / Math.Max( Brightness, 0.01f ) );
}
public void TurnOn()
{
SetEnabledState( true );
}
[Button]
public void ApplySelectedPreset()
{
ApplyPreset( SelectedPreset );
}
public void TurnOff()
{
SetEnabledState( false );
}
public void Toggle()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
IsEnabled = !IsEnabled;
}
public void SetPowered( bool powered )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
PoweredState = powered;
}
public void SetExternalBrightness( float multiplier )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
ExternalBrightnessMultiplierState = Math.Max( multiplier, 0f );
}
public void ResetExternalBrightness()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
ExternalBrightnessMultiplierState = 1.0f;
}
public void SetColorOverride( Color color )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
HasExternalColorOverrideState = true;
ExternalColorOverrideState = color;
}
public void ClearColorOverride()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
HasExternalColorOverrideState = false;
ExternalColorOverrideState = Color.White;
}
public void TriggerFlash( float duration = 0.15f, float brightnessMultiplier = 2f )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
FlashEndTimeState = RealTime.Now + Math.Max( duration, 0.01f );
FlashBrightnessMultiplierState = Math.Max( brightnessMultiplier, 1.0f );
}
public void TriggerAlarm( float duration = -1f )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
if ( duration <= 0f )
{
duration = DefaultAlarmDuration;
}
AlarmEndTimeState = Math.Max( AlarmEndTimeState, RealTime.Now + duration );
}
public void ClearAlarm()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
AlarmEndTimeState = 0f;
}
[Button]
public void PreviewFlash()
{
TriggerFlash();
}
[Button]
public void PreviewAlarm()
{
TriggerAlarm();
}
[Button]
public void ResetRuntimeOverrides()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
ClearAlarm();
ClearColorOverride();
ResetExternalBrightness();
SetPowered( true );
}
public void ApplyPreset( LightPreset preset )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
ApplyPresetInternal( preset );
}
public void SetEnabledState( bool isEnabled )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
IsEnabled = isEnabled;
}
public void SetBrightnessLevel( float brightness )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
Brightness = Math.Max( brightness, 0f );
}
public void SetLightColorValue( Color color )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
LightColor = color;
}
private void ApplyPresetInternal( LightPreset preset )
{
SelectedPreset = preset;
ResetPresetControlledFeatures();
switch ( preset )
{
case LightPreset.Candle:
Brightness = 0.75f;
LightColor = new Color( 1.0f, 0.76f, 0.5f );
SecondaryColor = new Color( 1.0f, 0.66f, 0.35f );
EnableKelvin = true;
KelvinTemperature = 1800;
EnableFire = true;
FireSpeed = 10.0f;
FireIntensity = 0.18f;
FireChaos = 0.6f;
VolumetricBoost = 0.6f;
break;
case LightPreset.Torch:
Brightness = 1.35f;
LightColor = new Color( 1.0f, 0.72f, 0.38f );
SecondaryColor = new Color( 1.0f, 0.45f, 0.2f );
EnableKelvin = true;
KelvinTemperature = 2200;
EnableFire = true;
FireSpeed = 13.0f;
FireIntensity = 0.28f;
FireChaos = 1.0f;
VolumetricBoost = 1.4f;
break;
case LightPreset.Neon:
Brightness = 1.15f;
LightColor = new Color( 0.2f, 0.95f, 1.0f );
SecondaryColor = new Color( 1.0f, 0.2f, 0.85f );
EnableColorTransition = true;
ColorTransitionSpeed = 0.65f;
EnablePulse = true;
PulseSpeed = 1.2f;
PulseMin = 0.75f;
CastShadows = false;
VolumetricBoost = 0.25f;
break;
case LightPreset.Alarm:
Brightness = 2.0f;
LightColor = new Color( 1.0f, 0.18f, 0.12f );
AlarmColor = LightColor;
EnableStrobe = true;
StrobeSpeed = 7.0f;
StrobeDutyCycle = 0.45f;
CastShadows = false;
VolumetricBoost = 1.1f;
break;
case LightPreset.BrokenLamp:
Brightness = 1.0f;
LightColor = new Color( 1.0f, 0.93f, 0.82f );
EnableHorror = true;
MinFlickerDelay = 0.04f;
MaxFlickerDelay = 0.25f;
DamageSeverity = 0.85f;
EnableKelvin = true;
KelvinTemperature = 3400;
break;
case LightPreset.SciFi:
Brightness = 1.6f;
LightColor = new Color( 0.35f, 0.78f, 1.0f );
SecondaryColor = new Color( 0.1f, 1.0f, 0.8f );
EnableColorTransition = true;
ColorTransitionSpeed = 1.15f;
EnablePulse = true;
PulseSpeed = 0.85f;
PulseMin = 0.55f;
VolumetricBoost = 2.0f;
break;
case LightPreset.StreetLight:
Brightness = 1.4f;
LightColor = new Color( 1.0f, 0.84f, 0.68f );
EnableKelvin = true;
KelvinTemperature = 3500;
MaxDistance = 4500.0f;
ShadowMaxDistance = 1200.0f;
VolumetricBoost = 0.55f;
break;
case LightPreset.Custom:
default:
break;
}
_lastAppliedPreset = preset;
_hasAppliedPreset = true;
}
private void ResetPresetControlledFeatures()
{
EnableFire = false;
EnableHorror = false;
EnableDisco = false;
EnableColorTransition = false;
EnablePulse = false;
EnableStrobe = false;
EnableKelvin = false;
}
private void SyncPresetSelectionIfNeeded()
{
bool autoApplyChanged = AutoApplyPreset != _lastObservedAutoApplyPreset;
bool presetChanged = SelectedPreset != _lastObservedPreset;
_lastObservedAutoApplyPreset = AutoApplyPreset;
_lastObservedPreset = SelectedPreset;
if ( !AutoApplyPreset )
{
return;
}
if ( autoApplyChanged || presetChanged || !_hasAppliedPreset || SelectedPreset != _lastAppliedPreset )
{
ApplyPresetInternal( SelectedPreset );
}
}
private void SyncComponentsIfNeeded( bool force = false )
{
bool missingActiveLight = TargetLightType == LightTypeEnum.Point ? _pointLight == null : _spotLight == null;
if ( !force && !missingActiveLight && TargetLightType == _lastSyncedLightType )
{
return;
}
Component createdComponent = null;
if ( TargetLightType == LightTypeEnum.Point )
{
if ( _pointLight == null )
{
_pointLight = Components.GetOrCreate<PointLight>();
createdComponent = _pointLight;
}
if ( _spotLight != null && _spotLight.Enabled )
{
_spotLight.Enabled = false;
}
}
else
{
if ( _spotLight == null )
{
_spotLight = Components.GetOrCreate<SpotLight>();
createdComponent = _spotLight;
}
if ( _pointLight != null && _pointLight.Enabled )
{
_pointLight.Enabled = false;
}
}
_lastSyncedLightType = TargetLightType;
if ( createdComponent != null && Game.IsPlaying && GameObject.NetworkMode == NetworkMode.Object )
{
GameObject.Network.Refresh( createdComponent );
}
}
private CameraComponent GetViewerCamera( float absoluteTime )
{
var sceneCamera = Scene.Camera;
if ( sceneCamera != null )
{
_cachedViewerCamera = sceneCamera;
_nextViewerCameraRefreshTime = absoluteTime + 0.25f;
return sceneCamera;
}
if ( _cachedViewerCamera != null && absoluteTime < _nextViewerCameraRefreshTime )
{
return _cachedViewerCamera;
}
_nextViewerCameraRefreshTime = absoluteTime + 0.25f;
_cachedViewerCamera = Scene.GetAllComponents<CameraComponent>().FirstOrDefault( camera => camera != null && camera.Enabled );
return _cachedViewerCamera;
}
private void EnsureNetworkMode()
{
if ( !ForceNetworkObjectMode || !Game.IsPlaying || GameObject.NetworkMode == NetworkMode.Object )
{
return;
}
GameObject.NetworkMode = NetworkMode.Object;
}
private bool ShouldIgnoreNetworkMutation()
{
return Game.IsPlaying && IsProxy;
}
private float GetFrameDelta( float absoluteTime )
{
float delta = Math.Clamp( absoluteTime - _lastUpdateTimestamp, 0.0001f, 0.25f );
_lastUpdateTimestamp = absoluteTime;
return delta;
}
private bool ShouldSkipAdaptiveUpdate( bool isPlaying, CameraComponent viewerCam, float distSq, float absoluteTime )
{
if ( !EnableAdaptiveUpdates || !isPlaying || viewerCam == null )
{
return false;
}
if ( absoluteTime < _nextAdaptiveUpdateTime )
{
return true;
}
float maxDistanceSq = Math.Max( MaxDistance * MaxDistance, 1f );
float distRatio = Math.Clamp( distSq / maxDistanceSq, 0f, 1f );
float nearInterval = 1f / Math.Max( NearUpdateRate, 1f );
float farInterval = 1f / Math.Max( FarUpdateRate, 1f );
_nextAdaptiveUpdateTime = absoluteTime + MathX.Lerp( nearInterval, farInterval, distRatio );
return false;
}
private void UpdateSensorWeight( CameraComponent viewerCam, float distSq, float deltaTime )
{
if ( EnableSensor && viewerCam != null && SensorRange > 0.01f )
{
float distRatio = Math.Clamp( 1.0f - (MathF.Sqrt( distSq ) / SensorRange), 0f, 1f );
float rawWeight = InvertSensor ? 1.0f - distRatio : distRatio;
_sensorWeightTarget = MathX.Lerp( SensorMinBrightness, SensorMaxBrightness, rawWeight );
}
else
{
_sensorWeightTarget = 1.0f;
}
_sensorWeightCurrent = MathX.Lerp( _sensorWeightCurrent, _sensorWeightTarget, Math.Clamp( deltaTime * SensorSmoothness, 0f, 1f ) );
}
private void UpdateEnabledBlend( float deltaTime )
{
float target = IsEnabled && PoweredState ? 1.0f : 0.0f;
if ( !EnableFade )
{
_enabledBlend = target;
return;
}
float duration = target > _enabledBlend ? Math.Max( FadeInDuration, 0.0001f ) : Math.Max( FadeOutDuration, 0.0001f );
float lerp = Math.Clamp( deltaTime / duration, 0f, 1f );
_enabledBlend = MathX.Lerp( _enabledBlend, target, lerp );
if ( Math.Abs( _enabledBlend - target ) < 0.001f )
{
_enabledBlend = target;
}
}
private void UpdateSway( float effectTime )
{
if ( EnableSway )
{
float pitch = MathF.Sin( effectTime * SwaySpeedPitch ) * SwayAmountPitch;
float roll = MathF.Cos( effectTime * SwaySpeedRoll ) * SwayAmountRoll;
LocalRotation = _baseRotation * Rotation.From( pitch, 0f, roll );
return;
}
LocalRotation = _baseRotation;
}
private float EvaluatePulseAndStrobe( float effectTime )
{
if ( EnableStrobe )
{
float cycle = (effectTime * StrobeSpeed) % 1.0f;
return cycle < StrobeDutyCycle ? 1.0f : 0.0f;
}
if ( EnablePulse )
{
float sine = (MathF.Sin( effectTime * PulseSpeed * 2.0f ) + 1.0f) * 0.5f;
return MathX.Lerp( PulseMin, 1.0f, sine );
}
return 1.0f;
}
private float EvaluatePattern( float effectTime )
{
if ( !EnablePattern || string.IsNullOrWhiteSpace( Pattern ) )
{
return 1.0f;
}
int index = (int)(effectTime * PatternSpeed) % Pattern.Length;
float value = Math.Max( 0, (char.ToLower( Pattern[index] ) - 'a') / 12.0f );
return value;
}
private float EvaluateFire( float effectTime )
{
if ( !EnableFire )
{
return 1.0f;
}
float noise = MathF.Sin( effectTime * FireSpeed ) + MathF.Sin( effectTime * FireSpeed * 0.5f );
if ( FireChaos > 0f )
{
noise += MathF.Sin( effectTime * FireSpeed * 1.5f ) * FireChaos;
}
return 1.0f - (noise * 0.15f * FireIntensity);
}
private float EvaluateHorror( float effectTime, bool isPlaying )
{
if ( !EnableHorror )
{
return 1.0f;
}
if ( effectTime > _nextFlicker )
{
bool isDamaged = Game.Random.Float( 0f, 1f ) < DamageSeverity;
_brokenMultiplier = isDamaged ? Game.Random.Float( 0.0f, 0.4f ) : 1.0f;
_nextFlicker = effectTime + Game.Random.Float( MinFlickerDelay, MaxFlickerDelay );
if ( isPlaying && isDamaged && SparkSound != null && _brokenMultiplier < 0.2f )
{
Sound.Play( SparkSound, WorldPosition );
}
}
return _brokenMultiplier;
}
private float EvaluatePowerSurge( float absoluteTime )
{
if ( !EnablePowerSurge )
{
return 1.0f;
}
if ( _nextSurgeTime <= 0f )
{
ScheduleNextSurge( absoluteTime );
}
if ( absoluteTime >= _nextSurgeTime )
{
_surgeEndTime = absoluteTime + Math.Max( SurgeDuration, 0.01f );
ScheduleNextSurge( _surgeEndTime );
}
return absoluteTime < _surgeEndTime ? Math.Max( SurgeBrightnessMultiplier, 1.0f ) : 1.0f;
}
private float EvaluateAlarm( float effectTime, float absoluteTime )
{
if ( absoluteTime >= AlarmEndTimeState )
{
return 1.0f;
}
float cycle = (effectTime * AlarmStrobeSpeed) % 1.0f;
float gate = cycle < 0.5f ? 1.0f : 0.15f;
return gate * Math.Max( AlarmBrightnessMultiplier, 0f );
}
private void ScheduleNextSurge( float absoluteTime )
{
float minInterval = Math.Min( SurgeMinInterval, SurgeMaxInterval );
float maxInterval = Math.Max( SurgeMinInterval, SurgeMaxInterval );
_nextSurgeTime = absoluteTime + Game.Random.Float( Math.Max( minInterval, 0.01f ), Math.Max( maxInterval, 0.01f ) );
}
private Color ResolveLightColor( float effectTime, float absoluteTime )
{
Color color = LightColor;
if ( EnableKelvin )
{
if ( KelvinTemperature != _lastKelvin )
{
_cachedKelvinColor = KelvinToColor( KelvinTemperature );
_lastKelvin = KelvinTemperature;
}
color = _cachedKelvinColor;
}
if ( EnableColorTransition )
{
float lerp = (MathF.Sin( effectTime * ColorTransitionSpeed ) + 1.0f) * 0.5f;
color = Color.Lerp( color, SecondaryColor, lerp, true );
}
if ( EnableDisco )
{
color = new ColorHsv( (effectTime * DiscoSpeed) % 360f, DiscoSaturation, DiscoValue ).ToColor();
}
if ( absoluteTime < AlarmEndTimeState )
{
color = Color.Lerp( color, AlarmColor, 0.85f, true );
}
if ( HasExternalColorOverrideState )
{
color = ExternalColorOverrideState;
}
return color;
}
private void UpdateOutputState( bool shouldBeEnabled, bool playOneShot )
{
if ( !_hasOutputState )
{
_hasOutputState = true;
_lastOutputEnabled = shouldBeEnabled;
return;
}
if ( _lastOutputEnabled == shouldBeEnabled )
{
return;
}
if ( playOneShot && Game.IsPlaying )
{
if ( shouldBeEnabled && ToggleOnSound != null )
{
Sound.Play( ToggleOnSound, WorldPosition );
}
else if ( !shouldBeEnabled && ToggleOffSound != null )
{
Sound.Play( ToggleOffSound, WorldPosition );
}
}
_lastOutputEnabled = shouldBeEnabled;
}
private void DisableOutput( Light light )
{
light.Enabled = false;
UpdateOutputState( false, false );
ManageAudio( false, 0f );
}
private void UpdateAmbientSoundPosition()
{
if ( _ambientSoundHandle != null && !_ambientSoundHandle.IsStopped )
{
_ambientSoundHandle.Position = WorldPosition;
}
}
private void ManageAudio( bool isLightEnabled, float intensityRatio )
{
if ( !Game.IsPlaying || AmbientSound == null )
{
return;
}
if ( isLightEnabled )
{
if ( _ambientSoundHandle == null || _ambientSoundHandle.IsStopped )
{
_ambientSoundHandle = Sound.Play( AmbientSound, WorldPosition );
}
if ( _ambientSoundHandle != null )
{
_ambientSoundHandle.Position = WorldPosition;
_ambientSoundHandle.Volume = BaseVolume * (ModulateVolumeWithLight ? intensityRatio : 1.0f);
_ambientSoundHandle.Pitch = ModulatePitchWithLight ? MathX.Lerp( MinPitch, MaxPitch, intensityRatio ) : 1.0f;
}
}
else if ( _ambientSoundHandle != null )
{
_ambientSoundHandle.Stop();
_ambientSoundHandle = null;
}
}
private Color KelvinToColor( int kelvin )
{
float temperature = kelvin / 100.0f;
float red;
float green;
float blue;
if ( temperature <= 66f )
{
red = 255f;
green = Math.Clamp( 99.47f * MathF.Log( temperature ) - 161.11f, 0f, 255f );
}
else
{
red = Math.Clamp( 329.698f * MathF.Pow( temperature - 60f, -0.133f ), 0f, 255f );
green = Math.Clamp( 288.12f * MathF.Pow( temperature - 60f, -0.075f ), 0f, 255f );
}
if ( temperature >= 66f )
{
blue = 255f;
}
else if ( temperature <= 19f )
{
blue = 0f;
}
else
{
blue = Math.Clamp( 138.51f * MathF.Log( temperature - 10f ) - 305.04f, 0f, 255f );
}
return new Color( red / 255f, green / 255f, blue / 255f );
}
protected override void DrawGizmos()
{
if ( !ShowDebugGizmos )
{
return;
}
Gizmo.Draw.Text( $"Group: {LightGroup}", new Transform( Vector3.Up * 20f ), size: 12 );
if ( !string.IsNullOrWhiteSpace( PowerGridTag ) )
{
Gizmo.Draw.Text( $"Grid: {PowerGridTag}", new Transform( Vector3.Up * 34f ), size: 12 );
}
if ( EnableSensor )
{
Gizmo.Draw.Color = Color.Cyan.WithAlpha( 0.2f );
Gizmo.Draw.SolidSphere( Vector3.Zero, SensorRange );
Gizmo.Draw.Color = Color.Cyan;
Gizmo.Draw.LineSphere( Vector3.Zero, SensorRange );
}
if ( EnableCulling )
{
Gizmo.Draw.Color = Color.Red.WithAlpha( 0.05f );
Gizmo.Draw.LineSphere( Vector3.Zero, MaxDistance );
Gizmo.Draw.Text( $"Cull: {MaxDistance}", new Transform( Vector3.Up * (MaxDistance * 0.9f) ), size: 14 );
}
}
protected override void OnDestroy()
{
if ( _ambientSoundHandle != null )
{
_ambientSoundHandle.Stop();
}
}
}