Search the source of every open source package.
1802 results
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 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using Editor;
using Sandbox;
using System.Text;
class SpectogramWidget : Widget
{
private short[] samples;
private int sampleRate;
private List<int> splitPoints = new List<int>();
private int? dragPoint = null;
private Label loadingLabel;
private Label dropLabel;
private bool isLoading = true;
public SoundFile CurrentSound { get; private set; }
public SpectogramWidget(SoundFile soundFile) : base(null)
{
MinimumSize = 100;
MouseTracking = true;
AcceptDrops = true;
loadingLabel = new Label(this);
loadingLabel.Text = "Loading audio data...";
loadingLabel.Visible = false;
dropLabel = new Label(this);
dropLabel.Text = "Drop a sound file here";
dropLabel.SetStyles("font-size: 18px; color: #aaa; text-align: center;");
if (soundFile != null)
{
LoadSound(soundFile);
}
}
public async void LoadSound(SoundFile soundFile)
{
CurrentSound = soundFile;
isLoading = true;
samples = null;
splitPoints.Clear();
loadingLabel.Visible = true;
dropLabel.Visible = false;
await LoadAudioDataAsync(soundFile);
}
private async Task LoadAudioDataAsync(SoundFile soundFile)
{
try
{
await soundFile.LoadAsync();
samples = await soundFile.GetSamplesAsync();
if (samples == null)
{
loadingLabel.Text = "Failed to load audio data";
return;
}
sampleRate = soundFile.Rate;
splitPoints.Add(0);
splitPoints.Add(samples.Length - 1);
loadingLabel.Visible = false;
isLoading = false;
Update();
}
catch (Exception ex)
{
loadingLabel.Text = $"Error loading audio: {ex.Message}";
}
}
protected override void DoLayout()
{
base.DoLayout();
if (loadingLabel != null)
{
loadingLabel.Position = new Vector2(10, Height / 2 - 10);
loadingLabel.Size = new Vector2(Width - 20, 20);
}
if (dropLabel != null)
{
dropLabel.Position = new Vector2(10, Height / 2 - 10);
dropLabel.Size = new Vector2(Width - 20, 20);
}
}
public override void OnDragDrop(DragEvent e)
{
base.OnDragDrop(e);
if (!e.Data.HasFileOrFolder) return;
var asset = AssetSystem.FindByPath(e.Data.FileOrFolder);
if (asset?.AssetType != AssetType.SoundFile) return;
var soundFile = SoundFile.Load(asset.Path);
if (soundFile != null)
{
LoadSound(soundFile);
}
}
public override void OnDragHover(DragEvent e)
{
base.OnDragHover(e);
if (!e.Data.HasFileOrFolder) return;
var asset = AssetSystem.FindByPath(e.Data.FileOrFolder);
if (asset?.AssetType != AssetType.SoundFile) return;
e.Action = DropAction.Link;
}
protected override void OnMouseClick(MouseEvent e)
{
if (isLoading) return;
base.OnMouseClick(e);
if (e.Button == MouseButtons.Left)
{
var samplePos = (int)(e.LocalPosition.x / Width * samples.Length);
var nearPoint = splitPoints.FirstOrDefault(p => Math.Abs(p - samplePos) < (samples.Length / Width * 5));
if (nearPoint != default)
{
dragPoint = splitPoints.IndexOf(nearPoint);
}
else
{
splitPoints.Add(samplePos);
splitPoints.Sort();
Update();
}
}
}
protected override void OnMouseMove(MouseEvent e)
{
if (isLoading) return;
base.OnMouseMove(e);
if (dragPoint.HasValue)
{
var samplePos = (int)(e.LocalPosition.x / Width * samples.Length);
splitPoints[dragPoint.Value] = samplePos;
splitPoints.Sort();
Update();
}
}
protected override void OnMouseReleased(MouseEvent e)
{
if (isLoading) return;
base.OnMouseReleased(e);
dragPoint = null;
}
protected override void OnPaint()
{
base.OnPaint();
if (isLoading || samples == null)
{
return;
}
Paint.ClearPen();
Paint.SetBrush(Theme.Grey.WithAlpha(0.1f));
Paint.DrawRect(LocalRect);
Paint.SetPen(Theme.Blue);
var samplesPerPixel = samples.Length / Width;
for (int x = 0; x < Width; x++)
{
var startSample = (int)(x * samplesPerPixel);
var endSample = Math.Min(startSample + samplesPerPixel, samples.Length);
var max = short.MinValue;
var min = short.MaxValue;
for (int i = startSample; i < endSample; i++)
{
max = Math.Max(max, samples[i]);
min = Math.Min(min, samples[i]);
}
var y1 = Height / 2 + (min / (float)short.MaxValue * Height / 2);
var y2 = Height / 2 + (max / (float)short.MaxValue * Height / 2);
Paint.DrawLine(new Vector2(x, y1), new Vector2(x, y2));
}
Paint.SetPen(Theme.Red);
foreach (var point in splitPoints)
{
var x = point / (float)samples.Length * Width;
Paint.DrawLine(new Vector2(x, 0), new Vector2(x, Height));
}
}
public List<int> GetSplitPoints()
{
return new List<int>(splitPoints);
}
public void SplitCurrentSound(Action<SoundFile> onSoundCreated)
{
if (CurrentSound == null || samples == null) return;
var splitPoints = GetSplitPoints();
if (splitPoints.Count < 2) return;
try
{
var baseFileName = Path.GetFileNameWithoutExtension(CurrentSound.ResourcePath);
var outputDir = Path.Combine(
Project.Current.GetAssetsPath(),
"generated",
$"{baseFileName}_splits"
);
Directory.CreateDirectory(outputDir);
for (int i = 0; i < splitPoints.Count - 1; i++)
{
var start = splitPoints[i];
var end = splitPoints[i + 1];
var length = end - start;
var segmentSamples = new short[length];
Array.Copy(samples, start, segmentSamples, 0, length);
var wavPath = Path.Combine(outputDir, $"{baseFileName}_part_{i + 1}.wav");
using (var writer = new BinaryWriter(File.Create(wavPath)))
{
writer.Write(Encoding.ASCII.GetBytes("RIFF"));
writer.Write(36 + (segmentSamples.Length * 2));
writer.Write(Encoding.ASCII.GetBytes("WAVE"));
writer.Write(Encoding.ASCII.GetBytes("fmt "));
writer.Write(16);
writer.Write((short)1);
writer.Write((short)CurrentSound.Channels);
writer.Write(CurrentSound.Rate);
writer.Write(CurrentSound.Rate * CurrentSound.Channels * 2);
writer.Write((short)(CurrentSound.Channels * 2));
writer.Write((short)16);
writer.Write(Encoding.ASCII.GetBytes("data"));
writer.Write(segmentSamples.Length * 2);
foreach (var sample in segmentSamples)
{
writer.Write(sample);
}
}
var asset = AssetSystem.RegisterFile(wavPath);
if (asset != null)
{
var soundFile = SoundFile.Load(asset.RelativePath);
if (soundFile != null)
{
onSoundCreated?.Invoke(soundFile);
}
}
}
}
catch (Exception ex)
{
Log.Error($"Error splitting sound: {ex.Message}");
}
}
}using System;
using System.Threading.Tasks;
using Editor;
using Sandbox;
namespace YAAI;
public sealed class YaaiWindow : Window
{
public Task RunningTask;
private bool IsRunning = false;
public readonly int MinimumSpeed = 10;
public readonly int MaximumSpeed = 30;
private Random random = new Random();
private float RandomSpeed => random.Float( MinimumSpeed, MaximumSpeed );
public YaaiWindow()
{
WindowTitle = "YAAI";
Width = 250;
Height = 200;
HasMaximizeButton = false;
DeleteOnClose = true;
WindowFlags = WindowFlags.WithFlag( WindowFlags.WindowStaysOnTopHint, true );
WindowFlags = WindowFlags.WithFlag( WindowFlags.MinimizeButton, false);
new YaaiWidget(this );
Show();
RunningTask = Move();
}
protected override bool OnClose()
{
// Dispose of Task
IsRunning = false;
RunningTask.Wait();
RunningTask.Dispose();
YaaiManager.OnWindowClose( this );
return base.OnClose();
}
async Task Move()
{
IsRunning = true;
bool moveDown = random.Int( 0,1 ) == 1;
bool moveRight = random.Int( 0,1 ) == 1;
float Speed = RandomSpeed;
while ( IsWindow && IsRunning )
{
float nx = moveRight ? Position.x + Speed/2 : Position.x - Speed/2;
float ny = moveDown ? Position.y + Speed : Position.y - Speed;
bool shouldChangeSpeed = false;
if ( Position.x + Width > ScreenGeometry.Width )
{
shouldChangeSpeed = true;
moveRight = false;
}
else if ( Position.x < 0)
{
shouldChangeSpeed = true;
moveRight = true;
}
if ( Position.y + Height > ScreenGeometry.Height )
{
shouldChangeSpeed = true;
moveDown = false;
}
else if ( Position.y < 0)
{
shouldChangeSpeed = true;
moveDown = true;
}
if ( shouldChangeSpeed )
Speed = RandomSpeed;
Position = Position.WithX( nx ).WithY( ny );
await Task.Delay( 25 );
}
}
}
using Editor;
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;
namespace Ink.Parsed
{
// Used by the FlowBase when constructing the weave flow from
// a flat list of content objects.
public class Weave : Parsed.Object
{
// Containers can be chained as multiple gather points
// get created as the same indentation level.
// rootContainer is always the first in the chain, while
// currentContainer is the latest.
public Runtime.Container rootContainer {
get {
if (_rootContainer == null) {
GenerateRuntimeObject ();
}
return _rootContainer;
}
}
Runtime.Container currentContainer { get; set; }
public int baseIndentIndex { get; private set; }
// Loose ends are:
// - Choices or Gathers that need to be joined up
// - Explicit Divert to gather points (i.e. "->" without a target)
public List<IWeavePoint> looseEnds;
public List<GatherPointToResolve> gatherPointsToResolve;
public class GatherPointToResolve
{
public Runtime.Divert divert;
public Runtime.Object targetRuntimeObj;
}
public Parsed.Object lastParsedSignificantObject
{
get {
if (content.Count == 0) return null;
// Don't count extraneous newlines or VAR/CONST declarations,
// since they're "empty" statements outside of the main flow.
Parsed.Object lastObject = null;
for (int i = content.Count - 1; i >= 0; --i) {
lastObject = content [i];
var lastText = lastObject as Parsed.Text;
if (lastText && lastText.text == "\n") {
continue;
}
if (IsGlobalDeclaration (lastObject))
continue;
break;
}
var lastWeave = lastObject as Weave;
if (lastWeave)
lastObject = lastWeave.lastParsedSignificantObject;
return lastObject;
}
}
public Weave(List<Parsed.Object> cont, int indentIndex=-1)
{
if (indentIndex == -1) {
baseIndentIndex = DetermineBaseIndentationFromContent (cont);
} else {
baseIndentIndex = indentIndex;
}
AddContent (cont);
ConstructWeaveHierarchyFromIndentation ();
}
public void ResolveWeavePointNaming ()
{
var namedWeavePoints = FindAll<IWeavePoint> (w => !string.IsNullOrEmpty (w.name));
_namedWeavePoints = new Dictionary<string, IWeavePoint> ();
foreach (var weavePoint in namedWeavePoints) {
// Check for weave point naming collisions
IWeavePoint existingWeavePoint;
if (_namedWeavePoints.TryGetValue (weavePoint.name, out existingWeavePoint)) {
var typeName = existingWeavePoint is Gather ? "gather" : "choice";
var existingObj = (Parsed.Object)existingWeavePoint;
Error ("A " + typeName + " with the same label name '" + weavePoint.name + "' already exists in this context on line " + existingObj.debugMetadata.startLineNumber, (Parsed.Object)weavePoint);
}
_namedWeavePoints [weavePoint.name] = weavePoint;
}
}
void ConstructWeaveHierarchyFromIndentation()
{
// Find nested indentation and convert to a proper object hierarchy
// (i.e. indented content is replaced with a Weave object that contains
// that nested content)
int contentIdx = 0;
while (contentIdx < content.Count) {
Parsed.Object obj = content [contentIdx];
// Choice or Gather
if (obj is IWeavePoint) {
var weavePoint = (IWeavePoint)obj;
var weaveIndentIdx = weavePoint.indentationDepth - 1;
// Inner level indentation - recurse
if (weaveIndentIdx > baseIndentIndex) {
// Step through content until indent jumps out again
int innerWeaveStartIdx = contentIdx;
while (contentIdx < content.Count) {
var innerWeaveObj = content [contentIdx] as IWeavePoint;
if (innerWeaveObj != null) {
var innerIndentIdx = innerWeaveObj.indentationDepth - 1;
if (innerIndentIdx <= baseIndentIndex) {
break;
}
}
contentIdx++;
}
int weaveContentCount = contentIdx - innerWeaveStartIdx;
var weaveContent = content.GetRange (innerWeaveStartIdx, weaveContentCount);
content.RemoveRange (innerWeaveStartIdx, weaveContentCount);
var weave = new Weave (weaveContent, weaveIndentIdx);
InsertContent (innerWeaveStartIdx, weave);
// Continue iteration from this point
contentIdx = innerWeaveStartIdx;
}
}
contentIdx++;
}
}
// When the indentation wasn't told to us at construction time using
// a choice point with a known indentation level, we may be told to
// determine the indentation level by incrementing from our closest ancestor.
public int DetermineBaseIndentationFromContent(List<Parsed.Object> contentList)
{
foreach (var obj in contentList) {
if (obj is IWeavePoint) {
return ((IWeavePoint)obj).indentationDepth - 1;
}
}
// No weave points, so it doesn't matter
return 0;
}
public override Runtime.Object GenerateRuntimeObject ()
{
_rootContainer = currentContainer = new Runtime.Container();
looseEnds = new List<IWeavePoint> ();
gatherPointsToResolve = new List<GatherPointToResolve> ();
// Iterate through content for the block at this level of indentation
// - Normal content is nested under Choices and Gathers
// - Blocks that are further indented cause recursion
// - Keep track of loose ends so that they can be diverted to Gathers
foreach(var obj in content) {
// Choice or Gather
if (obj is IWeavePoint) {
AddRuntimeForWeavePoint ((IWeavePoint)obj);
}
// Non-weave point
else {
// Nested weave
if (obj is Weave) {
var weave = (Weave)obj;
AddRuntimeForNestedWeave (weave);
gatherPointsToResolve.AddRange (weave.gatherPointsToResolve);
}
// Other object
// May be complex object that contains statements - e.g. a multi-line conditional
else {
AddGeneralRuntimeContent (obj.runtimeObject);
}
}
}
// Pass any loose ends up the hierarhcy
PassLooseEndsToAncestors();
return _rootContainer;
}
// Found gather point:
// - gather any loose ends
// - set the gather as the main container to dump new content in
void AddRuntimeForGather(Gather gather)
{
// Determine whether this Gather should be auto-entered:
// - It is auto-entered if there were no choices in the last section
// - A section is "since the previous gather" - so reset now
bool autoEnter = !hasSeenChoiceInSection;
hasSeenChoiceInSection = false;
var gatherContainer = gather.runtimeContainer;
if (gather.name == null) {
// Use disallowed character so it's impossible to have a name collision
gatherContainer.name = "g-" + _unnamedGatherCount;
_unnamedGatherCount++;
}
// Auto-enter: include in main content
if (autoEnter) {
currentContainer.AddContent (gatherContainer);
}
// Don't auto-enter:
// Add this gather to the main content, but only accessible
// by name so that it isn't stepped into automatically, but only via
// a divert from a loose end.
else {
_rootContainer.AddToNamedContentOnly (gatherContainer);
}
// Consume loose ends: divert them to this gather
foreach (IWeavePoint looseEndWeavePoint in looseEnds) {
var looseEnd = (Parsed.Object)looseEndWeavePoint;
// Skip gather loose ends that are at the same level
// since they'll be handled by the auto-enter code below
// that only jumps into the gather if (current runtime choices == 0)
if (looseEnd is Gather) {
var prevGather = (Gather)looseEnd;
if (prevGather.indentationDepth == gather.indentationDepth) {
continue;
}
}
Runtime.Divert divert = null;
if (looseEnd is Parsed.Divert) {
divert = (Runtime.Divert) looseEnd.runtimeObject;
} else {
divert = new Runtime.Divert ();
var looseWeavePoint = looseEnd as IWeavePoint;
looseWeavePoint.runtimeContainer.AddContent (divert);
}
// Pass back knowledge of this loose end being diverted
// to the FlowBase so that it can maintain a list of them,
// and resolve the divert references later
gatherPointsToResolve.Add (new GatherPointToResolve{ divert = divert, targetRuntimeObj = gatherContainer });
}
looseEnds.Clear ();
// Replace the current container itself
currentContainer = gatherContainer;
}
void AddRuntimeForWeavePoint(IWeavePoint weavePoint)
{
// Current level Gather
if (weavePoint is Gather) {
AddRuntimeForGather ((Gather)weavePoint);
}
// Current level choice
else if (weavePoint is Choice) {
// Gathers that contain choices are no longer loose ends
// (same as when weave points get nested content)
if (previousWeavePoint is Gather) {
looseEnds.Remove (previousWeavePoint);
}
// Add choice point content
var choice = (Choice)weavePoint;
currentContainer.AddContent (choice.runtimeObject);
// Add choice's inner content to self
choice.innerContentContainer.name = "c-" + _choiceCount;
currentContainer.AddToNamedContentOnly (choice.innerContentContainer);
_choiceCount++;
hasSeenChoiceInSection = true;
}
// Keep track of loose ends
addContentToPreviousWeavePoint = false; // default
if (WeavePointHasLooseEnd (weavePoint)) {
looseEnds.Add (weavePoint);
var looseChoice = weavePoint as Choice;
if (looseChoice) {
addContentToPreviousWeavePoint = true;
}
}
previousWeavePoint = weavePoint;
}
// Add nested block at a greater indentation level
public void AddRuntimeForNestedWeave(Weave nestedResult)
{
// Add this inner block to current container
// (i.e. within the main container, or within the last defined Choice/Gather)
AddGeneralRuntimeContent (nestedResult.rootContainer);
// Now there's a deeper indentation level, the previous weave point doesn't
// count as a loose end (since it will have content to go to)
if (previousWeavePoint != null) {
looseEnds.Remove (previousWeavePoint);
addContentToPreviousWeavePoint = false;
}
}
// Normal content gets added into the latest Choice or Gather by default,
// unless there hasn't been one yet.
void AddGeneralRuntimeContent(Runtime.Object content)
{
// Content is allowed to evaluate runtimeObject to null
// (e.g. AuthorWarning, which doesn't make it into the runtime)
if (content == null)
return;
if (addContentToPreviousWeavePoint) {
previousWeavePoint.runtimeContainer.AddContent (content);
} else {
currentContainer.AddContent (content);
}
}
void PassLooseEndsToAncestors()
{
if (looseEnds.Count == 0) return;
// Search for Weave ancestor to pass loose ends to for gathering.
// There are two types depending on whether the current weave
// is separated by a conditional or sequence.
// - An "inner" weave is one that is directly connected to the current
// weave - i.e. you don't have to pass through a conditional or
// sequence to get to it. We're allowed to pass all loose ends to
// one of these.
// - An "outer" weave is one that is outside of a conditional/sequence
// that the current weave is nested within. We're only allowed to
// pass gathers (i.e. 'normal flow') loose ends up there, not normal
// choices. The rule is that choices have to be diverted explicitly
// by the author since it's ambiguous where flow should go otherwise.
//
// e.g.:
//
// - top <- e.g. outer weave
// {true:
// * choice <- e.g. inner weave
// * * choice 2
// more content <- e.g. current weave
// * choice 2
// }
// - more of outer weave
//
Weave closestInnerWeaveAncestor = null;
Weave closestOuterWeaveAncestor = null;
// Find inner and outer ancestor weaves as defined above.
bool nested = false;
for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
{
// Found ancestor?
var weaveAncestor = ancestor as Weave;
if (weaveAncestor != null)
{
if (!nested && closestInnerWeaveAncestor == null)
closestInnerWeaveAncestor = weaveAncestor;
if (nested && closestOuterWeaveAncestor == null)
closestOuterWeaveAncestor = weaveAncestor;
}
// Weaves nested within Sequences or Conditionals are
// "sealed" - any loose ends require explicit diverts.
if (ancestor is Sequence || ancestor is Conditional)
nested = true;
}
// No weave to pass loose ends to at all?
if (closestInnerWeaveAncestor == null && closestOuterWeaveAncestor == null)
return;
// Follow loose end passing logic as defined above
for (int i = looseEnds.Count - 1; i >= 0; i--) {
var looseEnd = looseEnds[i];
bool received = false;
// This weave is nested within a conditional or sequence:
// - choices can only be passed up to direct ancestor ("inner") weaves
// - gathers can be passed up to either, but favour the closer (inner) weave
// if there is one
if(nested) {
if( looseEnd is Choice && closestInnerWeaveAncestor != null) {
closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
received = true;
}
else if( !(looseEnd is Choice) ) {
var receivingWeave = closestInnerWeaveAncestor ?? closestOuterWeaveAncestor;
if(receivingWeave != null) {
receivingWeave.ReceiveLooseEnd(looseEnd);
received = true;
}
}
}
// No nesting, all loose ends can be safely passed up
else {
closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
received = true;
}
if(received) looseEnds.RemoveAt(i);
}
}
void ReceiveLooseEnd(IWeavePoint childWeaveLooseEnd)
{
looseEnds.Add(childWeaveLooseEnd);
}
public override void ResolveReferences(Story context)
{
base.ResolveReferences (context);
// Check that choices nested within conditionals and sequences are terminated
if( looseEnds != null && looseEnds.Count > 0 ) {
var isNestedWeave = false;
for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
{
if (ancestor is Sequence || ancestor is Conditional)
{
isNestedWeave = true;
break;
}
}
if (isNestedWeave)
{
ValidateTermination(BadNestedTerminationHandler);
}
}
foreach(var gatherPoint in gatherPointsToResolve) {
gatherPoint.divert.targetPath = gatherPoint.targetRuntimeObj.path;
}
CheckForWeavePointNamingCollisions ();
}
public IWeavePoint WeavePointNamed(string name)
{
if (_namedWeavePoints == null)
return null;
IWeavePoint weavePointResult = null;
if (_namedWeavePoints.TryGetValue (name, out weavePointResult))
return weavePointResult;
return null;
}
// Global VARs and CONSTs are treated as "outside of the flow"
// when iterating over content that follows loose ends
bool IsGlobalDeclaration (Parsed.Object obj)
{
var varAss = obj as VariableAssignment;
if (varAss && varAss.isGlobalDeclaration && varAss.isDeclaration)
return true;
var constDecl = obj as ConstantDeclaration;
if (constDecl)
return true;
return false;
}
// While analysing final loose ends, we look to see whether there
// are any diverts etc which choices etc divert from
IEnumerable<Parsed.Object> ContentThatFollowsWeavePoint (IWeavePoint weavePoint)
{
var obj = (Parsed.Object)weavePoint;
// Inner content first (e.g. for a choice)
if (obj.content != null) {
foreach (var contentObj in obj.content) {
// Global VARs and CONSTs are treated as "outside of the flow"
if (IsGlobalDeclaration (contentObj)) continue;
yield return contentObj;
}
}
var parentWeave = obj.parent as Weave;
if (parentWeave == null) {
throw new System.Exception ("Expected weave point parent to be weave?");
}
var weavePointIdx = parentWeave.content.IndexOf (obj);
for (int i = weavePointIdx+1; i < parentWeave.content.Count; i++) {
var laterObj = parentWeave.content [i];
// Global VARs and CONSTs are treated as "outside of the flow"
if (IsGlobalDeclaration (laterObj)) continue;
// End of the current flow
if (laterObj is IWeavePoint)
break;
// Other weaves will be have their own loose ends
if (laterObj is Weave)
break;
yield return laterObj;
}
}
public delegate void BadTerminationHandler (Parsed.Object terminatingObj);
public void ValidateTermination (BadTerminationHandler badTerminationHandler)
{
// Don't worry if the last object in the flow is a "TODO",
// even if there are other loose ends in other places
if (lastParsedSignificantObject is AuthorWarning) {
return;
}
// By now, any sub-weaves will have passed loose ends up to the root weave (this).
// So there are 2 possible situations:
// - There are loose ends from somewhere in the flow.
// These aren't necessarily "real" loose ends - they're weave points
// that don't connect to any lower weave points, so we just
// have to check that they terminate properly.
// - This weave is just a list of content with no actual weave points,
// so we just need to check that the list of content terminates.
bool hasLooseEnds = looseEnds != null && looseEnds.Count > 0;
if (hasLooseEnds) {
foreach (var looseEnd in looseEnds) {
var looseEndFlow = ContentThatFollowsWeavePoint (looseEnd);
ValidateFlowOfObjectsTerminates (looseEndFlow, (Parsed.Object)looseEnd, badTerminationHandler);
}
}
// No loose ends... is there any inner weaving at all?
// If not, make sure the single content stream is terminated correctly
else {
// If there's any actual weaving, assume that content is
// terminated correctly since we would've had a loose end otherwise
foreach (var obj in content) {
if (obj is IWeavePoint) return;
}
// Straight linear flow? Check it terminates
ValidateFlowOfObjectsTerminates (content, this, badTerminationHandler);
}
}
void BadNestedTerminationHandler(Parsed.Object terminatingObj)
{
Conditional conditional = null;
for (var ancestor = terminatingObj.parent; ancestor != null; ancestor = ancestor.parent) {
if( ancestor is Sequence || ancestor is Conditional ) {
conditional = ancestor as Conditional;
break;
}
}
var errorMsg = "Choices nested in conditionals or sequences need to explicitly divert afterwards.";
// Tutorialise proper choice syntax if this looks like a single choice within a condition, e.g.
// { condition:
// * choice
// }
if (conditional != null) {
var numChoices = conditional.FindAll<Choice>().Count;
if( numChoices == 1 ) {
errorMsg = "Choices with conditions should be written: '* {condition} choice'. Otherwise, "+ errorMsg.ToLower();
}
}
Error(errorMsg, terminatingObj);
}
void ValidateFlowOfObjectsTerminates (IEnumerable<Parsed.Object> objFlow, Parsed.Object defaultObj, BadTerminationHandler badTerminationHandler)
{
bool terminated = false;
Parsed.Object terminatingObj = defaultObj;
foreach (var flowObj in objFlow) {
var divert = flowObj.Find<Divert> (d => !d.isThread && !d.isTunnel && !d.isFunctionCall && !(d.parent is DivertTarget));
if (divert != null) {
terminated = true;
}
if (flowObj.Find<TunnelOnwards> () != null) {
terminated = true;
break;
}
terminatingObj = flowObj;
}
if (!terminated) {
// Author has left a note to self here - clearly we don't need
// to leave them with another warning since they know what they're doing.
if (terminatingObj is AuthorWarning) {
return;
}
badTerminationHandler (terminatingObj);
}
}
bool WeavePointHasLooseEnd(IWeavePoint weavePoint)
{
// No content, must be a loose end.
if (weavePoint.content == null) return true;
// If a weave point is diverted from, it doesn't have a loose end.
// Detect a divert object within a weavePoint's main content
// Work backwards since we're really interested in the end,
// although it doesn't actually make a difference!
// (content after a divert will simply be inaccessible)
for (int i = weavePoint.content.Count - 1; i >= 0; --i) {
var innerDivert = weavePoint.content [i] as Divert;
if (innerDivert) {
bool willReturn = innerDivert.isThread || innerDivert.isTunnel || innerDivert.isFunctionCall;
if (!willReturn) return false;
}
}
return true;
}
// Enforce rule that weave points must not have the same
// name as any stitches or knots upwards in the hierarchy
void CheckForWeavePointNamingCollisions()
{
if (_namedWeavePoints == null)
return;
var ancestorFlows = new List<FlowBase> ();
foreach (var obj in this.ancestry) {
var flow = obj as FlowBase;
if (flow)
ancestorFlows.Add (flow);
else
break;
}
foreach (var namedWeavePointPair in _namedWeavePoints) {
var weavePointName = namedWeavePointPair.Key;
var weavePoint = (Parsed.Object) namedWeavePointPair.Value;
foreach(var flow in ancestorFlows) {
// Shallow search
var otherContentWithName = flow.ContentWithNameAtLevel (weavePointName);
if (otherContentWithName && otherContentWithName != weavePoint) {
var errorMsg = string.Format ("{0} '{1}' has the same label name as a {2} (on {3})",
weavePoint.GetType().Name,
weavePointName,
otherContentWithName.GetType().Name,
otherContentWithName.debugMetadata);
Error(errorMsg, (Parsed.Object) weavePoint);
}
}
}
}
// Keep track of previous weave point (Choice or Gather)
// at the current indentation level:
// - to add ordinary content to be nested under it
// - to add nested content under it when it's indented
// - to remove it from the list of loose ends when
// - it has indented content since it's no longer a loose end
// - it's a gather and it has a choice added to it
IWeavePoint previousWeavePoint = null;
bool addContentToPreviousWeavePoint = false;
// Used for determining whether the next Gather should auto-enter
bool hasSeenChoiceInSection = false;
int _unnamedGatherCount;
int _choiceCount;
Runtime.Container _rootContainer;
Dictionary<string, IWeavePoint> _namedWeavePoints;
}
}
namespace Ink.Parsed
{
public class Wrap<T> : Parsed.Object where T : Runtime.Object
{
public Wrap (T objToWrap)
{
_objToWrap = objToWrap;
}
public override Runtime.Object GenerateRuntimeObject ()
{
return _objToWrap;
}
T _objToWrap;
}
// Shorthand for writing Parsed.Wrap<Runtime.Glue> and Parsed.Wrap<Runtime.Tag>
public class Glue : Wrap<Runtime.Glue> {
public Glue (Runtime.Glue glue) : base(glue) {}
}
public class LegacyTag : Wrap<Runtime.Tag> {
public LegacyTag (Runtime.Tag tag) : base (tag) { }
}
}
using System.Runtime.CompilerServices;
public static class OpenSimplex2S
{
private const long PRIME_X = 0x5205402B9270C86FL;
private const long PRIME_Y = 0x598CD327003817B5L;
private const long PRIME_Z = 0x5BCC226E9FA0BACBL;
private const long PRIME_W = 0x56CC5227E58F554BL;
private const long HASH_MULTIPLIER = 0x53A3F72DEEC546F5L;
private const long SEED_FLIP_3D = -0x52D547B2E96ED629L;
private const double ROOT2OVER2 = 0.7071067811865476;
private const double SKEW_2D = 0.366025403784439;
private const double UNSKEW_2D = -0.21132486540518713;
private const double ROOT3OVER3 = 0.577350269189626;
private const double FALLBACK_ROTATE3 = 2.0 / 3.0;
private const double ROTATE3_ORTHOGONALIZER = UNSKEW_2D;
private const float SKEW_4D = 0.309016994374947f;
private const float UNSKEW_4D = -0.138196601125011f;
private const int N_GRADS_2D_EXPONENT = 7;
private const int N_GRADS_3D_EXPONENT = 8;
private const int N_GRADS_4D_EXPONENT = 9;
private const int N_GRADS_2D = 1 << N_GRADS_2D_EXPONENT;
private const int N_GRADS_3D = 1 << N_GRADS_3D_EXPONENT;
private const int N_GRADS_4D = 1 << N_GRADS_4D_EXPONENT;
private const double NORMALIZER_2D = 0.05481866495625118;
private const double NORMALIZER_3D = 0.2781926117527186;
private const double NORMALIZER_4D = 0.11127401889945551;
private const float RSQUARED_2D = 2.0f / 3.0f;
private const float RSQUARED_3D = 3.0f / 4.0f;
private const float RSQUARED_4D = 4.0f / 5.0f;
/*
* Noise Evaluators
*/
/**
* 2D OpenSimplex2S/SuperSimplex noise, standard lattice orientation.
*/
public static float Noise2( long seed, double x, double y )
{
// Get points for A2* lattice
double s = SKEW_2D * (x + y);
double xs = x + s, ys = y + s;
return Noise2_UnskewedBase( seed, xs, ys );
}
/**
* 2D OpenSimplex2S/SuperSimplex noise, with Y pointing down the main diagonal.
* Might be better for a 2D sandbox style game, where Y is vertical.
* Probably slightly less optimal for heightmaps or continent maps,
* unless your map is centered around an equator. It's a slight
* difference, but the option is here to make it easy.
*/
public static float Noise2_ImproveX( long seed, double x, double y )
{
// Skew transform and rotation baked into one.
double xx = x * ROOT2OVER2;
double yy = y * (ROOT2OVER2 * (1 + 2 * SKEW_2D));
return Noise2_UnskewedBase( seed, yy + xx, yy - xx );
}
/**
* 2D OpenSimplex2S/SuperSimplex noise base.
*/
private static float Noise2_UnskewedBase( long seed, double xs, double ys )
{
// Get base points and offsets.
int xsb = FastFloor( xs ), ysb = FastFloor( ys );
float xi = (float)(xs - xsb), yi = (float)(ys - ysb);
// Prime pre-multiplication for hash.
long xsbp = xsb * PRIME_X, ysbp = ysb * PRIME_Y;
// Unskew.
float t = (xi + yi) * (float)UNSKEW_2D;
float dx0 = xi + t, dy0 = yi + t;
// First vertex.
float a0 = RSQUARED_2D - dx0 * dx0 - dy0 * dy0;
float value = (a0 * a0) * (a0 * a0) * Grad( seed, xsbp, ysbp, dx0, dy0 );
// Second vertex.
float a1 = (float)(2 * (1 + 2 * UNSKEW_2D) * (1 / UNSKEW_2D + 2)) * t + ((float)(-2 * (1 + 2 * UNSKEW_2D) * (1 + 2 * UNSKEW_2D)) + a0);
float dx1 = dx0 - (float)(1 + 2 * UNSKEW_2D);
float dy1 = dy0 - (float)(1 + 2 * UNSKEW_2D);
value += (a1 * a1) * (a1 * a1) * Grad( seed, xsbp + PRIME_X, ysbp + PRIME_Y, dx1, dy1 );
// Third and fourth vertices.
// Nested conditionals were faster than compact bit logic/arithmetic.
float xmyi = xi - yi;
if ( t < UNSKEW_2D )
{
if ( xi + xmyi > 1 )
{
float dx2 = dx0 - (float)(3 * UNSKEW_2D + 2);
float dy2 = dy0 - (float)(3 * UNSKEW_2D + 1);
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp + (PRIME_X << 1), ysbp + PRIME_Y, dx2, dy2 );
}
}
else
{
float dx2 = dx0 - (float)UNSKEW_2D;
float dy2 = dy0 - (float)(UNSKEW_2D + 1);
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp + PRIME_Y, dx2, dy2 );
}
}
if ( yi - xmyi > 1 )
{
float dx3 = dx0 - (float)(3 * UNSKEW_2D + 1);
float dy3 = dy0 - (float)(3 * UNSKEW_2D + 2);
float a3 = RSQUARED_2D - dx3 * dx3 - dy3 * dy3;
if ( a3 > 0 )
{
value += (a3 * a3) * (a3 * a3) * Grad( seed, xsbp + PRIME_X, ysbp + (PRIME_Y << 1), dx3, dy3 );
}
}
else
{
float dx3 = dx0 - (float)(UNSKEW_2D + 1);
float dy3 = dy0 - (float)UNSKEW_2D;
float a3 = RSQUARED_2D - dx3 * dx3 - dy3 * dy3;
if ( a3 > 0 )
{
value += (a3 * a3) * (a3 * a3) * Grad( seed, xsbp + PRIME_X, ysbp, dx3, dy3 );
}
}
}
else
{
if ( xi + xmyi < 0 )
{
float dx2 = dx0 + (float)(1 + UNSKEW_2D);
float dy2 = dy0 + (float)UNSKEW_2D;
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp - PRIME_X, ysbp, dx2, dy2 );
}
}
else
{
float dx2 = dx0 - (float)(UNSKEW_2D + 1);
float dy2 = dy0 - (float)UNSKEW_2D;
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp + PRIME_X, ysbp, dx2, dy2 );
}
}
if ( yi < xmyi )
{
float dx2 = dx0 + (float)UNSKEW_2D;
float dy2 = dy0 + (float)(UNSKEW_2D + 1);
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp - PRIME_Y, dx2, dy2 );
}
}
else
{
float dx2 = dx0 - (float)UNSKEW_2D;
float dy2 = dy0 - (float)(UNSKEW_2D + 1);
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp + PRIME_Y, dx2, dy2 );
}
}
}
return value;
}
/**
* 3D OpenSimplex2S/SuperSimplex noise, with better visual isotropy in (X, Y).
* Recommended for 3D terrain and time-varied animations.
* The Z coordinate should always be the "different" coordinate in whatever your use case is.
* If Y is vertical in world coordinates, call Noise3_ImproveXZ(x, z, Y) or use Noise3_XZBeforeY.
* If Z is vertical in world coordinates, call Noise3_ImproveXZ(x, y, Z).
* For a time varied animation, call Noise3_ImproveXY(x, y, T).
*/
public static float Noise3_ImproveXY( long seed, double x, double y, double z )
{
// Re-orient the cubic lattices without skewing, so Z points up the main lattice diagonal,
// and the planes formed by XY are moved far out of alignment with the cube faces.
// Orthonormal rotation. Not a skew transform.
double xy = x + y;
double s2 = xy * ROTATE3_ORTHOGONALIZER;
double zz = z * ROOT3OVER3;
double xr = x + s2 + zz;
double yr = y + s2 + zz;
double zr = xy * -ROOT3OVER3 + zz;
// Evaluate both lattices to form a BCC lattice.
return Noise3_UnrotatedBase( seed, xr, yr, zr );
}
/**
* 3D OpenSimplex2S/SuperSimplex noise, with better visual isotropy in (X, Z).
* Recommended for 3D terrain and time-varied animations.
* The Y coordinate should always be the "different" coordinate in whatever your use case is.
* If Y is vertical in world coordinates, call Noise3_ImproveXZ(x, Y, z).
* If Z is vertical in world coordinates, call Noise3_ImproveXZ(x, Z, y) or use Noise3_ImproveXY.
* For a time varied animation, call Noise3_ImproveXZ(x, T, y) or use Noise3_ImproveXY.
*/
public static float Noise3_ImproveXZ( long seed, double x, double y, double z )
{
// Re-orient the cubic lattices without skewing, so Y points up the main lattice diagonal,
// and the planes formed by XZ are moved far out of alignment with the cube faces.
// Orthonormal rotation. Not a skew transform.
double xz = x + z;
double s2 = xz * -0.211324865405187;
double yy = y * ROOT3OVER3;
double xr = x + s2 + yy;
double zr = z + s2 + yy;
double yr = xz * -ROOT3OVER3 + yy;
// Evaluate both lattices to form a BCC lattice.
return Noise3_UnrotatedBase( seed, xr, yr, zr );
}
/**
* 3D OpenSimplex2S/SuperSimplex noise, fallback rotation option
* Use Noise3_ImproveXY or Noise3_ImproveXZ instead, wherever appropriate.
* They have less diagonal bias. This function's best use is as a fallback.
*/
public static float Noise3_Fallback( long seed, double x, double y, double z )
{
// Re-orient the cubic lattices via rotation, to produce a familiar look.
// Orthonormal rotation. Not a skew transform.
double r = FALLBACK_ROTATE3 * (x + y + z);
double xr = r - x, yr = r - y, zr = r - z;
// Evaluate both lattices to form a BCC lattice.
return Noise3_UnrotatedBase( seed, xr, yr, zr );
}
/**
* Generate overlapping cubic lattices for 3D Re-oriented BCC noise.
* Lookup table implementation inspired by DigitalShadow.
* It was actually faster to narrow down the points in the loop itself,
* than to build up the index with enough info to isolate 8 points.
*/
private static float Noise3_UnrotatedBase( long seed, double xr, double yr, double zr )
{
// Get base points and offsets.
int xrb = FastFloor( xr ), yrb = FastFloor( yr ), zrb = FastFloor( zr );
float xi = (float)(xr - xrb), yi = (float)(yr - yrb), zi = (float)(zr - zrb);
// Prime pre-multiplication for hash. Also flip seed for second lattice copy.
long xrbp = xrb * PRIME_X, yrbp = yrb * PRIME_Y, zrbp = zrb * PRIME_Z;
long seed2 = seed ^ -0x52D547B2E96ED629L;
// -1 if positive, 0 if negative.
int xNMask = (int)(-0.5f - xi), yNMask = (int)(-0.5f - yi), zNMask = (int)(-0.5f - zi);
// First vertex.
float x0 = xi + xNMask;
float y0 = yi + yNMask;
float z0 = zi + zNMask;
float a0 = RSQUARED_3D - x0 * x0 - y0 * y0 - z0 * z0;
float value = (a0 * a0) * (a0 * a0) * Grad( seed,
xrbp + (xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x0, y0, z0 );
// Second vertex.
float x1 = xi - 0.5f;
float y1 = yi - 0.5f;
float z1 = zi - 0.5f;
float a1 = RSQUARED_3D - x1 * x1 - y1 * y1 - z1 * z1;
value += (a1 * a1) * (a1 * a1) * Grad( seed2,
xrbp + PRIME_X, yrbp + PRIME_Y, zrbp + PRIME_Z, x1, y1, z1 );
// Shortcuts for building the remaining falloffs.
// Derived by subtracting the polynomials with the offsets plugged in.
float xAFlipMask0 = ((xNMask | 1) << 1) * x1;
float yAFlipMask0 = ((yNMask | 1) << 1) * y1;
float zAFlipMask0 = ((zNMask | 1) << 1) * z1;
float xAFlipMask1 = (-2 - (xNMask << 2)) * x1 - 1.0f;
float yAFlipMask1 = (-2 - (yNMask << 2)) * y1 - 1.0f;
float zAFlipMask1 = (-2 - (zNMask << 2)) * z1 - 1.0f;
bool skip5 = false;
float a2 = xAFlipMask0 + a0;
if ( a2 > 0 )
{
float x2 = x0 - (xNMask | 1);
float y2 = y0;
float z2 = z0;
value += (a2 * a2) * (a2 * a2) * Grad( seed,
xrbp + (~xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x2, y2, z2 );
}
else
{
float a3 = yAFlipMask0 + zAFlipMask0 + a0;
if ( a3 > 0 )
{
float x3 = x0;
float y3 = y0 - (yNMask | 1);
float z3 = z0 - (zNMask | 1);
value += (a3 * a3) * (a3 * a3) * Grad( seed,
xrbp + (xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), x3, y3, z3 );
}
float a4 = xAFlipMask1 + a1;
if ( a4 > 0 )
{
float x4 = (xNMask | 1) + x1;
float y4 = y1;
float z4 = z1;
value += (a4 * a4) * (a4 * a4) * Grad( seed2,
xrbp + (xNMask & unchecked(PRIME_X * 2)), yrbp + PRIME_Y, zrbp + PRIME_Z, x4, y4, z4 );
skip5 = true;
}
}
bool skip9 = false;
float a6 = yAFlipMask0 + a0;
if ( a6 > 0 )
{
float x6 = x0;
float y6 = y0 - (yNMask | 1);
float z6 = z0;
value += (a6 * a6) * (a6 * a6) * Grad( seed,
xrbp + (xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x6, y6, z6 );
}
else
{
float a7 = xAFlipMask0 + zAFlipMask0 + a0;
if ( a7 > 0 )
{
float x7 = x0 - (xNMask | 1);
float y7 = y0;
float z7 = z0 - (zNMask | 1);
value += (a7 * a7) * (a7 * a7) * Grad( seed,
xrbp + (~xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), x7, y7, z7 );
}
float a8 = yAFlipMask1 + a1;
if ( a8 > 0 )
{
float x8 = x1;
float y8 = (yNMask | 1) + y1;
float z8 = z1;
value += (a8 * a8) * (a8 * a8) * Grad( seed2,
xrbp + PRIME_X, yrbp + (yNMask & (PRIME_Y << 1)), zrbp + PRIME_Z, x8, y8, z8 );
skip9 = true;
}
}
bool skipD = false;
float aA = zAFlipMask0 + a0;
if ( aA > 0 )
{
float xA = x0;
float yA = y0;
float zA = z0 - (zNMask | 1);
value += (aA * aA) * (aA * aA) * Grad( seed,
xrbp + (xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), xA, yA, zA );
}
else
{
float aB = xAFlipMask0 + yAFlipMask0 + a0;
if ( aB > 0 )
{
float xB = x0 - (xNMask | 1);
float yB = y0 - (yNMask | 1);
float zB = z0;
value += (aB * aB) * (aB * aB) * Grad( seed,
xrbp + (~xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), xB, yB, zB );
}
float aC = zAFlipMask1 + a1;
if ( aC > 0 )
{
float xC = x1;
float yC = y1;
float zC = (zNMask | 1) + z1;
value += (aC * aC) * (aC * aC) * Grad( seed2,
xrbp + PRIME_X, yrbp + PRIME_Y, zrbp + (zNMask & (PRIME_Z << 1)), xC, yC, zC );
skipD = true;
}
}
if ( !skip5 )
{
float a5 = yAFlipMask1 + zAFlipMask1 + a1;
if ( a5 > 0 )
{
float x5 = x1;
float y5 = (yNMask | 1) + y1;
float z5 = (zNMask | 1) + z1;
value += (a5 * a5) * (a5 * a5) * Grad( seed2,
xrbp + PRIME_X, yrbp + (yNMask & (PRIME_Y << 1)), zrbp + (zNMask & (PRIME_Z << 1)), x5, y5, z5 );
}
}
if ( !skip9 )
{
float a9 = xAFlipMask1 + zAFlipMask1 + a1;
if ( a9 > 0 )
{
float x9 = (xNMask | 1) + x1;
float y9 = y1;
float z9 = (zNMask | 1) + z1;
value += (a9 * a9) * (a9 * a9) * Grad( seed2,
xrbp + (xNMask & unchecked(PRIME_X * 2)), yrbp + PRIME_Y, zrbp + (zNMask & (PRIME_Z << 1)), x9, y9, z9 );
}
}
if ( !skipD )
{
float aD = xAFlipMask1 + yAFlipMask1 + a1;
if ( aD > 0 )
{
float xD = (xNMask | 1) + x1;
float yD = (yNMask | 1) + y1;
float zD = z1;
value += (aD * aD) * (aD * aD) * Grad( seed2,
xrbp + (xNMask & (PRIME_X << 1)), yrbp + (yNMask & (PRIME_Y << 1)), zrbp + PRIME_Z, xD, yD, zD );
}
}
return value;
}
/**
* 4D SuperSimplex noise, with XYZ oriented like Noise3_ImproveXY
* and W for an extra degree of freedom. W repeats eventually.
* Recommended for time-varied animations which texture a 3D object (W=time)
* in a space where Z is vertical
*/
public static float Noise4_ImproveXYZ_ImproveXY( long seed, double x, double y, double z, double w )
{
double xy = x + y;
double s2 = xy * -0.21132486540518699998;
double zz = z * 0.28867513459481294226;
double ww = w * 1.118033988749894;
double xr = x + (zz + ww + s2), yr = y + (zz + ww + s2);
double zr = xy * -0.57735026918962599998 + (zz + ww);
double wr = z * -0.866025403784439 + ww;
return Noise4_UnskewedBase( seed, xr, yr, zr, wr );
}
/**
* 4D SuperSimplex noise, with XYZ oriented like Noise3_ImproveXZ
* and W for an extra degree of freedom. W repeats eventually.
* Recommended for time-varied animations which texture a 3D object (W=time)
* in a space where Y is vertical
*/
public static float Noise4_ImproveXYZ_ImproveXZ( long seed, double x, double y, double z, double w )
{
double xz = x + z;
double s2 = xz * -0.21132486540518699998;
double yy = y * 0.28867513459481294226;
double ww = w * 1.118033988749894;
double xr = x + (yy + ww + s2), zr = z + (yy + ww + s2);
double yr = xz * -0.57735026918962599998 + (yy + ww);
double wr = y * -0.866025403784439 + ww;
return Noise4_UnskewedBase( seed, xr, yr, zr, wr );
}
/**
* 4D SuperSimplex noise, with XYZ oriented like Noise3_Fallback
* and W for an extra degree of freedom. W repeats eventually.
* Recommended for time-varied animations which texture a 3D object (W=time)
* where there isn't a clear distinction between horizontal and vertical
*/
public static float Noise4_ImproveXYZ( long seed, double x, double y, double z, double w )
{
double xyz = x + y + z;
double ww = w * 1.118033988749894;
double s2 = xyz * -0.16666666666666666 + ww;
double xs = x + s2, ys = y + s2, zs = z + s2, ws = -0.5 * xyz + ww;
return Noise4_UnskewedBase( seed, xs, ys, zs, ws );
}
/**
* 4D SuperSimplex noise, fallback lattice orientation.
*/
public static float Noise4_Fallback( long seed, double x, double y, double z, double w )
{
// Get points for A4 lattice
double s = SKEW_4D * (x + y + z + w);
double xs = x + s, ys = y + s, zs = z + s, ws = w + s;
return Noise4_UnskewedBase( seed, xs, ys, zs, ws );
}
/**
* 4D SuperSimplex noise base.
* Using ultra-simple 4x4x4x4 lookup partitioning.
* This isn't as elegant or SIMD/GPU/etc. portable as other approaches,
* but it competes performance-wise with optimized 2014 OpenSimplex.
*/
private static float Noise4_UnskewedBase( long seed, double xs, double ys, double zs, double ws )
{
// Get base points and offsets
int xsb = FastFloor( xs ), ysb = FastFloor( ys ), zsb = FastFloor( zs ), wsb = FastFloor( ws );
float xsi = (float)(xs - xsb), ysi = (float)(ys - ysb), zsi = (float)(zs - zsb), wsi = (float)(ws - wsb);
// Unskewed offsets
float ssi = (xsi + ysi + zsi + wsi) * UNSKEW_4D;
float xi = xsi + ssi, yi = ysi + ssi, zi = zsi + ssi, wi = wsi + ssi;
// Prime pre-multiplication for hash.
long xsvp = xsb * PRIME_X, ysvp = ysb * PRIME_Y, zsvp = zsb * PRIME_Z, wsvp = wsb * PRIME_W;
// Index into initial table.
int index = ((FastFloor( xs * 4 ) & 3) << 0)
| ((FastFloor( ys * 4 ) & 3) << 2)
| ((FastFloor( zs * 4 ) & 3) << 4)
| ((FastFloor( ws * 4 ) & 3) << 6);
// Point contributions
float value = 0;
(int secondaryIndexStart, int secondaryIndexStop) = LOOKUP_4D_A[index];
for ( int i = secondaryIndexStart; i < secondaryIndexStop; i++ )
{
LatticeVertex4D c = LOOKUP_4D_B[i];
float dx = xi + c.dx, dy = yi + c.dy, dz = zi + c.dz, dw = wi + c.dw;
float a = (dx * dx + dy * dy) + (dz * dz + dw * dw);
if ( a < RSQUARED_4D )
{
a -= RSQUARED_4D;
a *= a;
value += a * a * Grad( seed, xsvp + c.xsvp, ysvp + c.ysvp, zsvp + c.zsvp, wsvp + c.wsvp, dx, dy, dz, dw );
}
}
return value;
}
/*
* Utility
*/
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static float Grad( long seed, long xsvp, long ysvp, float dx, float dy )
{
long hash = seed ^ xsvp ^ ysvp;
hash *= HASH_MULTIPLIER;
hash ^= hash >> (64 - N_GRADS_2D_EXPONENT + 1);
int gi = (int)hash & ((N_GRADS_2D - 1) << 1);
return GRADIENTS_2D[gi | 0] * dx + GRADIENTS_2D[gi | 1] * dy;
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static float Grad( long seed, long xrvp, long yrvp, long zrvp, float dx, float dy, float dz )
{
long hash = (seed ^ xrvp) ^ (yrvp ^ zrvp);
hash *= HASH_MULTIPLIER;
hash ^= hash >> (64 - N_GRADS_3D_EXPONENT + 2);
int gi = (int)hash & ((N_GRADS_3D - 1) << 2);
return GRADIENTS_3D[gi | 0] * dx + GRADIENTS_3D[gi | 1] * dy + GRADIENTS_3D[gi | 2] * dz;
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static float Grad( long seed, long xsvp, long ysvp, long zsvp, long wsvp, float dx, float dy, float dz, float dw )
{
long hash = seed ^ (xsvp ^ ysvp) ^ (zsvp ^ wsvp);
hash *= HASH_MULTIPLIER;
hash ^= hash >> (64 - N_GRADS_4D_EXPONENT + 2);
int gi = (int)hash & ((N_GRADS_4D - 1) << 2);
return (GRADIENTS_4D[gi | 0] * dx + GRADIENTS_4D[gi | 1] * dy) + (GRADIENTS_4D[gi | 2] * dz + GRADIENTS_4D[gi | 3] * dw);
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static int FastFloor( double x )
{
int xi = (int)x;
return x < xi ? xi - 1 : xi;
}
/*
* Lookup Tables & Gradients
*/
private static readonly float[] GRADIENTS_2D;
private static readonly float[] GRADIENTS_3D;
private static readonly float[] GRADIENTS_4D;
private static readonly (short SecondaryIndexStart, short SecondaryIndexStop)[] LOOKUP_4D_A;
private static readonly LatticeVertex4D[] LOOKUP_4D_B;
static OpenSimplex2S()
{
GRADIENTS_2D = new float[N_GRADS_2D * 2];
float[] grad2 = {
0.38268343236509f, 0.923879532511287f,
0.923879532511287f, 0.38268343236509f,
0.923879532511287f, -0.38268343236509f,
0.38268343236509f, -0.923879532511287f,
-0.38268343236509f, -0.923879532511287f,
-0.923879532511287f, -0.38268343236509f,
-0.923879532511287f, 0.38268343236509f,
-0.38268343236509f, 0.923879532511287f,
//-------------------------------------//
0.130526192220052f, 0.99144486137381f,
0.608761429008721f, 0.793353340291235f,
0.793353340291235f, 0.608761429008721f,
0.99144486137381f, 0.130526192220051f,
0.99144486137381f, -0.130526192220051f,
0.793353340291235f, -0.60876142900872f,
0.608761429008721f, -0.793353340291235f,
0.130526192220052f, -0.99144486137381f,
-0.130526192220052f, -0.99144486137381f,
-0.608761429008721f, -0.793353340291235f,
-0.793353340291235f, -0.608761429008721f,
-0.99144486137381f, -0.130526192220052f,
-0.99144486137381f, 0.130526192220051f,
-0.793353340291235f, 0.608761429008721f,
-0.608761429008721f, 0.793353340291235f,
-0.130526192220052f, 0.99144486137381f,
};
for ( int i = 0; i < grad2.Length; i++ )
{
grad2[i] = (float)(grad2[i] / NORMALIZER_2D);
}
for ( int i = 0, j = 0; i < GRADIENTS_2D.Length; i++, j++ )
{
if ( j == grad2.Length ) j = 0;
GRADIENTS_2D[i] = grad2[j];
}
GRADIENTS_3D = new float[N_GRADS_3D * 4];
float[] grad3 = {
2.22474487139f, 2.22474487139f, -1.0f, 0.0f,
2.22474487139f, 2.22474487139f, 1.0f, 0.0f,
3.0862664687972017f, 1.1721513422464978f, 0.0f, 0.0f,
1.1721513422464978f, 3.0862664687972017f, 0.0f, 0.0f,
-2.22474487139f, 2.22474487139f, -1.0f, 0.0f,
-2.22474487139f, 2.22474487139f, 1.0f, 0.0f,
-1.1721513422464978f, 3.0862664687972017f, 0.0f, 0.0f,
-3.0862664687972017f, 1.1721513422464978f, 0.0f, 0.0f,
-1.0f, -2.22474487139f, -2.22474487139f, 0.0f,
1.0f, -2.22474487139f, -2.22474487139f, 0.0f,
0.0f, -3.0862664687972017f, -1.1721513422464978f, 0.0f,
0.0f, -1.1721513422464978f, -3.0862664687972017f, 0.0f,
-1.0f, -2.22474487139f, 2.22474487139f, 0.0f,
1.0f, -2.22474487139f, 2.22474487139f, 0.0f,
0.0f, -1.1721513422464978f, 3.0862664687972017f, 0.0f,
0.0f, -3.0862664687972017f, 1.1721513422464978f, 0.0f,
//--------------------------------------------------------------------//
-2.22474487139f, -2.22474487139f, -1.0f, 0.0f,
-2.22474487139f, -2.22474487139f, 1.0f, 0.0f,
-3.0862664687972017f, -1.1721513422464978f, 0.0f, 0.0f,
-1.1721513422464978f, -3.0862664687972017f, 0.0f, 0.0f,
-2.22474487139f, -1.0f, -2.22474487139f, 0.0f,
-2.22474487139f, 1.0f, -2.22474487139f, 0.0f,
-1.1721513422464978f, 0.0f, -3.0862664687972017f, 0.0f,
-3.0862664687972017f, 0.0f, -1.1721513422464978f, 0.0f,
-2.22474487139f, -1.0f, 2.22474487139f, 0.0f,
-2.22474487139f, 1.0f, 2.22474487139f, 0.0f,
-3.0862664687972017f, 0.0f, 1.1721513422464978f, 0.0f,
-1.1721513422464978f, 0.0f, 3.0862664687972017f, 0.0f,
-1.0f, 2.22474487139f, -2.22474487139f, 0.0f,
1.0f, 2.22474487139f, -2.22474487139f, 0.0f,
0.0f, 1.1721513422464978f, -3.0862664687972017f, 0.0f,
0.0f, 3.0862664687972017f, -1.1721513422464978f, 0.0f,
-1.0f, 2.22474487139f, 2.22474487139f, 0.0f,
1.0f, 2.22474487139f, 2.22474487139f, 0.0f,
0.0f, 3.0862664687972017f, 1.1721513422464978f, 0.0f,
0.0f, 1.1721513422464978f, 3.0862664687972017f, 0.0f,
2.22474487139f, -2.22474487139f, -1.0f, 0.0f,
2.22474487139f, -2.22474487139f, 1.0f, 0.0f,
1.1721513422464978f, -3.0862664687972017f, 0.0f, 0.0f,
3.0862664687972017f, -1.1721513422464978f, 0.0f, 0.0f,
2.22474487139f, -1.0f, -2.22474487139f, 0.0f,
2.22474487139f, 1.0f, -2.22474487139f, 0.0f,
3.0862664687972017f, 0.0f, -1.1721513422464978f, 0.0f,
1.1721513422464978f, 0.0f, -3.0862664687972017f, 0.0f,
2.22474487139f, -1.0f, 2.22474487139f, 0.0f,
2.22474487139f, 1.0f, 2.22474487139f, 0.0f,
1.1721513422464978f, 0.0f, 3.0862664687972017f, 0.0f,
3.0862664687972017f, 0.0f, 1.1721513422464978f, 0.0f,
};
for ( int i = 0; i < grad3.Length; i++ )
{
grad3[i] = (float)(grad3[i] / NORMALIZER_3D);
}
for ( int i = 0, j = 0; i < GRADIENTS_3D.Length; i++, j++ )
{
if ( j == grad3.Length ) j = 0;
GRADIENTS_3D[i] = grad3[j];
}
GRADIENTS_4D = new float[N_GRADS_4D * 4];
float[] grad4 = {
-0.6740059517812944f, -0.3239847771997537f, -0.3239847771997537f, 0.5794684678643381f,
-0.7504883828755602f, -0.4004672082940195f, 0.15296486218853164f, 0.5029860367700724f,
-0.7504883828755602f, 0.15296486218853164f, -0.4004672082940195f, 0.5029860367700724f,
-0.8828161875373585f, 0.08164729285680945f, 0.08164729285680945f, 0.4553054119602712f,
-0.4553054119602712f, -0.08164729285680945f, -0.08164729285680945f, 0.8828161875373585f,
-0.5029860367700724f, -0.15296486218853164f, 0.4004672082940195f, 0.7504883828755602f,
-0.5029860367700724f, 0.4004672082940195f, -0.15296486218853164f, 0.7504883828755602f,
-0.5794684678643381f, 0.3239847771997537f, 0.3239847771997537f, 0.6740059517812944f,
-0.6740059517812944f, -0.3239847771997537f, 0.5794684678643381f, -0.3239847771997537f,
-0.7504883828755602f, -0.4004672082940195f, 0.5029860367700724f, 0.15296486218853164f,
-0.7504883828755602f, 0.15296486218853164f, 0.5029860367700724f, -0.4004672082940195f,
-0.8828161875373585f, 0.08164729285680945f, 0.4553054119602712f, 0.08164729285680945f,
-0.4553054119602712f, -0.08164729285680945f, 0.8828161875373585f, -0.08164729285680945f,
-0.5029860367700724f, -0.15296486218853164f, 0.7504883828755602f, 0.4004672082940195f,
-0.5029860367700724f, 0.4004672082940195f, 0.7504883828755602f, -0.15296486218853164f,
-0.5794684678643381f, 0.3239847771997537f, 0.6740059517812944f, 0.3239847771997537f,
-0.6740059517812944f, 0.5794684678643381f, -0.3239847771997537f, -0.3239847771997537f,
-0.7504883828755602f, 0.5029860367700724f, -0.4004672082940195f, 0.15296486218853164f,
-0.7504883828755602f, 0.5029860367700724f, 0.15296486218853164f, -0.4004672082940195f,
-0.8828161875373585f, 0.4553054119602712f, 0.08164729285680945f, 0.08164729285680945f,
-0.4553054119602712f, 0.8828161875373585f, -0.08164729285680945f, -0.08164729285680945f,
-0.5029860367700724f, 0.7504883828755602f, -0.15296486218853164f, 0.4004672082940195f,
-0.5029860367700724f, 0.7504883828755602f, 0.4004672082940195f, -0.15296486218853164f,
-0.5794684678643381f, 0.6740059517812944f, 0.3239847771997537f, 0.3239847771997537f,
0.5794684678643381f, -0.6740059517812944f, -0.3239847771997537f, -0.3239847771997537f,
0.5029860367700724f, -0.7504883828755602f, -0.4004672082940195f, 0.15296486218853164f,
0.5029860367700724f, -0.7504883828755602f, 0.15296486218853164f, -0.4004672082940195f,
0.4553054119602712f, -0.8828161875373585f, 0.08164729285680945f, 0.08164729285680945f,
0.8828161875373585f, -0.4553054119602712f, -0.08164729285680945f, -0.08164729285680945f,
0.7504883828755602f, -0.5029860367700724f, -0.15296486218853164f, 0.4004672082940195f,
0.7504883828755602f, -0.5029860367700724f, 0.4004672082940195f, -0.15296486218853164f,
0.6740059517812944f, -0.5794684678643381f, 0.3239847771997537f, 0.3239847771997537f,
//------------------------------------------------------------------------------------------//
-0.753341017856078f, -0.37968289875261624f, -0.37968289875261624f, -0.37968289875261624f,
-0.7821684431180708f, -0.4321472685365301f, -0.4321472685365301f, 0.12128480194602098f,
-0.7821684431180708f, -0.4321472685365301f, 0.12128480194602098f, -0.4321472685365301f,
-0.7821684431180708f, 0.12128480194602098f, -0.4321472685365301f, -0.4321472685365301f,
-0.8586508742123365f, -0.508629699630796f, 0.044802370851755174f, 0.044802370851755174f,
-0.8586508742123365f, 0.044802370851755174f, -0.508629699630796f, 0.044802370851755174f,
-0.8586508742123365f, 0.044802370851755174f, 0.044802370851755174f, -0.508629699630796f,
-0.9982828964265062f, -0.03381941603233842f, -0.03381941603233842f, -0.03381941603233842f,
-0.37968289875261624f, -0.753341017856078f, -0.37968289875261624f, -0.37968289875261624f,
-0.4321472685365301f, -0.7821684431180708f, -0.4321472685365301f, 0.12128480194602098f,
-0.4321472685365301f, -0.7821684431180708f, 0.12128480194602098f, -0.4321472685365301f,
0.12128480194602098f, -0.7821684431180708f, -0.4321472685365301f, -0.4321472685365301f,
-0.508629699630796f, -0.8586508742123365f, 0.044802370851755174f, 0.044802370851755174f,
0.044802370851755174f, -0.8586508742123365f, -0.508629699630796f, 0.044802370851755174f,
0.044802370851755174f, -0.8586508742123365f, 0.044802370851755174f, -0.508629699630796f,
-0.03381941603233842f, -0.9982828964265062f, -0.03381941603233842f, -0.03381941603233842f,
-0.37968289875261624f, -0.37968289875261624f, -0.753341017856078f, -0.37968289875261624f,
-0.4321472685365301f, -0.4321472685365301f, -0.7821684431180708f, 0.12128480194602098f,
-0.4321472685365301f, 0.12128480194602098f, -0.7821684431180708f, -0.4321472685365301f,
0.12128480194602098f, -0.4321472685365301f, -0.7821684431180708f, -0.4321472685365301f,
-0.508629699630796f, 0.044802370851755174f, -0.8586508742123365f, 0.044802370851755174f,
0.044802370851755174f, -0.508629699630796f, -0.8586508742123365f, 0.044802370851755174f,
0.044802370851755174f, 0.044802370851755174f, -0.8586508742123365f, -0.508629699630796f,
-0.03381941603233842f, -0.03381941603233842f, -0.9982828964265062f, -0.03381941603233842f,
-0.37968289875261624f, -0.37968289875261624f, -0.37968289875261624f, -0.753341017856078f,
-0.4321472685365301f, -0.4321472685365301f, 0.12128480194602098f, -0.7821684431180708f,
-0.4321472685365301f, 0.12128480194602098f, -0.4321472685365301f, -0.7821684431180708f,
0.12128480194602098f, -0.4321472685365301f, -0.4321472685365301f, -0.7821684431180708f,
-0.508629699630796f, 0.044802370851755174f, 0.044802370851755174f, -0.8586508742123365f,
0.044802370851755174f, -0.508629699630796f, 0.044802370851755174f, -0.8586508742123365f,
0.044802370851755174f, 0.044802370851755174f, -0.508629699630796f, -0.8586508742123365f,
-0.03381941603233842f, -0.03381941603233842f, -0.03381941603233842f, -0.9982828964265062f,
-0.3239847771997537f, -0.6740059517812944f, -0.3239847771997537f, 0.5794684678643381f,
-0.4004672082940195f, -0.7504883828755602f, 0.15296486218853164f, 0.5029860367700724f,
0.15296486218853164f, -0.7504883828755602f, -0.4004672082940195f, 0.5029860367700724f,
0.08164729285680945f, -0.8828161875373585f, 0.08164729285680945f, 0.4553054119602712f,
-0.08164729285680945f, -0.4553054119602712f, -0.08164729285680945f, 0.8828161875373585f,
-0.15296486218853164f, -0.5029860367700724f, 0.4004672082940195f, 0.7504883828755602f,
0.4004672082940195f, -0.5029860367700724f, -0.15296486218853164f, 0.7504883828755602f,
0.3239847771997537f, -0.5794684678643381f, 0.3239847771997537f, 0.6740059517812944f,
-0.3239847771997537f, -0.3239847771997537f, -0.6740059517812944f, 0.5794684678643381f,
-0.4004672082940195f, 0.15296486218853164f, -0.7504883828755602f, 0.5029860367700724f,
0.15296486218853164f, -0.4004672082940195f, -0.7504883828755602f, 0.5029860367700724f,
0.08164729285680945f, 0.08164729285680945f, -0.8828161875373585f, 0.4553054119602712f,
-0.08164729285680945f, -0.08164729285680945f, -0.4553054119602712f, 0.8828161875373585f,
-0.15296486218853164f, 0.4004672082940195f, -0.5029860367700724f, 0.7504883828755602f,
0.4004672082940195f, -0.15296486218853164f, -0.5029860367700724f, 0.7504883828755602f,
0.3239847771997537f, 0.3239847771997537f, -0.5794684678643381f, 0.6740059517812944f,
-0.3239847771997537f, -0.6740059517812944f, 0.5794684678643381f, -0.3239847771997537f,
-0.4004672082940195f, -0.7504883828755602f, 0.5029860367700724f, 0.15296486218853164f,
0.15296486218853164f, -0.7504883828755602f, 0.5029860367700724f, -0.4004672082940195f,
0.08164729285680945f, -0.8828161875373585f, 0.4553054119602712f, 0.08164729285680945f,
-0.08164729285680945f, -0.4553054119602712f, 0.8828161875373585f, -0.08164729285680945f,
-0.15296486218853164f, -0.5029860367700724f, 0.7504883828755602f, 0.4004672082940195f,
0.4004672082940195f, -0.5029860367700724f, 0.7504883828755602f, -0.15296486218853164f,
0.3239847771997537f, -0.5794684678643381f, 0.6740059517812944f, 0.3239847771997537f,
-0.3239847771997537f, -0.3239847771997537f, 0.5794684678643381f, -0.6740059517812944f,
-0.4004672082940195f, 0.15296486218853164f, 0.5029860367700724f, -0.7504883828755602f,
0.15296486218853164f, -0.4004672082940195f, 0.5029860367700724f, -0.7504883828755602f,
0.08164729285680945f, 0.08164729285680945f, 0.4553054119602712f, -0.8828161875373585f,
-0.08164729285680945f, -0.08164729285680945f, 0.8828161875373585f, -0.4553054119602712f,
-0.15296486218853164f, 0.4004672082940195f, 0.7504883828755602f, -0.5029860367700724f,
0.4004672082940195f, -0.15296486218853164f, 0.7504883828755602f, -0.5029860367700724f,
0.3239847771997537f, 0.3239847771997537f, 0.6740059517812944f, -0.5794684678643381f,
-0.3239847771997537f, 0.5794684678643381f, -0.6740059517812944f, -0.3239847771997537f,
-0.4004672082940195f, 0.5029860367700724f, -0.7504883828755602f, 0.15296486218853164f,
0.15296486218853164f, 0.5029860367700724f, -0.7504883828755602f, -0.4004672082940195f,
0.08164729285680945f, 0.4553054119602712f, -0.8828161875373585f, 0.08164729285680945f,
-0.08164729285680945f, 0.8828161875373585f, -0.4553054119602712f, -0.08164729285680945f,
-0.15296486218853164f, 0.7504883828755602f, -0.5029860367700724f, 0.4004672082940195f,
0.4004672082940195f, 0.7504883828755602f, -0.5029860367700724f, -0.15296486218853164f,
0.3239847771997537f, 0.6740059517812944f, -0.5794684678643381f, 0.3239847771997537f,
-0.3239847771997537f, 0.5794684678643381f, -0.3239847771997537f, -0.6740059517812944f,
-0.4004672082940195f, 0.5029860367700724f, 0.15296486218853164f, -0.7504883828755602f,
0.15296486218853164f, 0.5029860367700724f, -0.4004672082940195f, -0.7504883828755602f,
0.08164729285680945f, 0.4553054119602712f, 0.08164729285680945f, -0.8828161875373585f,
-0.08164729285680945f, 0.8828161875373585f, -0.08164729285680945f, -0.4553054119602712f,
-0.15296486218853164f, 0.7504883828755602f, 0.4004672082940195f, -0.5029860367700724f,
0.4004672082940195f, 0.7504883828755602f, -0.15296486218853164f, -0.5029860367700724f,
0.3239847771997537f, 0.6740059517812944f, 0.3239847771997537f, -0.5794684678643381f,
0.5794684678643381f, -0.3239847771997537f, -0.6740059517812944f, -0.3239847771997537f,
0.5029860367700724f, -0.4004672082940195f, -0.7504883828755602f, 0.15296486218853164f,
0.5029860367700724f, 0.15296486218853164f, -0.7504883828755602f, -0.4004672082940195f,
0.4553054119602712f, 0.08164729285680945f, -0.8828161875373585f, 0.08164729285680945f,
0.8828161875373585f, -0.08164729285680945f, -0.4553054119602712f, -0.08164729285680945f,
0.7504883828755602f, -0.15296486218853164f, -0.5029860367700724f, 0.4004672082940195f,
0.7504883828755602f, 0.4004672082940195f, -0.5029860367700724f, -0.15296486218853164f,
0.6740059517812944f, 0.3239847771997537f, -0.5794684678643381f, 0.3239847771997537f,
0.5794684678643381f, -0.3239847771997537f, -0.3239847771997537f, -0.6740059517812944f,
0.5029860367700724f, -0.4004672082940195f, 0.15296486218853164f, -0.7504883828755602f,
0.5029860367700724f, 0.15296486218853164f, -0.4004672082940195f, -0.7504883828755602f,
0.4553054119602712f, 0.08164729285680945f, 0.08164729285680945f, -0.8828161875373585f,
0.8828161875373585f, -0.08164729285680945f, -0.08164729285680945f, -0.4553054119602712f,
0.7504883828755602f, -0.15296486218853164f, 0.4004672082940195f, -0.5029860367700724f,
0.7504883828755602f, 0.4004672082940195f, -0.15296486218853164f, -0.5029860367700724f,
0.6740059517812944f, 0.3239847771997537f, 0.3239847771997537f, -0.5794684678643381f,
0.03381941603233842f, 0.03381941603233842f, 0.03381941603233842f, 0.9982828964265062f,
-0.044802370851755174f, -0.044802370851755174f, 0.508629699630796f, 0.8586508742123365f,
-0.044802370851755174f, 0.508629699630796f, -0.044802370851755174f, 0.8586508742123365f,
-0.12128480194602098f, 0.4321472685365301f, 0.4321472685365301f, 0.7821684431180708f,
0.508629699630796f, -0.044802370851755174f, -0.044802370851755174f, 0.8586508742123365f,
0.4321472685365301f, -0.12128480194602098f, 0.4321472685365301f, 0.7821684431180708f,
0.4321472685365301f, 0.4321472685365301f, -0.12128480194602098f, 0.7821684431180708f,
0.37968289875261624f, 0.37968289875261624f, 0.37968289875261624f, 0.753341017856078f,
0.03381941603233842f, 0.03381941603233842f, 0.9982828964265062f, 0.03381941603233842f,
-0.044802370851755174f, 0.044802370851755174f, 0.8586508742123365f, 0.508629699630796f,
-0.044802370851755174f, 0.508629699630796f, 0.8586508742123365f, -0.044802370851755174f,
-0.12128480194602098f, 0.4321472685365301f, 0.7821684431180708f, 0.4321472685365301f,
0.508629699630796f, -0.044802370851755174f, 0.8586508742123365f, -0.044802370851755174f,
0.4321472685365301f, -0.12128480194602098f, 0.7821684431180708f, 0.4321472685365301f,
0.4321472685365301f, 0.4321472685365301f, 0.7821684431180708f, -0.12128480194602098f,
0.37968289875261624f, 0.37968289875261624f, 0.753341017856078f, 0.37968289875261624f,
0.03381941603233842f, 0.9982828964265062f, 0.03381941603233842f, 0.03381941603233842f,
-0.044802370851755174f, 0.8586508742123365f, -0.044802370851755174f, 0.508629699630796f,
-0.044802370851755174f, 0.8586508742123365f, 0.508629699630796f, -0.044802370851755174f,
-0.12128480194602098f, 0.7821684431180708f, 0.4321472685365301f, 0.4321472685365301f,
0.508629699630796f, 0.8586508742123365f, -0.044802370851755174f, -0.044802370851755174f,
0.4321472685365301f, 0.7821684431180708f, -0.12128480194602098f, 0.4321472685365301f,
0.4321472685365301f, 0.7821684431180708f, 0.4321472685365301f, -0.12128480194602098f,
0.37968289875261624f, 0.753341017856078f, 0.37968289875261624f, 0.37968289875261624f,
0.9982828964265062f, 0.03381941603233842f, 0.03381941603233842f, 0.03381941603233842f,
0.8586508742123365f, -0.044802370851755174f, -0.044802370851755174f, 0.508629699630796f,
0.8586508742123365f, -0.044802370851755174f, 0.508629699630796f, -0.044802370851755174f,
0.7821684431180708f, -0.12128480194602098f, 0.4321472685365301f, 0.4321472685365301f,
0.8586508742123365f, 0.508629699630796f, -0.044802370851755174f, -0.044802370851755174f,
0.7821684431180708f, 0.4321472685365301f, -0.12128480194602098f, 0.4321472685365301f,
0.7821684431180708f, 0.4321472685365301f, 0.4321472685365301f, -0.12128480194602098f,
0.753341017856078f, 0.37968289875261624f, 0.37968289875261624f, 0.37968289875261624f,
};
for ( int i = 0; i < grad4.Length; i++ )
{
grad4[i] = (float)(grad4[i] / NORMALIZER_4D);
}
for ( int i = 0, j = 0; i < GRADIENTS_4D.Length; i++, j++ )
{
if ( j == grad4.Length ) j = 0;
GRADIENTS_4D[i] = grad4[j];
}
int[][] lookup4DVertexCodes = {
new int[] { 0x15, 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x15, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA },
new int[] { 0x01, 0x05, 0x11, 0x15, 0x41, 0x45, 0x51, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA },
new int[] { 0x01, 0x15, 0x16, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x15, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xAA },
new int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xAA },
new int[] { 0x05, 0x15, 0x16, 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xAA, 0xAB },
new int[] { 0x04, 0x05, 0x14, 0x15, 0x44, 0x45, 0x54, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xAA },
new int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0xAA },
new int[] { 0x05, 0x15, 0x16, 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x5B, 0x6A, 0x9A, 0xAA, 0xAB },
new int[] { 0x04, 0x15, 0x19, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x05, 0x15, 0x19, 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xAA, 0xAE },
new int[] { 0x05, 0x15, 0x19, 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x5E, 0x6A, 0x9A, 0xAA, 0xAE },
new int[] { 0x05, 0x15, 0x1A, 0x45, 0x4A, 0x55, 0x56, 0x59, 0x5A, 0x5B, 0x5E, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x15, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xAA },
new int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xAA },
new int[] { 0x11, 0x15, 0x16, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x9A, 0xA6, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x15, 0x16, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x6B, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xAA },
new int[] { 0x15, 0x16, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x6B, 0x9A, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x19, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x19, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x6E, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x19, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x6E, 0x9A, 0xAA, 0xAE },
new int[] { 0x15, 0x1A, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x6B, 0x6E, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x10, 0x11, 0x14, 0x15, 0x50, 0x51, 0x54, 0x55, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xAA },
new int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0xA6, 0xAA },
new int[] { 0x11, 0x15, 0x16, 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x67, 0x6A, 0xA6, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA6, 0xAA },
new int[] { 0x15, 0x16, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x6B, 0xA6, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xAA },
new int[] { 0x15, 0x16, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x19, 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x6A, 0x6D, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x19, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x6E, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x19, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6E, 0xAA, 0xAE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x6B, 0x6E, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x10, 0x15, 0x25, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x11, 0x15, 0x25, 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xAA, 0xBA },
new int[] { 0x11, 0x15, 0x25, 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x6A, 0x76, 0xA6, 0xAA, 0xBA },
new int[] { 0x11, 0x15, 0x26, 0x51, 0x55, 0x56, 0x62, 0x65, 0x66, 0x67, 0x6A, 0x76, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x14, 0x15, 0x25, 0x54, 0x55, 0x59, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x25, 0x55, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x25, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA6, 0xAA, 0xBA },
new int[] { 0x15, 0x26, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x6B, 0x7A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x14, 0x15, 0x25, 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x6A, 0x79, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x25, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x25, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xAA, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0x7A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x14, 0x15, 0x29, 0x54, 0x55, 0x59, 0x65, 0x68, 0x69, 0x6A, 0x6D, 0x79, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x15, 0x29, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x6E, 0x7A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x15, 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6E, 0x7A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0x6E, 0x7A, 0xAA, 0xAB, 0xAE, 0xBA, 0xBF },
new int[] { 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x5A, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xAA },
new int[] { 0x41, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x45, 0x46, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x5A, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xAA },
new int[] { 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x9A, 0x9B, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x49, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x99, 0x9A, 0x9E, 0xAA, 0xAE },
new int[] { 0x45, 0x4A, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x59, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB },
new int[] { 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x56, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x15, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE },
new int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x66, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xAA },
new int[] { 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xA7, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA },
new int[] { 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA9, 0xAA },
new int[] { 0x15, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xA6, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA, 0xAD, 0xAE },
new int[] { 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x66, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x51, 0x55, 0x61, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x6A, 0xA5, 0xA6, 0xAA, 0xB6, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x62, 0x65, 0x66, 0x6A, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA },
new int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x55, 0x56, 0x65, 0x66, 0x6A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x6A, 0xA5, 0xA9, 0xAA, 0xB9, 0xBA },
new int[] { 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x68, 0x69, 0x6A, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xBE },
new int[] { 0x55, 0x59, 0x65, 0x69, 0x6A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x15, 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xAA, 0xAB, 0xAE, 0xBA, 0xBF },
new int[] { 0x40, 0x41, 0x44, 0x45, 0x50, 0x51, 0x54, 0x55, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x95, 0x96, 0x9A, 0xA6, 0xAA },
new int[] { 0x41, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x95, 0x96, 0x97, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA },
new int[] { 0x45, 0x46, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x95, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xAA },
new int[] { 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x95, 0x99, 0x9A, 0x9D, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x49, 0x55, 0x59, 0x5A, 0x95, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9E, 0xAA, 0xAE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x99, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA },
new int[] { 0x51, 0x52, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xEA },
new int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },
new int[] { 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x58, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE },
new int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x99, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA },
new int[] { 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE },
new int[] { 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB5, 0xBA },
new int[] { 0x51, 0x55, 0x61, 0x65, 0x66, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xB6, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x64, 0x65, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA },
new int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA5, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xB9, 0xBA },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xBE },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA5, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xBA },
new int[] { 0x40, 0x45, 0x51, 0x54, 0x55, 0x85, 0x91, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x85, 0x91, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA, 0xEA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x85, 0x91, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xD6, 0xEA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x86, 0x92, 0x95, 0x96, 0x97, 0x9A, 0xA6, 0xAA, 0xAB, 0xD6, 0xEA, 0xEB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x85, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x55, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xDA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xDA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x86, 0x95, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB, 0xDA, 0xEA, 0xEB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x85, 0x94, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xD9, 0xEA },
new int[] { 0x45, 0x55, 0x59, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xDA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xAA, 0xDA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB, 0xDA, 0xEA, 0xEB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x89, 0x95, 0x98, 0x99, 0x9A, 0x9D, 0xA9, 0xAA, 0xAE, 0xD9, 0xEA, 0xEE },
new int[] { 0x45, 0x55, 0x59, 0x89, 0x95, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE, 0xDA, 0xEA, 0xEE },
new int[] { 0x45, 0x55, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE, 0xDA, 0xEA, 0xEE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xDA, 0xEA, 0xEF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x91, 0x94, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x51, 0x55, 0x91, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xE6, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x91, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xE6, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x92, 0x95, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB, 0xE6, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xE9, 0xEA },
new int[] { 0x55, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x55, 0x56, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x55, 0x56, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x59, 0x94, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xE9, 0xEA },
new int[] { 0x55, 0x59, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x59, 0x95, 0x98, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE, 0xE9, 0xEA, 0xEE },
new int[] { 0x55, 0x59, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },
new int[] { 0x45, 0x55, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xAA, 0xAB, 0xAE, 0xEA, 0xEF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x91, 0x94, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xE5, 0xEA },
new int[] { 0x51, 0x55, 0x65, 0x91, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xE6, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x91, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xE6, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xE6, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x65, 0x94, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xE9, 0xEA },
new int[] { 0x55, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x94, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xE9, 0xEA },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x55, 0x56, 0x59, 0x65, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xE9, 0xEA, 0xEE },
new int[] { 0x54, 0x55, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },
new int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xEA },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0xA1, 0xA4, 0xA5, 0xA6, 0xA9, 0xAA, 0xB5, 0xBA, 0xE5, 0xEA, 0xFA },
new int[] { 0x51, 0x55, 0x65, 0x95, 0xA1, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA, 0xE6, 0xEA, 0xFA },
new int[] { 0x51, 0x55, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA, 0xE6, 0xEA, 0xFA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xE6, 0xEA, 0xFB },
new int[] { 0x54, 0x55, 0x65, 0x95, 0xA4, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA, 0xE9, 0xEA, 0xFA },
new int[] { 0x55, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },
new int[] { 0x51, 0x55, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },
new int[] { 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xAB, 0xBA, 0xEA, 0xFB },
new int[] { 0x54, 0x55, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA, 0xE9, 0xEA, 0xFA },
new int[] { 0x54, 0x55, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },
new int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },
new int[] { 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA, 0xEA },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xE9, 0xEA, 0xFE },
new int[] { 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAE, 0xBA, 0xEA, 0xFE },
new int[] { 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA, 0xEA },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xBA, 0xEA },
};
LatticeVertex4D[] latticeVerticesByCode = new LatticeVertex4D[256];
for ( int i = 0; i < 256; i++ )
{
int cx = ((i >> 0) & 3) - 1;
int cy = ((i >> 2) & 3) - 1;
int cz = ((i >> 4) & 3) - 1;
int cw = ((i >> 6) & 3) - 1;
latticeVerticesByCode[i] = new LatticeVertex4D( cx, cy, cz, cw );
}
int nLatticeVerticesTotal = 0;
for ( int i = 0; i < 256; i++ )
{
nLatticeVerticesTotal += lookup4DVertexCodes[i].Length;
}
LOOKUP_4D_A = new (short SecondaryIndexStart, short SecondaryIndexStop)[256];
LOOKUP_4D_B = new LatticeVertex4D[nLatticeVerticesTotal];
for ( int i = 0, j = 0; i < 256; i++ )
{
LOOKUP_4D_A[i] = ((short)j, (short)(j + lookup4DVertexCodes[i].Length));
for ( int k = 0; k < lookup4DVertexCodes[i].Length; k++ )
{
LOOKUP_4D_B[j++] = latticeVerticesByCode[lookup4DVertexCodes[i][k]];
}
}
}
private class LatticeVertex4D
{
public readonly float dx, dy, dz, dw;
public readonly long xsvp, ysvp, zsvp, wsvp;
public LatticeVertex4D( int xsv, int ysv, int zsv, int wsv )
{
this.xsvp = xsv * PRIME_X; this.ysvp = ysv * PRIME_Y;
this.zsvp = zsv * PRIME_Z; this.wsvp = wsv * PRIME_W;
float ssv = (xsv + ysv + zsv + wsv) * UNSKEW_4D;
this.dx = -xsv - ssv;
this.dy = -ysv - ssv;
this.dz = -zsv - ssv;
this.dw = -wsv - ssv;
}
}
}
using Editor;
using Sandbox;
using System;
namespace Sturnus.TerrainGenerationTool;
public static class Islands
{
public static float Default( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize = 0.1f, float warpStrength = 0.5f )
{
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
float warpX;
float warpY;
float warpedNx;
float warpedNy;
float noise;
if ( warp )
{
// Generate warp offsets using additional noise
warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
// Apply domain warping
warpedNx = nx + warpX;
warpedNy = ny + warpY;
}
else
{
warpedNx = nx;
warpedNy = ny;
}
// Radial distance from the center
float distance = (float)Math.Sqrt( nx * nx + ny * ny );
float falloff = 1.0f - Math.Clamp( distance, 0.25f, 1 ); // Smooth taper from center to edge
// Central mountain shape (parabolic for smooth curvature)
float centralMountain = (1.0f - distance * distance) * falloff;
// Beach-style taper near the edges
float beachStart = 0.5f; // Start of the beach region (distance normalized)
float beachEnd = 0.98f; // End of the beach region (ocean level)
float beachFalloff = Math.Clamp( (distance - beachStart) / (beachEnd - beachStart), 0.1f, 1 );
float beachTaper = (1.0f - beachFalloff) * 0.25f; // Smooth transition to flat region
// Add subtle noise for terrain variation
if ( warp )
{
noise = OpenSimplex2S.Noise2( seed, warpedNx * 2, warpedNy * 2 ) * 0.4f;
}
else
{
noise = OpenSimplex2S.Noise2( seed, nx * 6, ny * 6 ) * 0.05f; // Low-frequency noise
}
// Combine components: central mountain, beach taper, and noise
float output = centralMountain * (1.0f - beachFalloff) + beachTaper + noise;
// Combine all effects
float heightValue = output;
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Archipelagos( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1;
float ny = (y / (float)height) * 2 - 1;
// Apply domain warping
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Use noise layers to create clusters of small islands
float baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.5f, ny * 1.5f );
float secondaryNoise = OpenSimplex2S.Noise2( seed + 1, nx * 3.0f, ny * 3.0f ) * 0.5f;
float archipelagoHeight = baseNoise + secondaryNoise;
// Apply radial falloff to form rounded island clusters
float distance = MathF.Sqrt( nx * nx + ny * ny );
float falloff = Math.Clamp( 1 - distance * 1.2f, 0, 1 );
float heightValue = Math.Clamp( archipelagoHeight * falloff, 0, 1 );
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Atoll( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
// Apply domain warping
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 20, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 21, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Calculate distance from the center
float distance = MathF.Sqrt( nx * nx + ny * ny );
// Define parameters for the single ring
float ringCenter = 0.6f; // Center of the ring
float ringWidth = 0.1f; // Width of the ring
// Create a single ring using a Gaussian-like function
float ring = MathF.Exp( -MathF.Pow( (distance - ringCenter) / ringWidth, 2 ) );
// Add some noise for variation
float baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.0f, ny * 1.0f ) * 0.4f;
// Introduce a beach-like area (reduce noise and height for one side of the map)
float beachEffect = Math.Clamp( (1 - nx) * 0.5f, 0.2f, 1.0f ); // Reduces height on one side of the map
float beachNoise = OpenSimplex2S.Noise2( seed + 30, nx * 2.0f, ny * 2.0f ) * 0.2f;
// Combine the ring, noise, and beach effect
float heightValue = (ring + baseNoise * beachEffect + beachNoise) * beachEffect;
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Islets( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1;
float ny = (y / (float)height) * 2 - 1;
// Apply domain warping
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 30, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 31, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Generate scattered small islands
float scatterNoise = OpenSimplex2S.Noise2( seed, nx * 6.0f, ny * 6.0f );
float baseNoise = OpenSimplex2S.Noise2( seed + 1, nx * 3.0f, ny * 3.0f ) * 0.5f;
float isletHeight = scatterNoise + baseNoise;
// Apply distance falloff to create isolated islets
float distance = MathF.Sqrt( nx * nx + ny * ny );
float falloff = Math.Clamp( 1 - distance * 1.5f, 0, 1 );
float heightValue = Math.Clamp( isletHeight * falloff, 0, 1 );
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Oceanic( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1;
float ny = (y / (float)height) * 2 - 1;
// Apply domain warping
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 40, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 41, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Generate large, continuous landmass with a few scattered features
float baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.0f, ny * 1.0f );
float featureNoise = OpenSimplex2S.Noise2( seed + 1, nx * 2.0f, ny * 2.0f ) * 0.5f;
float oceanicHeight = baseNoise + featureNoise;
// Apply radial falloff for a natural ocean/land mix
float distance = MathF.Sqrt( nx * nx + ny * ny );
float falloff = Math.Clamp( 1 - distance * 1.0f, 0, 1 );
float heightValue = Math.Clamp( oceanicHeight * falloff, 0, 1 );
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
}
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
namespace Sturnus.TerrainGenerationTool;
public static class Planetary
{
public static float Sharded(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool warp, // Apply domain warping
float warpSize, // Warp scale
float warpStrength // Warp strength
)
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
float shardHeight = 10f;
float crackDepth = 1f;
float noiseStrength = 0.05f;
int cellCount = 5;
// Apply domain warping for irregularity
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Generate cracks
float minDist = float.MaxValue;
float secondaryDist = float.MaxValue;
float cellSize = 2.0f / cellCount; // Normalize the cell size
for ( int i = 0; i < cellCount; i++ )
{
for ( int j = 0; j < cellCount; j++ )
{
float cellX = -1 + i * cellSize + OpenSimplex2S.Noise2( seed + 20, i, j ) * cellSize * 0.5f;
float cellY = -1 + j * cellSize + OpenSimplex2S.Noise2( seed + 21, i, j ) * cellSize * 0.5f;
float distance = MathF.Sqrt( (nx - cellX) * (nx - cellX) + (ny - cellY) * (ny - cellY) );
if ( distance < minDist )
{
secondaryDist = minDist;
minDist = distance;
}
else if ( distance < secondaryDist )
{
secondaryDist = distance;
}
}
}
// Compute shard height based on the secondary distance
float shardNoise = OpenSimplex2S.Noise2( seed + 30, nx, ny ) * noiseStrength;
float heightValue = MathF.Max( secondaryDist - minDist, 0f ) * shardHeight + shardNoise;
// Apply crack depth at borders between shards
if ( secondaryDist - minDist < 0.03f ) // Control the width of cracks
{
heightValue -= crackDepth;
}
// Add a baseline value to ensure no flat zero areas
//heightValue = MathF.Max( heightValue, minHeight );
// Clamp height to avoid negative values
heightValue = Math.Clamp( heightValue, 0, 1 );
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Craters(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool warp, // Apply domain warping for irregularity
float warpSize, // Warp scale
float warpStrength // Warp strength
)
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
int craterCount = 100;
float minCraterSize = 0.1f;
float maxCraterSize = 0.3f;
float craterDepth = 0.05f;
float rimHeight = 0.05f;
float rimWidthRatio = 0.2f;
float noiseStrength = 0.05f;
float largeCraterRatio = 0.2f;
float slopeFalloff = 0.9f; // Controls smoothness of ramps
// Apply domain warping for irregularity
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Base terrain noise
float baseTerrain = OpenSimplex2S.Noise2( seed + 1, nx * 2.0f, ny * 2.0f ) * noiseStrength;
baseTerrain = (baseTerrain + 1) * 0.5f; // Normalize to [0, 1]
float heightValue = baseTerrain;
// Iterate through craters in reverse order to overwrite previous craters
for ( int i = craterCount - 1; i >= 0; i-- )
{
// Randomize crater properties
float craterX = random.Next( -100000, 100000 ) / 50000.0f;
float craterY = random.Next( -100000, 100000 ) / 50000.0f;
float craterRadius = (i < craterCount * largeCraterRatio)
? random.Next( (int)(maxCraterSize * 500), (int)(maxCraterSize * 1000) ) / 1000.0f // Large craters
: random.Next( (int)(minCraterSize * 500), (int)(minCraterSize * 1000) ) / 1000.0f; // Small craters
// Distance from the current point to the crater center
float distance = MathF.Sqrt( (nx - craterX) * (nx - craterX) + (ny - craterY) * (ny - craterY) );
if ( distance < craterRadius )
{
float rimStart = craterRadius * (1f - rimWidthRatio);
float rimEnd = craterRadius;
// Inside the pit
if ( distance < rimStart )
{
float pitFalloff = Math.Clamp( 1f - (distance / rimStart), 0f, 1f );
heightValue = baseTerrain - MathF.Pow( pitFalloff, slopeFalloff ) * craterDepth; // Smooth ramp to the center
}
// Raised rim
else if ( distance >= rimStart && distance < rimEnd )
{
float rimFalloff = Math.Clamp( (distance - rimStart) / (rimEnd - rimStart), 0f, 1f );
heightValue = baseTerrain + MathF.Pow( 1f - rimFalloff, slopeFalloff ) * rimHeight; // Rounded rim
}
// Reset terrain below the rim to prevent intersecting ridges
if ( distance >= rimEnd )
{
heightValue = baseTerrain;
}
// Exit the loop once the current crater is applied
break;
}
}
// Ensure height values are clamped to [0, 1]
heightValue = Math.Clamp( heightValue, 0, 1 );
return heightValue;
}
}
using Editor;
using Sandbox;
using Sandbox.UI;
namespace Panelize;
public class Properties : Widget
{
public static object SelectedObject { get; set; }
private static object currentSelectedObject { get; set; }
Layout editor;
public Properties(MainWindow mainWindow) : base(mainWindow)
{
WindowTitle = "Properties";
Layout = Layout.Column();
editor = Layout.AddRow( 1 );
SelectedObject = null;
currentSelectedObject = null;
}
[EditorEvent.Frame]
public void Frame()
{
//Log.Info( $"Select {SelectedObject}" );
if ( SelectedObject != currentSelectedObject )
{
Select( SelectedObject );
}
}
private void Select(object obj )
{
currentSelectedObject = obj;
editor.Clear(true);
var so = obj.GetSerialized();
Widget inspector = null;
if(obj is Panel p )
{
inspector = new PanelInspector( this, p, PanelEditorSession.Current );
}
else
{
inspector = InspectorWidget.Create( so );
}
if ( inspector.IsValid() )
{
editor.Add( inspector, 1 );
}
else
{
// Try CanEdit still..
// todo: Everything that should be an inspector should be an InspectorWidget
inspector = CanEditAttribute.CreateEditorForObject( obj );
if ( inspector.IsValid() )
{
editor.Add( inspector, 1 );
}
else
{
try
{
var sheet = new ControlSheet();
sheet.AddObject( so );
var scroller = new ScrollArea( this );
scroller.Canvas = new Widget();
scroller.Canvas.Layout = Layout.Column();
scroller.Canvas.VerticalSizeMode = SizeMode.CanGrow;
scroller.Canvas.HorizontalSizeMode = SizeMode.Flexible;
scroller.Canvas.Layout.Add( sheet );
scroller.Canvas.Layout.AddStretchCell();
editor.Add( scroller );
}
catch { }
}
}
}
}
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.UI;
using System.Linq;
namespace Panelize;
public class PanelBrowser : Widget
{
public float EntrySize { get; set; } = 200f;
public int Columns { get; set; } = 2;
GridLayout grid;
public PanelBrowser()
{
WindowTitle = "Panel List";
Layout = Layout.Column();
grid = Layout.AddLayout(Layout.Grid(), 10);
grid.Spacing = 5f;
BuildList();
}
private void BuildList()
{
grid.Clear( true );
var builderTypes = EditorTypeLibrary.GetTypes<PanelBuilder>();
int x = 0;
int y = 0;
foreach (var type in builderTypes)
{
if ( type.IsAbstract || type.IsGenericType ) continue;
if (x > Columns)
{
x = 0;
y++;
}
PanelPreviewWidget preview = new PanelPreviewWidget( type.Create<PanelBuilder>() )
{
FixedWidth = EntrySize,
FixedHeight = EntrySize
};
grid.AddCell(x, y, preview);
x++;
}
}
}
public class PanelPreviewWidget : Widget
{
PanelBuilder builder;
Color bgColor = Color.Gray;
//Drag drag;
public PanelPreviewWidget( PanelBuilder builder )
{
Assert.NotNull( builder );
this.builder = builder;
IsDraggable = true;
}
protected override void OnPaint()
{
Rect size = LocalRect;
Paint.SetBrushAndPen( bgColor );
Paint.DrawRect( size.Shrink(5f), 1f );
Paint.SetPen( Color.White );
Paint.SetFont( "Roboto", 24f, 800 );
Paint.DrawText( size, builder.Title );
}
bool dragging = false;
protected override void OnDragStart()
{
if ( dragging ) return;
dragging = true;
Log.Info( $"Drag Start!" );
}
protected override void OnMouseReleased( MouseEvent e )
{
if(e.LeftMouseButton && dragging )
{
Vector2 pos = e.WindowPosition;
Log.Info( $"Drag stop!" );
PanelEditorSession.Current.EditorWidget?.OnBuilderDrop( builder, e );
dragging = false;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Panelize;
public class Checkbox : Widget
{
public bool Value { get; set; }
public string IconEnabled { get; set; }
public string IconDisabled { get; set; }
public Action<bool> OnEdited;
public Checkbox( Widget parent = null, bool value = false, string iconEnabled = null, string iconDisabled = null, float? size = null) : base(parent)
{
Value = value;
IconEnabled = iconEnabled;
IconDisabled = iconDisabled;
Cursor = CursorShape.Finger;
MinimumWidth = size ?? ControlWidget.ControlRowHeight;
MinimumHeight = size ?? ControlWidget.ControlRowHeight;
HorizontalSizeMode = SizeMode.CanShrink;
VerticalSizeMode = SizeMode.CanShrink;
}
protected override void OnMouseClick( MouseEvent e )
{
if(e.LeftMouseButton)
{
Value = !Value;
OnEdited?.Invoke(Value);
}
}
protected override void OnPaint()
{
Paint.Antialiasing = true;
Paint.TextAntialiasing = true;
float alpha = (ReadOnly ? 0.5f : 1f);
Rect localRect = LocalRect;
Color color = Theme.Blue;
Rect rect = localRect.Shrink( 2 );
Paint.ClearPen();
Paint.SetBrush( ControlWidget.ControlColor.Lighten( ReadOnly ? 0.5f : 0f ).WithAlphaMultiplied( alpha ) );
Paint.DrawRect( rect, 2f );
if(Value)
{
Paint.SetPen( color.WithAlpha( 0.3f * alpha ), 1f );
Paint.SetBrush( color.WithAlpha( 0.2f * alpha ) );
Paint.DrawRect( rect, 2f );
Paint.SetPen( color.WithAlphaMultiplied( 0.5f ) );
Paint.DrawIcon( rect, IconEnabled ?? "done", 13f );
}
else if ( IconDisabled != null )
{
Paint.SetPen( Theme.Grey.WithAlphaMultiplied( 0.5f ) );
Paint.DrawIcon( rect, IconDisabled, 13f );
}
if ( IsUnderMouse && !ReadOnly )
{
Paint.SetPen( color.WithAlpha(0.5f * alpha), 1);
Paint.ClearBrush();
Paint.DrawRect( in rect, 1f );
}
}
}
namespace RbxlReader.DataTypes;
public class NumberSequenceKeypoint {
public float Time;
public float Value;
public float Envelope;
}
public class NumberSequence {
public NumberSequenceKeypoint[] Keypoints;
public NumberSequence(NumberSequenceKeypoint[] points) {
Keypoints = points;
}
}namespace RbxlReader.DataTypes;
public class UDim {
public float Scale;
public int Offset;
public UDim() {}
public UDim(float scale, int offset) {
Scale = scale;
Offset = offset;
}
}using System.Collections.Generic;
using System.Linq;
namespace Ink
{
/// <summary>
/// A class representing a character range. Allows for lazy-loading a corresponding <see cref="CharacterSet">character set</see>.
/// </summary>
public sealed class CharacterRange
{
public static CharacterRange Define(char start, char end, IEnumerable<char> excludes = null)
{
return new CharacterRange (start, end, excludes);
}
/// <summary>
/// Returns a <see cref="CharacterSet">character set</see> instance corresponding to the character range
/// represented by the current instance.
/// </summary>
/// <remarks>
/// The internal character set is created once and cached in memory.
/// </remarks>
/// <returns>The char set.</returns>
public CharacterSet ToCharacterSet ()
{
if (_correspondingCharSet.Count == 0)
{
for (char c = _start; c <= _end; c++)
{
if (!_excludes.Contains (c))
{
_correspondingCharSet.Add (c);
}
}
}
return _correspondingCharSet;
}
public char start { get { return _start; } }
public char end { get { return _end; } }
CharacterRange (char start, char end, IEnumerable<char> excludes)
{
_start = start;
_end = end;
_excludes = excludes == null ? new HashSet<char>() : new HashSet<char> (excludes);
}
char _start;
char _end;
ICollection<char> _excludes;
CharacterSet _correspondingCharSet = new CharacterSet();
}
}
using System;
using Ink.Parsed;
using System.Collections.Generic;
namespace Ink
{
public partial class InkParser
{
protected class InfixOperator
{
public string type;
public int precedence;
public bool requireWhitespace;
public InfixOperator(string type, int precedence, bool requireWhitespace) {
this.type = type;
this.precedence = precedence;
this.requireWhitespace = requireWhitespace;
}
public override string ToString ()
{
return type;
}
}
protected Parsed.Object TempDeclarationOrAssignment()
{
Whitespace ();
bool isNewDeclaration = ParseTempKeyword();
Whitespace ();
Identifier varIdentifier = null;
if (isNewDeclaration) {
varIdentifier = (Identifier)Expect (IdentifierWithMetadata, "variable name");
} else {
varIdentifier = Parse(IdentifierWithMetadata);
}
if (varIdentifier == null) {
return null;
}
Whitespace();
// += -=
bool isIncrement = ParseString ("+") != null;
bool isDecrement = ParseString ("-") != null;
if (isIncrement && isDecrement) Error ("Unexpected sequence '+-'");
if (ParseString ("=") == null) {
// Definitely in an assignment expression?
if (isNewDeclaration) Error ("Expected '='");
return null;
}
Expression assignedExpression = (Expression)Expect (Expression, "value expression to be assigned");
if (isIncrement || isDecrement) {
var result = new IncDecExpression (varIdentifier, assignedExpression, isIncrement);
return result;
} else {
var result = new VariableAssignment (varIdentifier, assignedExpression);
result.isNewTemporaryDeclaration = isNewDeclaration;
return result;
}
}
protected void DisallowIncrement (Parsed.Object expr)
{
if (expr is Parsed.IncDecExpression)
Error ("Can't use increment/decrement here. It can only be used on a ~ line");
}
protected bool ParseTempKeyword()
{
var ruleId = BeginRule ();
if (Parse (Identifier) == "temp") {
SucceedRule (ruleId);
return true;
} else {
FailRule (ruleId);
return false;
}
}
protected Parsed.Return ReturnStatement()
{
Whitespace ();
var returnOrDone = Parse(Identifier);
if (returnOrDone != "return") {
return null;
}
Whitespace ();
var expr = Parse(Expression);
var returnObj = new Return (expr);
return returnObj;
}
protected Expression Expression() {
return Expression(minimumPrecedence:0);
}
// Pratt Parser
// aka "Top down operator precedence parser"
// http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
// Algorithm overview:
// The two types of precedence are handled in two different ways:
// ((((a . b) . c) . d) . e) #1
// (a . (b . (c . (d . e)))) #2
// Where #1 is automatically handled by successive loops within the main 'while' in this function,
// so long as continuing operators have lower (or equal) precedence (e.g. imagine some series of "*"s then "+" above.
// ...and #2 is handled by recursion of the right hand term in the binary expression parser.
// (see link for advice on how to extend for postfix and mixfix operators)
protected Expression Expression(int minimumPrecedence)
{
Whitespace ();
// First parse a unary expression e.g. "-a" or parethensised "(1 + 2)"
var expr = ExpressionUnary ();
if (expr == null) {
return null;
}
Whitespace ();
// Attempt to parse (possibly multiple) continuing infix expressions (e.g. 1 + 2 + 3)
while(true) {
var ruleId = BeginRule ();
// Operator
var infixOp = ParseInfixOperator ();
if (infixOp != null && infixOp.precedence > minimumPrecedence) {
// Expect right hand side of operator
var expectationMessage = string.Format("right side of '{0}' expression", infixOp.type);
var multiaryExpr = Expect (() => ExpressionInfixRight (left: expr, op: infixOp), expectationMessage);
if (multiaryExpr == null) {
// Fail for operator and right-hand side of multiary expression
FailRule (ruleId);
return null;
}
expr = SucceedRule(ruleId, multiaryExpr) as Parsed.Expression;
continue;
}
FailRule (ruleId);
break;
}
Whitespace ();
return expr;
}
protected Expression ExpressionUnary()
{
// Divert target is a special case - it can't have any other operators
// applied to it, and we also want to check for it first so that we don't
// confuse "->" for subtraction.
var divertTarget = Parse (ExpressionDivertTarget);
if (divertTarget != null) {
return divertTarget;
}
var prefixOp = (string) OneOf (String ("-"), String ("!"));
// Don't parse like the string rules above, in case its actually
// a variable that simply starts with "not", e.g. "notable".
// This rule uses the Identifier rule, which will scan as much text
// as possible before returning.
if (prefixOp == null) {
prefixOp = Parse(ExpressionNot);
}
Whitespace ();
// - Since we allow numbers at the start of variable names, variable names are checked before literals
// - Function calls before variable names in case we see parentheses
var expr = OneOf (ExpressionList, ExpressionParen, ExpressionFunctionCall, ExpressionVariableName, ExpressionLiteral) as Expression;
// Only recurse immediately if we have one of the (usually optional) unary ops
if (expr == null && prefixOp != null) {
expr = ExpressionUnary ();
}
if (expr == null)
return null;
if (prefixOp != null) {
expr = UnaryExpression.WithInner(expr, prefixOp);
}
Whitespace ();
var postfixOp = (string) OneOf (String ("++"), String ("--"));
if (postfixOp != null) {
bool isInc = postfixOp == "++";
if (!(expr is VariableReference)) {
Error ("can only increment and decrement variables, but saw '" + expr + "'");
// Drop down and succeed without the increment after reporting error
} else {
// TODO: Language Server - (Identifier combined into one vs. list of Identifiers)
var varRef = (VariableReference)expr;
expr = new IncDecExpression(varRef.identifier, isInc);
}
}
return expr;
}
protected string ExpressionNot()
{
var id = Identifier ();
if (id == "not") {
return id;
}
return null;
}
protected Expression ExpressionLiteral()
{
return (Expression) OneOf (ExpressionFloat, ExpressionInt, ExpressionBool, ExpressionString);
}
protected Expression ExpressionDivertTarget()
{
Whitespace ();
var divert = Parse(SingleDivert);
if (divert == null)
return null;
if (divert.isThread)
return null;
Whitespace ();
return new DivertTarget (divert);
}
protected Number ExpressionInt()
{
int? intOrNull = ParseInt ();
if (intOrNull == null) {
return null;
} else {
return new Number (intOrNull.Value);
}
}
protected Number ExpressionFloat()
{
float? floatOrNull = ParseFloat ();
if (floatOrNull == null) {
return null;
} else {
return new Number (floatOrNull.Value);
}
}
protected StringExpression ExpressionString()
{
var openQuote = ParseString ("\"");
if (openQuote == null)
return null;
// Set custom parser state flag so that within the text parser,
// it knows to treat the quote character (") as an end character
parsingStringExpression = true;
List<Parsed.Object> textAndLogic = Parse (MixedTextAndLogic);
Expect (String ("\""), "close quote for string expression");
parsingStringExpression = false;
if (textAndLogic == null) {
textAndLogic = new List<Ink.Parsed.Object> ();
textAndLogic.Add (new Parsed.Text (""));
}
else if (textAndLogic.Exists (c => c is Divert))
Error ("String expressions cannot contain diverts (->)");
return new StringExpression (textAndLogic);
}
protected Number ExpressionBool()
{
var id = Parse(Identifier);
if (id == "true") {
return new Number (true);
} else if (id == "false") {
return new Number (false);
}
return null;
}
protected Expression ExpressionFunctionCall()
{
var iden = Parse(IdentifierWithMetadata);
if (iden == null)
return null;
Whitespace ();
var arguments = Parse(ExpressionFunctionCallArguments);
if (arguments == null) {
return null;
}
return new FunctionCall(iden, arguments);
}
protected List<Expression> ExpressionFunctionCallArguments()
{
if (ParseString ("(") == null)
return null;
// "Exclude" requires the rule to succeed, but causes actual comma string to be excluded from the list of results
ParseRule commas = Exclude (String (","));
var arguments = Interleave<Expression>(Expression, commas);
if (arguments == null) {
arguments = new List<Expression> ();
}
Whitespace ();
Expect (String (")"), "closing ')' for function call");
return arguments;
}
protected Expression ExpressionVariableName()
{
List<Identifier> path = Interleave<Identifier> (IdentifierWithMetadata, Exclude (Spaced (String ("."))));
if (path == null || Story.IsReservedKeyword (path[0].name) )
return null;
return new VariableReference (path);
}
protected Expression ExpressionParen()
{
if (ParseString ("(") == null)
return null;
var innerExpr = Parse(Expression);
if (innerExpr == null)
return null;
Whitespace ();
Expect (String(")"), "closing parenthesis ')' for expression");
return innerExpr;
}
protected Expression ExpressionInfixRight(Parsed.Expression left, InfixOperator op)
{
Whitespace ();
var right = Parse(() => Expression (op.precedence));
if (right) {
// We assume that the character we use for the operator's type is the same
// as that used internally by e.g. Runtime.Expression.Add, Runtime.Expression.Multiply etc
var expr = new BinaryExpression (left, right, op.type);
return expr;
}
return null;
}
private InfixOperator ParseInfixOperator()
{
foreach (var op in _binaryOperators) {
int ruleId = BeginRule ();
if (ParseString (op.type) != null) {
if (op.requireWhitespace) {
if (Whitespace () == null) {
FailRule (ruleId);
continue;
}
}
return (InfixOperator) SucceedRule(ruleId, op);
}
FailRule (ruleId);
}
return null;
}
protected Parsed.List ExpressionList ()
{
Whitespace ();
if (ParseString ("(") == null)
return null;
Whitespace ();
// When list has:
// - 0 elements (null list) - this is okay, it's an empty list: "()"
// - 1 element - it could be confused for a single non-list related
// identifier expression in brackets, but this is a useless thing
// to do, so we reserve that syntax for a list with one item.
// - 2 or more elements - normal!
List<Identifier> memberNames = SeparatedList (ListMember, Spaced (String (",")));
Whitespace ();
// May have failed to parse the inner list - the parentheses may
// be for a normal expression
if (ParseString (")") == null)
return null;
return new List (memberNames);
}
protected Identifier ListMember ()
{
Whitespace ();
Identifier identifier = Parse (IdentifierWithMetadata);
if (identifier == null)
return null;
var dot = ParseString (".");
if (dot != null) {
Identifier identifier2 = Expect (IdentifierWithMetadata, "element name within the set " + identifier) as Identifier;
identifier.name = identifier.name + "." + identifier2?.name;
}
Whitespace ();
return identifier;
}
void RegisterExpressionOperators()
{
_maxBinaryOpLength = 0;
_binaryOperators = new List<InfixOperator> ();
// These will be tried in order, so we need "<=" before "<"
// for correctness
RegisterBinaryOperator ("&&", precedence:1);
RegisterBinaryOperator ("||", precedence:1);
RegisterBinaryOperator ("and", precedence:1, requireWhitespace: true);
RegisterBinaryOperator ("or", precedence:1, requireWhitespace: true);
RegisterBinaryOperator ("==", precedence:2);
RegisterBinaryOperator (">=", precedence:2);
RegisterBinaryOperator ("<=", precedence:2);
RegisterBinaryOperator ("<", precedence:2);
RegisterBinaryOperator (">", precedence:2);
RegisterBinaryOperator ("!=", precedence:2);
// (apples, oranges) + cabbages has (oranges, cabbages) == true
RegisterBinaryOperator ("?", precedence: 3);
RegisterBinaryOperator ("has", precedence: 3, requireWhitespace:true);
RegisterBinaryOperator ("!?", precedence: 3);
RegisterBinaryOperator ("hasnt", precedence: 3, requireWhitespace: true);
RegisterBinaryOperator ("^", precedence: 3);
RegisterBinaryOperator ("+", precedence:4);
RegisterBinaryOperator ("-", precedence:5);
RegisterBinaryOperator ("*", precedence:6);
RegisterBinaryOperator ("/", precedence:7);
RegisterBinaryOperator ("%", precedence:8);
RegisterBinaryOperator ("mod", precedence:8, requireWhitespace:true);
}
void RegisterBinaryOperator(string op, int precedence, bool requireWhitespace = false)
{
_binaryOperators.Add(new InfixOperator (op, precedence, requireWhitespace));
_maxBinaryOpLength = Math.Max (_maxBinaryOpLength, op.Length);
}
List<InfixOperator> _binaryOperators;
int _maxBinaryOpLength;
}
}
using Editor;
using Editor.TerrainEditor;
namespace Sandbox;
public class ProjectSettingsWindow : WidgetWindow
{
public ProjectSettingsWindow( Widget parent ) : base( parent, "Project Settings" )
{
Parent = parent;
DeleteOnClose = true;
Size = new Vector2( 1580, 720 );
WindowTitle = "Project Settings";
Float();
WindowFlags = WindowFlags.Dialog | WindowFlags.Customized | WindowFlags.CloseButton | WindowFlags.WindowSystemMenuHint | WindowFlags.WindowTitle | WindowFlags.MaximizeButton;
SetWindowIcon( "electrical_services" );
Layout = Layout.Column();
var inspectorWidget = InspectorWidget.Create( Project.Current.GetSerialized() );
Layout.Add( inspectorWidget );
}
}
using Sandbox;
using System.Linq;
using static Sandbox.ResourceLibrary;
namespace Editor;
public class MotivationNotice : NoticeWidget
{
public string Portrait { get; init; }
public string Message { get; init; }
public string Bubble => FileSystem.Mounted.GetFullPath( "portraits/bubble.png" );
protected override Vector2 SizeHint() => new( 512, 384 );
public MotivationNotice()
{
var personality = Game.Random.FromArray( GetAll<MotivationResource>().ToArray() );
Portrait = personality.GetPortrait();
Message = personality.GetMessage();
}
protected override void OnPaint()
{
Paint.SetPen( Theme.TextDark );
Paint.SetDefaultFont( 16 );
var rect = LocalRect.Align( 350, TextFlag.LeftBottom );
Paint.Draw( rect, Portrait );
rect = LocalRect.Align( 250, TextFlag.RightTop );
Paint.Draw( rect, Bubble );
rect = rect.Shrink( 0, 0, 0, 55 );
Paint.DrawText( rect, Message );
}
}
using Sandbox;
using System;
public class EditorTheme
{
[Group( "Window" )] public Color TabBackground { get; set; } = new Color( 0.231f, 0.231f, 0.231f );
[Group( "Window" )] public Color TabBarBackground { get; set; } = new Color( 0.141f, 0.141f, 0.141f );
[Group( "Window" )] public Color TabInactiveBackground { get; set; } = new Color( 0.141f, 0.141f, 0.141f );
[Group( "Window" )] public Color SurfaceBackground { get; set; } = new Color( 0.231f, 0.231f, 0.231f );
[Group( "Window" )] public Color SurfaceLightBackground { get; set; } = new Color( 0.412f, 0.412f, 0.412f );
[Group( "Window" )] public Color SidebarBackground { get; set; } = new Color( 0.141f, 0.141f, 0.141f );
[Group( "Window" )] public Color WindowBackground { get; set; } = new Color( 0.094f, 0.094f, 0.094f );
[Group( "Window" )] public Color WidgetBackground { get; set; } = new Color( 0.141f, 0.141f, 0.141f );
[Group( "Window" )] public Color ControlBackground { get; set; } = new Color( 0.094f, 0.094f, 0.094f );
[Group( "Window" )] public Color ButtonBackground { get; set; } = new Color( 0.231f, 0.231f, 0.231f );
[Group( "Window" )] public Color SelectedBackground { get; set; } = new Color( 0.502f, 0.502f, 0.502f );
[Group( "Window" )] public Color StatusBarBackground { get; set; } = new Color( 0.141f, 0.141f, 0.141f );
[Group( "Text" )] public Color Text { get; set; } = new Color( 1.0f, 1.0f, 1.0f );
[Group( "Text" )] public Color TextControl { get; set; } = new Color( 1.0f, 1.0f, 1.0f );
[Group( "Text" )] public Color TextLight { get; set; } = new Color( 0.62f, 0.62f, 0.62f );
[Group( "Text" )] public Color TextWidget { get; set; } = new Color( 1.0f, 1.0f, 1.0f );
[Group( "Text" )] public Color TextButton { get; set; } = new Color( 1.0f, 1.0f, 1.0f );
[Group( "Text" )] public Color TextSelected { get; set; } = new Color( 0.4f, 0.639f, 1.0f );
[Group( "Text" )] public Color TextLink { get; set; } = new Color( 0.4f, 0.639f, 1.0f );
[Group( "Text" )] public Color TextHighlight { get; set; } = new Color( 0.4f, 0.639f, 1.0f );
[Group( "Text" )] public Color TextDisabled { get; set; } = new Color( 1.0f, 1.0f, 1.0f, 0.47f );
[Group( "Text" )] public Color TextDark { get; set; } = new Color( 0.0f, 0.0f, 0.0f );
[Group( "Window" )] public Color Border { get; set; } = new Color( 0.322f, 0.322f, 0.322f );
[Group( "Window" )] public Color BorderLight { get; set; } = new Color( 0.412f, 0.412f, 0.412f );
[Group( "Window" )] public Color BorderButton { get; set; } = new Color( 0.412f, 0.412f, 0.412f );
[Group( "Window" )] public Color Shadow { get; set; } = new Color( 0.141f, 0.141f, 0.141f );
[Group( "Window" )] public Color Primary { get; set; } = new Color( 0.353f, 0.553f, 0.922f );
[Group( "Window" )] public Color Overlay { get; set; } = new Color( 0.141f, 0.141f, 0.141f );
[Group( "Window" )] public Color MultipleValues { get; set; } = new Color( 0.502f, 0.502f, 0.502f );
[Group( "Window" )] public Color Highlight { get; set; } = new Color( 0.298f, 0.478f, 0.749f );
[Group( "Checkbox" )] public Color ToggleEnabled { get; set; } = new Color( 0.353f, 0.922f, 0.361f );
[Group( "Checkbox" )] public Color ToggleDisabled { get; set; } = new Color( 0.337f, 0.431f, 0.337f );
[Group( "Window" )] public Color Base { get; set; } = new Color(0.125f, 0.125f, 0.125f);
[Group( "Window" )] public Color BaseAlt { get; set; } = new Color(0.164f,0.164f,0.164f);
[Group( "Colors?" )] public Color Blue { get; set; } = new Color( 0.353f, 0.553f, 0.922f );
[Group( "Colors?" )] public Color Green { get; set; } = new Color( 0.690f, 0.89f, 0.302f );
[Group( "Colors?" )] public Color Red { get; set; } = new Color( 0.984f, 0.353f, 0.353f );
[Group( "Colors?" )] public Color Yellow { get; set; } = new Color( 0.902f, 0.859f, 0.455f );
[Group( "Colors?" )] public Color Pink { get; set; } = new Color( 0.874f, 0.569f, 0.580f );
[Group( "Files" )] public Color Prefab { get; set; } = new Color( 0.353f, 0.553f, 0.922f );
[Group( "Files" )] public Color Folder { get; set; } = new Color( 0.902f, 0.859f, 0.455f );
[Group( "Layout" )] public int RowHeight { get; set; } = 22;
[Group( "Layout" )] public int ControlHeight { get; set; } = 22;
[Group( "Layout" )] public int ControlRadius { get; set; } = 4;
[FontName][Group( "Text" )] public string HeadingFont { get; set; } = "Inter";
[FontName][Group( "Text" )] public string DefaultFont { get; set; } = "Inter";
public EditorTheme()
{ }
public static EditorTheme ShallowCopy( EditorTheme source )
{
if ( source == null )
throw new ArgumentNullException( nameof( source ) );
EditorTheme copy = new EditorTheme();
var properties = typeof( EditorTheme ).GetProperties();
foreach ( var prop in properties )
{
if ( prop.CanRead && prop.CanWrite )
{
var value = prop.GetValue( source, null );
prop.SetValue( copy, value, null );
}
}
return copy;
}
}
using System.Text;
using System.Text.Json.Serialization;
using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
#nullable enable
/// <summary>
/// Describes a translation and scale that can be applied to <see cref="MovieTime"/>s.
/// </summary>
/// <param name="Translation">Time offset to apply.</param>
/// <param name="Scale">Time scale to apply.</param>
public readonly record struct MovieTransform(
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
MovieTime Translation = default,
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
MovieTimeScale Scale = default )
{
public static MovieTransform Identity => default;
[JsonIgnore]
public MovieTransform Inverse => new( Scale.Inverse * -Translation, Scale.Inverse );
public static MovieTime operator *( MovieTransform transform, MovieTime time ) =>
time * transform.Scale + transform.Translation;
public static MovieTimeRange operator *( MovieTransform transform, MovieTimeRange timeRange ) =>
(transform * timeRange.Start, transform * timeRange.End);
public static MovieTransform operator *( MovieTransform a, MovieTransform b ) =>
new( a * b.Translation, a.Scale * b.Scale );
public static MovieTransform operator *( MovieTimeScale a, MovieTransform b ) =>
new( a * b.Translation, a * b.Scale );
public static MovieTransform operator +( MovieTransform transform, MovieTime translation ) =>
transform with { Translation = transform.Translation + translation };
public static MovieTransform FromTo( MovieTimeRange from, MovieTimeRange to )
{
var scale = MovieTimeScale.FromDurationChange( from.Duration, to.Duration );
return scale * new MovieTransform( -from.Start ) + to.Start;
}
private bool PrintMembers( StringBuilder builder )
{
if ( this == Identity )
{
builder.Append( "Identity" );
return true;
}
if ( !Translation.IsZero )
{
builder.Append( $"{nameof(Translation)} = {Translation}" );
}
if ( Scale != MovieTimeScale.Identity )
{
if ( !Translation.IsZero )
{
builder.Append( ", " );
}
builder.Append( $"{nameof(Scale)} = {Scale}" );
}
return true;
}
}
namespace Editor.MovieMaker.BlockDisplays;
#nullable enable
public abstract class ThumbnailBlockItem<T> : PropertyBlockItem<T>
{
protected abstract Pixmap? GetThumbnail();
protected override void OnPaint()
{
base.OnPaint();
if ( GetThumbnail() is { } thumb )
{
Paint.Draw( LocalRect.Contain( Height ), thumb, 0.5f );
}
}
}
public sealed class ResourceBlockItem<T> : ThumbnailBlockItem<T>
where T : Resource
{
protected override Pixmap? GetThumbnail() => Block.GetValue( Block.TimeRange.Start ) is { ResourcePath: { } path }
? AssetSystem.FindByPath( path )?.GetAssetThumb()
: null;
}
using System.Linq;
using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
#nullable enable
// [MovieModification( "Stretch", Icon = "width_full" )]
file sealed class StretchModification() : PerTrackModification<StretchOptions>( StretchOptions.Default, true )
{
public override void Start( TimeSelection selection )
{
Options = Options with { SourceDuration = selection.TotalTimeRange.Duration };
}
protected override ITrackModification<TValue> OnCreateModification<TValue>( IPropertyTrack<TValue> track ) =>
new StretchTrackModification<TValue>();
}
file sealed record StretchOptions( MovieTime SourceDuration = default ) : IModificationOptions
{
public static StretchOptions Default => new();
}
file sealed class StretchTrackModification<T> : ITrackModification<T, StretchOptions>
{
public IEnumerable<PropertyBlock<T>> Apply( IReadOnlyList<PropertyBlock<T>> original,
TimeSelection selection, StretchOptions options )
{
return options.SourceDuration > 0 && options.SourceDuration != selection.TotalTimeRange
? original.Select( x => Stretch( x, selection, options ) )
: original;
}
private PropertyBlock<T> Stretch( PropertyBlock<T> original, TimeSelection selection, StretchOptions options )
{
var signal = original.Signal.SlidingStretch( options.SourceDuration, selection );
return new PropertyBlock<T>( signal, original.TimeRange );
}
}
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json.Serialization;
using Sandbox.MovieMaker.Compiled;
using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
#nullable enable
[JsonDiscriminator( "Source" )]
[method: JsonConstructor]
file sealed record CompiledSignal<T>( ProjectSourceClip Source, int TrackIndex, int BlockIndex,
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )] MovieTransform Transform = default,
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )] MovieTime SmoothingSize = default ) : PropertySignal<T>
{
private IReadOnlyList<T>? _samples;
private CompiledSampleBlock<T>? _block;
private CompiledSampleBlock<T> Block => _block ??= (CompiledSampleBlock<T>)((CompiledPropertyTrack<T>)Source.Clip.Tracks[TrackIndex]).Blocks[BlockIndex];
private IReadOnlyList<T> Samples => _samples ??= Block.Resample( Block.SampleRate, SmoothingSize, _interpolator );
public CompiledSignal( CompiledSignal<T> copy )
: base( copy )
{
Source = copy.Source;
TrackIndex = copy.TrackIndex;
BlockIndex = copy.BlockIndex;
Transform = copy.Transform;
SmoothingSize = copy.SmoothingSize;
_samples = null;
_block = null;
}
private MovieTime GetLocalTime( MovieTime time ) =>
(Transform.Inverse * time).Clamp( Block.TimeRange ) - Block.TimeRange.Start - Block.Offset;
public override T GetValue( MovieTime time ) =>
Samples.Sample( GetLocalTime( time ), Block.SampleRate, _interpolator );
protected override PropertySignal<T> OnTransform( MovieTransform transform ) =>
this with { Transform = transform * Transform };
protected override PropertySignal<T> OnReduce( MovieTime? start, MovieTime? end )
{
if ( start is { } s && GetLocalTime( s ) >= Block.TimeRange.Duration ) return Block.GetValue( Block.TimeRange.End );
if ( end is { } e && GetLocalTime( e ) <= 0d ) return Block.GetValue( Block.TimeRange.Start );
return this;
}
protected override PropertySignal<T> OnSmooth( MovieTime size ) =>
_interpolator is null ? this : this with { SmoothingSize = size };
public override IEnumerable<MovieTimeRange> GetPaintHints( MovieTimeRange timeRange )
{
if ( timeRange.Intersect( Transform * Block.TimeRange ) is { } intersection )
{
return [intersection];
}
return [];
}
protected override bool PrintMembers( StringBuilder builder )
{
builder.Append( $"Source = {Source}, " );
builder.Append( $"Block = {Block.TimeRange}" );
if ( Transform != MovieTransform.Identity )
{
builder.Append( $", Transform = {Transform}" );
}
if ( SmoothingSize != default )
{
builder.Append( $", SmoothingSize = {SmoothingSize}" );
}
return true;
}
public override int GetHashCode()
{
return HashCode.Combine( Source, TrackIndex, BlockIndex, Transform, SmoothingSize );
}
public bool Equals( CompiledSignal<T>? other )
{
if ( other is null ) return false;
return Source.Equals( other.Source )
&& TrackIndex == other.TrackIndex
&& BlockIndex == other.BlockIndex
&& Transform == other.Transform
&& SmoothingSize == other.SmoothingSize;
}
private static readonly IInterpolator<T>? _interpolator = Interpolator.GetDefault<T>();
}
file sealed class ResampleCache<T>
{
private readonly record struct Key( int SampleRate, MovieTime SmoothingSize );
#pragma warning disable SB3000
[SkipHotload]
private static ConditionalWeakTable<CompiledSampleBlock<T>, Dictionary<Key, WeakReference<T[]>>> Cache { get; } = new();
#pragma warning restore SB3000
public static T[]? Get( CompiledSampleBlock<T> block, int sampleRate, MovieTime smoothingSize )
{
return Cache.TryGetValue( block, out var dict )
&& dict.TryGetValue( new( sampleRate, smoothingSize ), out var weakRef )
&& weakRef.TryGetTarget( out var array ) ? array : null;
}
public static void Set( CompiledSampleBlock<T> block, int sampleRate, MovieTime smoothingSize, T[] array )
{
if ( !Cache.TryGetValue( block, out var dict ) )
{
Cache.TryAdd( block, dict = new Dictionary<Key, WeakReference<T[]>>() );
}
dict[new( sampleRate, smoothingSize )] = new WeakReference<T[]>( array );
}
}
partial class PropertySignalExtensions
{
public static IReadOnlyList<PropertyBlock<T>> AsBlocks<T>( this ProjectSourceClip source, IProjectPropertyTrack track )
{
var (refTrack, propertyPath) = track.GetPath();
if ( source.Clip.GetProperty<T>( refTrack.Id, propertyPath ) is not { } matchingTrack )
{
return [];
}
var trackIndex = source.Clip.Tracks.IndexOf( matchingTrack );
return matchingTrack.Blocks
.Select( (x, i) => new PropertyBlock<T>( x switch
{
CompiledConstantBlock<T> constant => constant.Value,
CompiledSampleBlock<T> => new CompiledSignal<T>( source, trackIndex, i ),
_ => throw new NotImplementedException()
}, x.TimeRange ) )
.ToImmutableArray();
}
public static IReadOnlyList<T> Resample<T>( this CompiledSampleBlock<T> source, int sampleRate,
MovieTime smoothingSize, IInterpolator<T>? interpolator )
{
if ( interpolator is null )
{
smoothingSize = default;
}
if ( sampleRate == source.SampleRate && smoothingSize <= 0d )
{
return source.Samples;
}
if ( ResampleCache<T>.Get( source, sampleRate, smoothingSize ) is { } cached )
{
return cached;
}
var sampleCount = sampleRate == source.SampleRate
? source.Samples.Length
: source.TimeRange.Duration.GetFrameCount( sampleRate );
var samples = new T[sampleCount];
var sourceSamples = source.Samples;
if ( sampleRate == source.SampleRate )
{
sourceSamples.CopyTo( samples );
}
else
{
for ( var i = 0; i < sampleCount; i++ )
{
var t = MovieTime.FromFrames( i, sampleRate );
samples[i] = sourceSamples.Sample( t, sampleRate, interpolator );
}
}
if ( smoothingSize <= 0d || interpolator is null )
{
ResampleCache<T>.Set( source, sampleRate, smoothingSize, samples );
return samples;
}
var smoothingPasses = smoothingSize.GetFrameCount( sampleRate );
T[] back = samples, front = [..samples];
for ( var pass = 0; pass < smoothingPasses; pass++ )
{
for ( var i = 0; i < sampleCount; i++ )
{
var prev = back[Math.Max( 0, i - 1 )];
var curr = back[i];
var next = back[Math.Min( sampleCount - 1, i + 1 )];
var prevCurr = interpolator.Interpolate( prev, curr, 0.5f );
var currNext = interpolator.Interpolate( curr, next, 0.5f );
front[i] = interpolator.Interpolate( prevCurr, currNext, 0.5f );
}
(back, front) = (front, back);
}
ResampleCache<T>.Set( source, sampleRate, smoothingSize, back );
return back;
}
}
using System.Diagnostics.CodeAnalysis;
using Sandbox.MovieMaker;
namespace Editor.MovieMaker.BlockDisplays;
#nullable enable
partial class BlockItem
{
public static BlockItem Create( TimelineTrack parent, ITrackBlock block, MovieTime offset )
{
var blockType = block.GetType();
var propertyType = (block as IPropertyBlock)?.PropertyType;
var inst = (BlockItem)Activator.CreateInstance( GetBlockItemType( blockType, propertyType ) )!;
try
{
inst.Initialize( parent, block, offset );
}
catch ( Exception ex )
{
Log.Error( ex );
BlockItemTypeCache[blockType] = typeof(DefaultBlockItem);
inst = new DefaultBlockItem();
inst.Initialize( parent, block, offset );
}
return inst;
}
[SkipHotload] private static Dictionary<Type, Type> BlockItemTypeCache { get; } = new();
[EditorEvent.Hotload]
private static void OnHotload()
{
BlockItemTypeCache.Clear();
}
private static Type GetBlockItemType( Type targetBlockType, Type? propertyType )
{
if ( BlockItemTypeCache.TryGetValue( targetBlockType, out var blockItemType ) ) return blockItemType;
var bestBlockItemType = typeof(DefaultBlockItem);
var bestScore = int.MaxValue;
foreach ( var typeDesc in EditorTypeLibrary.GetTypes<BlockItem>() )
{
var type = typeDesc.TargetType;
var baseDistance = 0;
if ( type.IsAbstract ) continue;
if ( type.IsGenericType )
{
if ( propertyType is null ) continue;
if ( !TryMakeGenericType( type, propertyType, out var newType ) )
{
continue;
}
type = newType;
baseDistance = 1;
}
var score = baseDistance + GetScore( type, targetBlockType, propertyType );
if ( score > bestScore ) continue;
bestBlockItemType = type;
bestScore = score;
}
BlockItemTypeCache[targetBlockType] = bestBlockItemType;
return bestBlockItemType;
}
private static bool TryMakeGenericType( Type trackPreviewType, Type propertyType,
[NotNullWhen( true )] out Type? newTrackPreviewType )
{
newTrackPreviewType = null;
if ( trackPreviewType.GetGenericArguments().Length != 1 )
{
return false;
}
try
{
newTrackPreviewType = trackPreviewType.MakeGenericType( propertyType );
return true;
}
catch
{
return false;
}
}
private static int GetScore( Type blockItemType, Type targetBlockType, Type? propertyType )
{
var score = int.MaxValue;
foreach ( var iFace in blockItemType.GetInterfaces() )
{
if ( !iFace.IsConstructedGenericType ) continue;
if ( iFace.GetGenericTypeDefinition() == typeof(IBlockItem<>) )
{
var iFaceTargetType = iFace.GetGenericArguments()[0];
score = Math.Min( score, GetDistance( iFaceTargetType, targetBlockType ) );
}
if ( iFace.GetGenericTypeDefinition() == typeof(IPropertyBlockItem<>) && propertyType != null )
{
var iFaceTargetType = iFace.GetGenericArguments()[0];
score = Math.Min( score, GetDistance( iFaceTargetType, propertyType ) );
}
}
return score;
}
private static int GetDistance( Type baseType, Type? derivedType )
{
if ( !baseType.IsAssignableFrom( derivedType ) ) return int.MaxValue;
if ( baseType.IsInterface && !derivedType.IsInterface ) return 1;
var distance = 0;
while ( baseType != derivedType && derivedType != null )
{
derivedType = derivedType.BaseType;
distance++;
}
return distance;
}
}
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Sandbox.MovieMaker;
using Sandbox.MovieMaker.Compiled;
namespace Editor.MovieMaker;
#nullable enable
public partial interface IProjectTrack : ITrack, IComparable<IProjectTrack>
{
Guid Id { get; }
MovieProject Project { get; }
new IProjectTrack? Parent { get; }
IReadOnlyList<IProjectTrack> Children { get; }
bool IsEmpty { get; }
int Order { get; }
void Remove();
IProjectTrack? GetChild( string name );
ICompiledTrack Compile( ICompiledTrack? compiledParent, bool headerOnly );
ITrack? ITrack.Parent => Parent;
IEnumerable<MovieResource> References { get; }
int IComparable<IProjectTrack>.CompareTo( IProjectTrack? other )
{
if ( ReferenceEquals( this, other ) )
{
return 0;
}
if ( other is null )
{
return 1;
}
var orderComparison = Order.CompareTo( other.Order );
if ( orderComparison != 0 ) return orderComparison;
return string.Compare( Name, other.Name, StringComparison.Ordinal );
}
}
internal interface IProjectTrackInternal : IProjectTrack
{
new IProjectTrackInternal? Parent { get; set; }
void AddChild( IProjectTrackInternal child );
void RemoveChild( IProjectTrackInternal child );
}
public abstract partial class ProjectTrack<T>( MovieProject project, Guid id, string name ) : IProjectTrackInternal
{
private readonly List<IProjectTrack> _children = new();
private bool _childrenChanged;
public MovieProject Project { get; } = project;
public Guid Id { get; } = id;
public string Name { get; set; } = name;
public Type TargetType { get; } = typeof(T);
public IProjectTrack? Parent { get; private set; }
public virtual IEnumerable<MovieResource> References => [];
public virtual bool IsEmpty => Children.Count == 0;
public virtual int Order => 0;
public IReadOnlyList<IProjectTrack> Children
{
get
{
UpdateChildren();
return _children;
}
}
public void Remove() => Project.RemoveTrackInternal( this );
public IProjectTrack? GetChild( string name ) => Children.FirstOrDefault( x => x.Name == name );
public abstract ICompiledTrack Compile( ICompiledTrack? compiledParent, bool headerOnly );
IProjectTrackInternal? IProjectTrackInternal.Parent
{
get => (IProjectTrackInternal?)Parent;
set => Parent = value;
}
void IProjectTrackInternal.AddChild( IProjectTrackInternal child )
{
if ( child.Parent != null )
{
throw new ArgumentException( "Track already has a parent!", nameof(child) );
}
child.Parent = this;
_children.Add( child );
_childrenChanged = true;
}
void IProjectTrackInternal.RemoveChild( IProjectTrackInternal child )
{
_children.Remove( child );
_childrenChanged = true;
}
private void UpdateChildren()
{
if ( !_childrenChanged ) return;
_childrenChanged = false;
_children.Sort();
}
}
public partial interface IProjectReferenceTrack : IProjectTrack, IReferenceTrack
{
public static IProjectReferenceTrack Create( MovieProject project, Guid id, string name, Type targetType )
{
var trackType = typeof(ProjectReferenceTrack<>).MakeGenericType( targetType );
return (IProjectReferenceTrack)Activator.CreateInstance( trackType, project, id, name )!;
}
new ProjectReferenceTrack<GameObject>? Parent { get; }
new Guid Id { get; }
new Guid? ReferenceId { get; set; }
IReferenceTrack<GameObject>? IReferenceTrack.Parent => Parent;
IProjectTrack? IProjectTrack.Parent => Parent;
Guid IReferenceTrack.Id => Id;
Guid IProjectTrack.Id => Id;
Guid? IReferenceTrack.ReferenceId => ReferenceId;
}
public partial class ProjectReferenceTrack<T>( MovieProject project, Guid id, string name )
: ProjectTrack<T>( project, id, name ), IProjectReferenceTrack, IReferenceTrack<T>
where T : class, IValid
{
public override int Order => -1000;
public new ProjectReferenceTrack<GameObject>? Parent => (ProjectReferenceTrack<GameObject>?)base.Parent;
public Guid? ReferenceId { get; set; }
public override ICompiledTrack Compile( ICompiledTrack? compiledParent, bool headerOnly ) =>
new CompiledReferenceTrack<T>( Id, Name, (CompiledReferenceTrack<GameObject>)compiledParent!, ReferenceId );
ITrack? ITrack.Parent => Parent;
}
public interface IProjectBlockTrack : IProjectTrack
{
MovieTimeRange TimeRange { get; }
IReadOnlyList<ITrackBlock> Blocks { get; }
}
public partial interface IProjectPropertyTrack : IPropertyTrack, IProjectBlockTrack
{
public static IProjectPropertyTrack Create( MovieProject project, Guid id, string name, Type targetType )
{
var trackType = typeof(ProjectPropertyTrack<>).MakeGenericType( targetType );
return (IProjectPropertyTrack)Activator.CreateInstance( trackType, project, id, name )!;
}
new IProjectTrack? Parent { get; }
new IReadOnlyList<IProjectPropertyBlock> Blocks { get; }
ITrack IPropertyTrack.Parent => Parent!;
/// <summary>
/// Add empty space from the start of <paramref name="timeRange"/>, with
/// the duration of <paramref name="timeRange"/>. Will split any blocks that
/// span the start time.
/// </summary>
bool Insert( MovieTimeRange timeRange );
/// <summary>
/// Remove the given <paramref name="timeRange"/>, then collapse the removed
/// time range so any blocks after the end of the range will start earlier.
/// </summary>
bool Remove( MovieTimeRange timeRange );
/// <summary>
/// Remove any blocks within the <paramref name="timeRange"/>, splitting any
/// blocks that span the start or end. This doesn't shift any blocks, so will
/// leave an empty region of time.
/// </summary>
bool Clear( MovieTimeRange timeRange );
/// <summary>
/// Adds a block, replacing any blocks that overlap its time range.
/// This will split any blocks that partially overlap.
/// </summary>
bool Add( MovieTimeRange timeRange, IPropertySignal signal );
/// <summary>
/// Adds a block, replacing any blocks that overlap its time range.
/// This will split any blocks that partially overlap.
/// </summary>
bool Add( IProjectPropertyBlock block );
bool AddRange( IEnumerable<IProjectPropertyBlock> blocks );
void SetBlocks( IReadOnlyList<IProjectPropertyBlock> blocks );
/// <summary>
/// Copies blocks that overlap the given <paramref name="timeRange"/> and returns
/// the copies.
/// </summary>
IReadOnlyList<IProjectPropertyBlock> Slice( MovieTimeRange timeRange );
IReadOnlyList<IProjectPropertyBlock> CreateSourceBlocks( ProjectSourceClip source );
IReadOnlyList<ITrackBlock> IProjectBlockTrack.Blocks => Blocks;
IProjectTrack? IProjectTrack.Parent => Parent;
ITrack? ITrack.Parent => Parent;
}
public sealed partial class ProjectPropertyTrack<T>( MovieProject project, Guid id, string name )
: ProjectTrack<T>( project, id, name ), IProjectPropertyTrack, IPropertyTrack<T>
{
private readonly List<PropertyBlock<T>> _blocks = new();
private bool _blocksChanged;
public MovieTimeRange TimeRange => (0d, Blocks.Select( x => x.TimeRange.End )
.DefaultIfEmpty()
.Max());
public IReadOnlyList<PropertyBlock<T>> Blocks
{
get
{
UpdateBlocks();
return _blocks;
}
}
public override bool IsEmpty => base.IsEmpty && Blocks.Count == 0;
IReadOnlyList<IProjectPropertyBlock> IProjectPropertyTrack.Blocks => Blocks;
public override ICompiledTrack Compile( ICompiledTrack? compiledParent, bool headerOnly )
{
var compiled = new CompiledPropertyTrack<T>( Name, compiledParent!, [] );
if ( headerOnly ) return compiled;
return compiled with { Blocks = [..Blocks.Select( x => x.Compile( this ) )] };
}
public bool TryGetValue( MovieTime time, [MaybeNullWhen( false )] out T value )
{
if ( Blocks.GetBlock( time ) is { } block )
{
value = block.GetValue( time );
return true;
}
value = default;
return false;
}
bool IPropertyTrack.TryGetValue( MovieTime time, out object? value )
{
if ( TryGetValue( time, out var val ) )
{
value = val;
return true;
}
value = null;
return false;
}
public bool Insert( MovieTimeRange timeRange )
{
return Clear( timeRange.Start )
| Shift( timeRange.Start, timeRange.Duration );
}
public bool Remove( MovieTimeRange timeRange )
{
return Clear( timeRange )
| Shift( timeRange.End, -timeRange.Duration );
}
private bool Shift( MovieTime from, MovieTime offset )
{
var changed = false;
for ( var i = 0; i < _blocks.Count; ++i )
{
var block = _blocks[i];
if ( block.TimeRange.Start >= from )
{
_blocks[i] += offset;
_blocksChanged = changed = true;
}
}
return changed;
}
public bool Clear( MovieTimeRange timeRange )
{
var overlaps = this.GetBlocks( timeRange ).ToArray();
if ( overlaps.Length == 0 ) return false;
foreach ( var overlap in overlaps )
{
_blocks.Remove( overlap );
if ( overlap.TimeRange.Start < timeRange.Start )
{
_blocks.Add( overlap.Slice( (overlap.TimeRange.Start, timeRange.Start) )! );
}
if ( overlap.TimeRange.End > timeRange.End )
{
_blocks.Add( overlap.Slice( (timeRange.End, overlap.TimeRange.End) )! );
}
}
_blocksChanged = true;
return true;
}
public bool Add( MovieTimeRange timeRange, PropertySignal<T> signal )
{
if ( timeRange.End < 0 ) return false;
timeRange = timeRange.ClampStart( 0 );
// Remove any overlaps
Clear( timeRange );
// Add to the end of _blocks, it'll get sorted later
_blocksChanged = true;
_blocks.AddRange( signal.AsBlocks( timeRange ) );
return true;
}
public bool Add( PropertyBlock<T> block )
{
if ( block.TimeRange.Start < 0 )
{
throw new ArgumentException( "Block can't have negative start time." );
}
if ( _blocks.Any( x => x.TimeRange.Contains( block.TimeRange ) && x.Signal.Equals( block.Signal ) ) )
{
return false;
}
Clear( block.TimeRange );
_blocksChanged = true;
_blocks.Add( block );
return true;
}
public bool AddRange( IEnumerable<PropertyBlock<T>> blocks )
{
var changed = false;
foreach ( var block in blocks )
{
changed |= Add( block );
}
return changed;
}
bool IProjectPropertyTrack.Add( MovieTimeRange timeRange, IPropertySignal signal ) =>
Add( timeRange, (PropertySignal<T>)signal );
bool IProjectPropertyTrack.Add( IProjectPropertyBlock block ) => Add( (PropertyBlock<T>)block );
bool IProjectPropertyTrack.AddRange( IEnumerable<IProjectPropertyBlock> blocks ) =>
AddRange( blocks.Cast<PropertyBlock<T>>() );
public void SetBlocks( IReadOnlyList<IProjectPropertyBlock> blocks )
{
_blocksChanged = true;
_blocks.Clear();
_blocks.AddRange( blocks.Cast<PropertyBlock<T>>() );
}
public IReadOnlyList<PropertyBlock<T>> Slice( MovieTimeRange timeRange )
{
return Blocks
.Where( x => x.TimeRange.Intersect( timeRange ) is { } intersection && (!intersection.IsEmpty || timeRange.IsEmpty) )
.Select( x => x.Slice( timeRange ) )
.OfType<PropertyBlock<T>>()
.ToImmutableArray();
}
IReadOnlyList<IProjectPropertyBlock> IProjectPropertyTrack.Slice( MovieTimeRange timeRange ) => Slice( timeRange );
IReadOnlyList<IProjectPropertyBlock> IProjectPropertyTrack.CreateSourceBlocks( ProjectSourceClip source ) =>
source.AsBlocks<T>( this );
private void UpdateBlocks()
{
if ( !_blocksChanged ) return;
_blocksChanged = false;
// Sort by time
_blocks.Sort( ( a, b ) => a.TimeRange.Start.CompareTo( b.TimeRange.Start ) );
// Merge touching blocks that have identical values at their interface
var comparer = EqualityComparer<T>.Default;
for ( var i = _blocks.Count - 2; i >= 0; --i )
{
var prev = _blocks[i];
var next = _blocks[i + 1];
if ( prev.TimeRange.End != next.TimeRange.Start ) continue;
var prevValue = prev.GetValue( prev.TimeRange.End );
var nextValue = next.GetValue( next.TimeRange.Start );
if ( !comparer.Equals( prevValue, nextValue ) )
{
continue;
}
var combinedTimeRange = prev.TimeRange.Union( next.TimeRange );
var combinedSignal = prev.Signal.HardCut( next.Signal, prev.TimeRange.End ).Reduce( combinedTimeRange );
_blocks[i] = new PropertyBlock<T>( combinedSignal, combinedTimeRange );
_blocks.RemoveAt( i + 1 );
}
}
}
using System.Linq;
using System.Text.Json.Serialization;
using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
#nullable enable
public abstract partial record PropertySignal : IPropertySignal
{
[JsonIgnore]
public abstract Type PropertyType { get; }
protected PropertySignal( PropertySignal copy )
{
// Empty so any lazily computed fields aren't copied
}
object? IPropertySignal.GetValue( MovieTime time ) => OnGetValue( time );
protected abstract object? OnGetValue( MovieTime time );
/// <summary>
/// Gets time ranges within the given <paramref name="timeRange"/> that have changing values.
/// For painting in the timeline.
/// </summary>
public virtual IEnumerable<MovieTimeRange> GetPaintHints( MovieTimeRange timeRange ) => [timeRange];
}
/// <summary>
/// A <see cref="IPropertySignal{T}"/> that can be composed with <see cref="PropertyOperation{T}"/>s,
/// and stored in a <see cref="IPropertyBlock{T}"/>.
/// </summary>
public abstract partial record PropertySignal<T>() : PropertySignal, IPropertySignal<T>
{
[JsonIgnore]
public sealed override Type PropertyType => typeof(T);
protected PropertySignal( PropertySignal<T> copy )
: base( copy )
{
// Empty so any lazily computed fields aren't copied
}
public abstract T GetValue( MovieTime time );
protected sealed override object? OnGetValue( MovieTime time ) => GetValue( time );
/// <summary>
/// Try to make a more minimal composition for this signal, optionally within a time range.
/// </summary>
/// <param name="start">Optional start time, we can discard any features before this if given.</param>
/// <param name="end">Optional end time, we can discard any features after this if given.</param>
public PropertySignal<T> Reduce( MovieTime? start = null, MovieTime? end = null )
{
return start >= end && !GetKeyframes( start.Value ).Any() ? GetValue( start.Value ) : OnReduce( start, end );
}
public PropertySignal<T> Reduce( MovieTimeRange timeRange ) =>
Reduce( timeRange.Start, timeRange.End );
public T[] Sample( MovieTimeRange timeRange, int sampleRate )
{
var sampleCount = timeRange.Duration.GetFrameCount( sampleRate );
var samples = new T[sampleCount];
for ( var i = 0; i < sampleCount; i++ )
{
var time = timeRange.Start + MovieTime.FromFrames( i, sampleRate );
samples[i] = GetValue( time );
}
return samples;
}
protected abstract PropertySignal<T> OnReduce( MovieTime? start, MovieTime? end );
public PropertySignal<T> Smooth( MovieTime size ) => size <= 0d ? this : OnSmooth( size );
protected virtual PropertySignal<T> OnSmooth( MovieTime size ) => this;
}
/// <summary>
/// Extension methods for creating and composing <see cref="IPropertySignal"/>s.
/// </summary>
// ReSharper disable once UnusedMember.Global
public static partial class PropertySignalExtensions;
using Sandbox;
using Editor;
using System;
using System.Linq;
namespace ChitChat.Editor;
public class DialogueSelector : Widget
{
public Action<SerializedProperty> onDialogueActionSelected;
public Action onSelectedDialogueRemoved;
private ScrollArea _dialogueArea;
private ListControlView _listView;
private Menu _menu;
private SerializedObject _serializedObject;
private SerializedCollection _serializedDialogueDatasArray;
private int _lastSelectedIndex = -1;
public DialogueSelector(Widget parent, DialogueData data) : base(parent)
{
WindowTitle = "Dialogue Selector";
Name = "Dialogue Selector";
Size = new Vector2(ChitChatEditorWindow.WINDOW_SIZE_X * 0.5f, ChitChatEditorWindow.WINDOW_SIZE_Y * 0.5f);
Layout = Layout.Column();
SetWindowIcon("list_alt");
_serializedObject = data.GetSerialized();
Rebuild();
}
public void SetData(DialogueData data) => _serializedObject = data.GetSerialized();
public void SelectLastest()
{
_listView.SelectItem(_lastSelectedIndex);
}
//TODO: Use this to see if something changed and update asset
public override void ChildValuesChanged(Widget source)
{
base.ChildValuesChanged(source);
}
public void Rebuild()
{
//Needs to save last selected index to reload it at that index
if(_listView.IsValid())
_lastSelectedIndex = _listView.SelectedIndex;
Layout.Clear(true);
SerializedProperty prop = _serializedObject.GetProperty(nameof(DialogueData.DialogueDatas));
//Scroll area
_dialogueArea = new ScrollArea(this);
_dialogueArea.Layout = Layout.Column();
_dialogueArea.Canvas = new Widget(_dialogueArea);
_dialogueArea.Canvas.Layout = Layout.Column();
_dialogueArea.Canvas.Layout.Alignment = TextFlag.LeftTop;
_dialogueArea.Canvas.Parent.Update();
if (prop.TryGetAsObject(out var obj))
{
//Dialogue list
_serializedDialogueDatasArray = (SerializedCollection)obj;
_listView = new ListControlView(prop, _serializedDialogueDatasArray, OnCreateUIListItem, false);
_listView.onItemSelected += OnItemSelected;
_listView.onItemRemoved += OnItemMovedOrRemoved;
_listView.onItemMoved += OnItemMovedOrRemoved;
_listView.onItemRightClicked += CreateDialogueActionMenuWithIndex;
_dialogueArea.Canvas.Layout.Add(_listView);
}
//Add dialogue action button
Widget addButtonContainer = new Widget(_dialogueArea.Canvas);
addButtonContainer.Layout = Layout.Row();
addButtonContainer.Layout.Alignment = TextFlag.Left;
addButtonContainer.Layout.AddSpacingCell(24);
IconButton addDialogueActionButton = new IconButton("add", CreateDialogueActionMenu);
addDialogueActionButton.Background = Theme.ControlBackground;
addButtonContainer.Layout.Add(addDialogueActionButton);
_dialogueArea.Canvas.Layout.Add(addButtonContainer);
_dialogueArea.Canvas.Layout.AddStretchCell();
Layout.Add(_dialogueArea);
}
private ControlWidget OnCreateUIListItem(SerializedProperty prop)
{
DialogueActionBase baseAction = prop.GetValue<DialogueActionBase>();
if (baseAction is SpeakAction)
{
return new SpeakActionControlWidget(prop);
}
else if (baseAction is EventAction)
{
return new EventActionControlWidget(prop);
}
else if (baseAction is ChoiceAction)
{
return new ChoiceActionControlWidget(prop);
}
return ControlWidget.Create(prop);
}
private void CreateDialogueActionMenu()
{
if(_menu.IsValid())
return;
CreateDialogueActionMenuWithIndex(_serializedDialogueDatasArray.Count() - 1);
}
private void CreateDialogueActionMenuWithIndex(int index)
{
if (_menu.IsValid())
return;
_menu = new ContextMenu();
_menu.AddHeading("Dialogue Actions");
_menu.AddOption("Speak Action", "record_voice_over", () => _serializedDialogueDatasArray.Add(index + 1, new SpeakAction(TemplateWindow.s_SpeakActionTemplate)));
_menu.AddSeparator();
_menu.AddOption("Event Action", "event", () => _serializedDialogueDatasArray.Add(index + 1, new EventAction(TemplateWindow.s_EventActionTemplate)));
_menu.AddSeparator();
_menu.AddOption("Choice Action", "alt_route", () => _serializedDialogueDatasArray.Add(index + 1, new ChoiceAction(TemplateWindow.s_ChoiceActionTemplate)));
_menu.OpenAtCursor(true);
_menu.MinimumWidth = ScreenRect.Width;
}
private void OnItemMovedOrRemoved(int index)
{
if(index == _listView.SelectedIndex)
onSelectedDialogueRemoved?.Invoke();
}
private void OnItemSelected(SerializedProperty prop)
{
onDialogueActionSelected?.Invoke(prop);
}
}
using Sandbox.MovieMaker;
namespace Editor.MovieMaker.BlockDisplays;
#nullable enable
public abstract partial class BlockItem : GraphicsItem
{
private ITrackBlock? _block;
public new TimelineTrack Parent { get; private set; } = null!;
public ITrackBlock Block
{
get => _block ?? throw new InvalidOperationException();
set
{
if ( ReferenceEquals( _block, value ) ) return;
if ( _block is IDynamicBlock oldBlock )
{
oldBlock.Changed -= Block_Changed;
}
_block = value;
if ( _block is IDynamicBlock newBlock )
{
newBlock.Changed += Block_Changed;
}
}
}
public MovieTime Offset { get; set; }
protected IProjectTrack Track => Parent.View.Track;
protected MovieTimeRange TimeRange => Block.TimeRange + Offset;
protected int DataHash => HashCode.Combine( Block, TimeRange.Duration, Width );
private void Initialize( TimelineTrack parent, ITrackBlock block, MovieTime offset )
{
base.Parent = Parent = parent;
Block = block;
Offset = offset;
}
private void Block_Changed() => Layout();
protected override void OnDestroy()
{
// To remove Changed event
Block = null!;
}
public void Layout()
{
var session = Parent.Session;
PrepareGeometryChange();
Position = new Vector2( session.TimeToPixels( TimeRange.Start ), 1f );
Size = new Vector2( session.TimeToPixels( TimeRange.Duration ), Parent.Height - 2f );
Update();
}
protected override void OnPaint()
{
Paint.SetBrushAndPen( Timeline.Colors.ChannelBackground.Lighten( Parent.View.IsLocked ? 0.2f : 0f ).WithAlpha( 0.75f ) );
Paint.DrawRect( LocalRect );
if ( Parent.View.IsLocked ) return;
Paint.ClearBrush();
Paint.SetPen( Color.White.WithAlpha( 0.1f ) );
Paint.DrawLine( LocalRect.BottomLeft, LocalRect.TopLeft );
Paint.DrawLine( LocalRect.BottomRight, LocalRect.TopRight );
}
}
public interface IBlockItem<T>;
public abstract class BlockItem<T> : BlockItem, IBlockItem<T>
where T : ITrackBlock
{
public new T Block => (T)base.Block;
}
public interface IPropertyBlockItem<T>;
public abstract class PropertyBlockItem<T> : BlockItem<IPropertyBlock<T>>, IPropertyBlockItem<T>;
using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
#nullable enable
public sealed partial class Session
{
private bool _isPlaying;
private bool _isLooping = true;
private float _timeScale = 1f;
private bool _syncPlayback = true;
private MovieTime? _lastPlayerPosition;
private bool _applyNextFrame;
private MovieTime _lastAppliedTime;
public bool IsOpenInEditor => Editor.Session == this;
public bool IsPlaying
{
get => IsEditorScene ? _isPlaying : Player.IsPlaying;
set
{
if ( IsEditorScene ) _isPlaying = value;
else Player.IsPlaying = value;
}
}
public bool IsLooping
{
get => IsEditorScene ? _isLooping : Player.IsLooping;
set
{
if ( IsEditorScene ) _isLooping = Cookies.IsLooping = value;
else Player.IsLooping = value;
}
}
public bool SyncPlayback
{
get => _syncPlayback;
set => _syncPlayback = Cookies.SyncPlayback = value;
}
public float TimeScale
{
get => IsEditorScene ? _timeScale : Player.TimeScale;
set
{
if ( IsEditorScene ) _timeScale = Cookies.TimeScale = value;
else Player.TimeScale = value;
}
}
public void ApplyFrame( MovieTime time )
{
_applyNextFrame = false;
if ( IsOpenInEditor && SyncPlayback )
{
foreach ( var player in Player.Scene.GetAllComponents<MoviePlayer>() )
{
if ( player == Player ) continue;
player.Position = time;
}
}
ApplyFrameCore( time );
}
private void ApplyFrameCore( MovieTime time )
{
Parent?.ApplyFrameCore( SequenceTransform * time );
foreach ( var view in TrackList.AllTracks )
{
view.ApplyFrame( time );
}
AdvanceAnimations( time - _lastAppliedTime );
_lastAppliedTime = time;
}
public void RefreshNextFrame()
{
_applyNextFrame = true;
}
private void PlaybackFrame()
{
if ( IsPlaying && IsEditorScene )
{
var targetTime = PlayheadTime + MovieTime.FromSeconds( RealTime.Delta * TimeScale );
// Handle looping / reaching end
if ( !IsRecording )
{
if ( LoopTimeRange is { } loopRange )
{
if ( targetTime <= loopRange.Start || targetTime >= loopRange.End )
{
targetTime = loopRange.Start;
}
}
else if ( targetTime >= Project.Duration && Project.Duration.IsPositive )
{
if ( IsLooping )
{
targetTime = MovieTime.Zero;
}
else
{
targetTime = Project.Duration;
IsPlaying = false;
}
}
}
PlayheadTime = targetTime;
}
else if ( _lastPlayerPosition is { } lastPlayerPosition && lastPlayerPosition != Player.Position )
{
// We're setting the backing field here because we don't want to call ApplyFrame / set the Player position,
// since we're reacting to the Player advancing time.
_playheadTime = lastPlayerPosition;
PlayheadChanged?.Invoke( PlayheadTime );
}
_lastPlayerPosition = Player.Position;
}
}
using Sandbox.MovieMaker;
using System.Linq;
using System.Reflection;
namespace Editor.MovieMaker;
#nullable enable
/// <summary>
/// Panel containing the track list.
/// </summary>
public sealed class ListPanel : MovieEditorPanel
{
public TrackListWidget TrackList { get; }
public ListPanel( MovieEditor parent, Session session )
: base( parent )
{
TrackList = new TrackListWidget( this, session );
Layout.Add( TrackList );
MinimumWidth = 300;
// File menu
var fileGroup = ToolBar.AddGroup( true );
var resourceIcon = typeof( MovieResource ).GetCustomAttribute<GameResourceAttribute>()!.Icon;
var fileDisplay = new ToolBarItemDisplay( "File", "folder", "Actions for saving / loading / importing movies, or switching player components." );
var fileAction = fileGroup.AddAction( fileDisplay, () =>
{
var menu = new Menu();
menu.AddHeading( "File" );
menu.AddOption( "New Movie", "note_add", Editor.SwitchToNewEmbedded );
menu.AddSeparator();
var openMenu = menu.AddMenu( "Open Movie", "file_open" );
var movies = ResourceLibrary.GetAll<MovieResource>().ToArray();
openMenu.AddOptions( movies, x => $"{x.ResourcePath}:{resourceIcon}", Editor.SwitchResource );
session.CreateImportMenu( menu );
menu.AddSeparator();
menu.AddOption( $"Save Movie", "save", parent.OnSave, shortcut: "CTRL+S" );
var saveAsMenu = menu.AddMenu( $"Save Movie As..", "save_as" );
var embed = saveAsMenu.AddOption( "Embedded", "attach_file", parent.SwitchToEmbedded );
embed.Checkable = true;
embed.Checked = session.Resource is EmbeddedMovieResource;
embed.ToolTip = "Store the movie inside this Movie Player component, embedded in the current scene or prefab.";
saveAsMenu.AddOption( "New Movie Resource", resourceIcon, parent.SaveFileAs );
menu.OpenAt( fileGroup.ScreenRect.BottomLeft );
} );
fileAction.ToolTip = "File menu for opening, importing, or saving movie projects.";
// MoviePlayer selection
var playerGroup = ToolBar.AddGroup( true );
var playerCombo = playerGroup.Layout.Add( new PlayerComboBox( session ) );
playerCombo.HorizontalSizeMode = SizeMode.CanGrow | SizeMode.Expand;
playerCombo.Bind( "Value" ).From(
() => session.Player,
value => session.Editor.Switch( value ) );
}
protected override void OnPaint()
{
Paint.SetBrushAndPen( Theme.TabBackground );
Paint.DrawRect( LocalRect );
}
}
file class PlayerComboBox : IconComboBox<MoviePlayer?>
{
private readonly Session _session;
public PlayerComboBox( Session session )
{
_session = session;
IconAspect = null;
}
protected override IEnumerable<MoviePlayer?> OnGetOptions() =>
_session.Player.Scene.IsValid ? _session.Player.Scene.GetAllComponents<MoviePlayer>() : [];
protected override string OnGetOptionTitle( MoviePlayer? option ) => option?.GameObject.Name ?? "None";
protected override void OnPaintOptionIcon( MoviePlayer? option, Rect rect )
{
Paint.DrawText( rect, OnGetOptionTitle( option ) );
}
protected override void OnCreateMenu( Menu menu )
{
base.OnCreateMenu( menu );
menu.AddSeparator();
menu.AddOption( "Create New Movie Player", "live_tv", _session.Editor.CreateNewPlayer );
}
}
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace EditorAllChat;
public class MessagesRequest
{
[JsonPropertyName( "success" )]
public bool Success { get; set; }
[JsonPropertyName( "messages" )]
public List<MessageObject> Messages { get; set; }
[JsonPropertyName( "count" )]
public int Count { get; set; }
[JsonPropertyName( "total_stored" )]
public int TotalStored { get; set; }
}
using System.Text.Json.Serialization;
namespace EditorAllChat;
public class MessageResponse
{
[JsonPropertyName( "success" )]
public bool Success { get; set; }
[JsonPropertyName( "message" )]
public string Message { get; set; }
[JsonPropertyName( "data" )]
public MessageObject Data { get; set; }
}
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace MANIFOLD.BHLib.Editor {
public class TimelineWidget : GraphicsView {
public class EventMarker : GraphicsItem {
public float time;
public List<AttackEvent> events;
public bool selected;
public int selectedIndex;
public Action<EventMarker> onSelected;
private TimelineWidget timeline;
public EventMarker(TimelineWidget widget) {
timeline = widget;
events = new List<AttackEvent>();
ZIndex = 0;
HoverEvents = true;
}
protected override void OnMousePressed(GraphicsMouseEvent e) {
if (e.LeftMouseButton) {
if (!selected) selectedIndex = 0;
else {
selectedIndex++;
if (selectedIndex >= events.Count) selectedIndex = 0;
}
selected = true;
Update();
onSelected?.Invoke(this);
e.Accepted = true;
}
}
protected override void OnPaint() {
base.OnPaint();
Paint.Antialiasing = false;
Color unselectedColor = Color.White.Darken(0.05f);
Color color;
if (selected) color = Color.Orange;
else if (Paint.HasMouseOver) color = Gizmo.Colors.Hovered;
else color = unselectedColor;
Paint.SetPen(color, 2);
var rect = LocalRect.Shrink(2);
Paint.DrawLine(new Vector2(rect.Left, rect.Bottom), new Vector2(rect.Left, rect.Top));
// Paint.DrawText(rect.Shrink(4, 0, 0, 6), string.Join("\n", events.Select(x => x.Name)), TextFlag.LeftBottom);
rect = rect.Shrink(4, 0, 0, 6);
for (int i = 0; i < events.Count; i++) {
Color textColor;
if (selected) {
textColor = i == selectedIndex ? color : unselectedColor;
} else {
textColor = color;
}
Paint.SetPen(textColor);
Paint.DrawText(rect, events[i].Name, TextFlag.LeftBottom);
rect = rect.Shrink(0, 0, 0, 9);
}
}
}
public class TimeAxis : GraphicsItem {
private TimelineWidget timeline;
public TimeAxis(TimelineWidget widget) {
timeline = widget;
ZIndex = 10;
HoverEvents = true;
}
protected override void OnPaint() {
base.OnPaint();
Paint.Antialiasing = false;
Paint.ClearPen();
Paint.SetBrush(Theme.ControlBackground);
Paint.DrawRect(LocalRect);
Paint.SetDefaultFont(7);
var rect = LocalRect.Shrink(1);
var zoom = timeline.ZoomFactor;
var spacing = 100 * zoom;
var lines = rect.Width / spacing;
var w = spacing;
var subdivisions = (int)(3 * zoom);
var subLineSpacing = w / subdivisions;
for (int i = 0; i < lines; i++) {
float xPos = rect.Left + w * i;
Paint.SetPen(Theme.Text.WithAlpha(0.5f));
Paint.DrawLine(new Vector2(xPos, rect.Bottom), new Vector2(xPos, rect.Bottom - 8));
Paint.DrawText(new Vector2(xPos, rect.Top), $"{i}");
Paint.SetPen(Theme.Text.WithAlpha(0.2f));
for (int j = 0; j < subdivisions; j++) {
var sublineX = w * i + subLineSpacing * j;
Paint.DrawLine(new Vector2(rect.Left + sublineX, rect.Bottom), new Vector2(rect.Left + sublineX, rect.Bottom - 4));
}
}
}
protected override void OnMousePressed(GraphicsMouseEvent e) {
base.OnMousePressed(e);
if (e.LeftMouseButton) {
timeline.ScrubTo(timeline.TimeFromPosition(e.LocalPosition.x));
}
}
}
public class Scrubber : GraphicsItem {
private TimelineWidget timeline;
public Scrubber(TimelineWidget widget) {
timeline = widget;
ZIndex = 20;
HoverEvents = true;
Cursor = CursorShape.SizeH;
Movable = true;
Selectable = true;
}
protected override void OnPaint() {
base.OnPaint();
Paint.Antialiasing = false;
Paint.ClearPen();
Paint.SetBrush(Theme.Green.WithAlpha(0.7f));
Paint.DrawRect(new Rect(0, new Vector2(LocalRect.Width, Theme.RowHeight + 1)));
Paint.SetPen(Theme.Green.WithAlpha(0.7f));
Paint.DrawLine(new Vector2(4, Theme.RowHeight + 1), new Vector2(4, LocalRect.Bottom));
}
protected override void OnMoved() {
base.OnMoved();
timeline.ScrubTo(timeline.TimeFromPosition(Position.x));
Position = Position.WithY(0);
Position = Position.WithX(MathF.Max(-4, Position.x));
}
}
private ITimeline timeline;
private float time;
private float zoomFactor;
private bool showHidden;
private TimeAxis timeAxis;
private Scrubber scrubber;
private List<EventMarker> markers;
private Dictionary<AttackEvent, EventMarker> eventToMarker;
private EventMarker selectedMarker;
public ITimeline Timeline {
get => timeline;
set {
timeline = value;
RebuildMarkers();
DoLayout();
}
}
public float Range { get; set; }
public float ZoomFactor {
get => zoomFactor;
set {
zoomFactor = value;
DoLayout();
timeAxis.Update();
scrubber.Update();
}
}
public float Time {
get => time;
set {
time = value;
DoLayout();
}
}
public bool ShowHidden {
get => showHidden;
set {
showHidden = value;
RebuildMarkers();
}
}
public AttackEvent SelectedEvent => selectedMarker?.events[selectedMarker.selectedIndex];
public Action<AttackEvent> OnEventSelected { get; set; }
public Action OnTimeScrubbed { get; set; }
public TimelineWidget(Widget parent = null) : base(parent) {
Antialiasing = false;
BilinearFiltering = false;
SceneRect = new Rect(0, Size);
HorizontalScrollbar = ScrollbarMode.Auto;
VerticalScrollbar = ScrollbarMode.Off;
MouseTracking = true;
Scale = 1;
zoomFactor = 1;
timeAxis = new TimeAxis(this);
Add(timeAxis);
scrubber = new Scrubber(this);
Add(scrubber);
markers = new List<EventMarker>();
eventToMarker = new Dictionary<AttackEvent, EventMarker>();
}
protected override void DoLayout() {
base.DoLayout();
var size = Size;
size.x = MathF.Max(size.x, PositionFromTime(Range + 3));
SceneRect = new Rect(0, size);
timeAxis.Size = new Vector2(size.x, Theme.RowHeight);
scrubber.Size = new Vector2(9, size.y);
var rect = SceneRect;
rect.Top = timeAxis.SceneRect.Bottom;
scrubber.Position = scrubber.Position.WithX(PositionFromTime(Time) - 3).SnapToGrid(1f);
foreach (var marker in markers) {
var markerRect = rect;
markerRect.Left = PositionFromTime(marker.time);
markerRect.Width = 80;
marker.SceneRect = markerRect;
}
}
protected override void OnWheel(WheelEvent e) {
e.Accept();
}
public void RebuildMarkers() {
var previousSelectedEvent = SelectedEvent;
foreach (var marker in markers) {
marker.Destroy();
}
markers.Clear();
eventToMarker.Clear();
if (timeline != null) {
foreach (var evt in Timeline.Events) {
if (!ShowHidden && evt.Hidden) continue;
AddEvent(evt);
}
}
DoLayout();
if (previousSelectedEvent != null) {
if (eventToMarker.TryGetValue(previousSelectedEvent, out EventMarker marker)) {
marker.selected = true;
marker.selectedIndex = marker.events.IndexOf(previousSelectedEvent);
try {
marker.Update();
} catch {
// ignored
}
}
}
}
public void ScrubTo(float time) {
Time = time;
OnTimeScrubbed?.Invoke();
}
public float PositionFromTime(float time) {
return 100 * ZoomFactor * time;
}
public float TimeFromPosition(float position) {
return (ZoomFactor / 100) * position;
}
private void AddEvent(AttackEvent evt) {
var existing = markers.FirstOrDefault(x => x.time.AlmostEqual(evt.Time));
if (existing != null) {
existing.events.Add(evt);
eventToMarker.Add(evt, existing);
} else {
EventMarker marker = new EventMarker(this);
marker.time = evt.Time;
marker.events.Add(evt);
marker.onSelected = OnMarkerSelected;
Add(marker);
markers.Add(marker);
eventToMarker.Add(evt, marker);
}
}
private void OnMarkerSelected(EventMarker marker) {
if (marker != selectedMarker && selectedMarker != null) {
selectedMarker.selected = false;
try {
selectedMarker.Update();
} catch {
// ignored
}
}
selectedMarker = marker;
OnEventSelected?.Invoke(SelectedEvent);
}
}
}
namespace Braxnet;
public class CompletionsCapability
{
}
using System.Text.Json.Serialization;
namespace Braxnet;
public class PromptsCapability
{
[JsonPropertyName( "listChanged" )] public bool ListChanged { get; set; }
}
using System.Text.Json.Serialization;
namespace Braxnet;
public class RootsCapability
{
[JsonPropertyName( "listChanged" )] public bool ListChanged { get; set; }
}
using System.Threading.Tasks;
using System.Text.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System;
using Sandbox;
using Editor;
using FileSystem = Editor.FileSystem;
namespace Braxnet.Commands;
[MCPCommand( "resources/list" )]
public class ListResourcesCommand : IMCPCommand
{
public string Name => "resources/list";
public async Task<object> ExecuteAsync( JsonRpcRequest request, string sessionId, string protocolVersion )
{
var resources = new List<Resource>();
try
{
var projectDir = Path.Combine( FileSystem.Mounted.GetFullPath( "." ), ".." );
projectDir = Path.GetFullPath( projectDir );
var files = Directory.GetFiles( projectDir, "*.*", SearchOption.AllDirectories )
.Where( f => !f.Contains( ".git" ) && !f.Contains( ".vscode" ) && !f.Contains( ".idea" ) )
.Take( 100 );
foreach ( var file in files )
{
var relativePath = Path.GetRelativePath( projectDir, file );
var fileInfo = new FileInfo( file );
resources.Add( new Resource
{
Uri = $"file://{file.Replace( '\\', '/' )}",
Name = relativePath.Replace( '\\', '/' ),
Title = Path.GetFileName( file ),
Description = $"Project file: {relativePath}",
MimeType = MCPServer.GetMimeType( file ),
Size = fileInfo.Length
} );
}
}
catch ( Exception ex )
{
Log.Error( $"Error listing resources: {ex.Message}" );
}
return new ListResourcesResult { Resources = resources };
}
}
[MCPCommand( "resources/read" )]
public class ReadResourceCommand : IMCPCommand
{
public string Name => "resources/read";
public async Task<object> ExecuteAsync( JsonRpcRequest request, string sessionId, string protocolVersion )
{
var contents = new List<ResourceContents>();
if ( request.Params.HasValue )
{
var paramsObj = JsonSerializer.Deserialize<Dictionary<string, object>>(
request.Params.Value.GetRawText(), MCPServer.JsonOptions );
if ( paramsObj?.ContainsKey( "uri" ) == true )
{
var uriStr = paramsObj["uri"]?.ToString();
if ( !string.IsNullOrEmpty( uriStr ) && uriStr.StartsWith( "file://" ) )
{
var filePath = uriStr.Substring( 7 );
try
{
if ( File.Exists( filePath ) )
{
var text = await File.ReadAllTextAsync( filePath );
contents.Add( new ResourceContents
{
Uri = uriStr, Text = text, MimeType = MCPServer.GetMimeType( filePath )
} );
}
}
catch ( Exception ex )
{
Log.Error( $"Error reading file {filePath}: {ex.Message}" );
throw new Exception( $"Could not read file: {ex.Message}" );
}
}
}
}
return new ReadResourceResult { Contents = contents };
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Sandbox;
namespace Braxnet.Commands.Tools;
[MCPTool( "get_children", "Get Children Tool",
"Get the children of a GameObject in the current scene" )]
public class GetChildrenTool : IMCPTool
{
public string Name => "get_children";
public string Title => "Get Children Tool";
public string Description => "Get the children of a GameObject in the current scene";
public JsonElement InputSchema => JsonSerializer.SerializeToElement( new
{
type = "object",
properties =
new { id = new { type = "string", description = "The ID of the GameObject to get children for" } },
required = new[] { "id" }
} );
public JsonElement OutputSchema => default;
public async Task<CallToolResult> ExecuteAsync( Dictionary<string, object> arguments, string sessionId )
{
var result = new CallToolResult();
if ( !arguments.TryGetValue( "id", out var idObj ) || idObj is not string id )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = "Invalid or missing 'id' argument." } );
return result;
}
if ( string.IsNullOrEmpty( id ) )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = "GameObject ID cannot be empty." } );
return result;
}
if ( !Guid.TryParse( id, out var guid ) )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = "Invalid GameObject ID format." } );
return result;
}
var gameObject = Game.ActiveScene.Directory.FindByGuid( guid );
if ( gameObject == null )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = $"GameObject with ID '{id}' not found." } );
return result;
}
await GameTask.MainThread(); // Ensure this runs on the main thread
var childrenList = gameObject.Children.Select( child => new
{
Id = child.Id,
Name = child.Name,
LocalPosition = child.LocalPosition,
LocalRotation = child.LocalRotation,
LocalScale = child.LocalScale,
Components = child.Components.Count,
ChildrenCount = child.Children.Count,
Enabled = child.Enabled,
Tags = child.Tags.ToList(),
} ).ToList();
result.StructuredContent = new { Children = childrenList };
return result;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Editor;
using Sandbox;
namespace Braxnet.Commands.Tools;
[MCPTool( "get_game_objects", "List Game Objects",
"List all GameObjects in the current scene" )]
public class ListGameObjectsTool : IMCPTool
{
public string Name => "get_game_objects";
public string Title => "List Game Objects";
public string Description => "List all GameObjects in the current scene";
public JsonElement InputSchema => JsonSerializer.SerializeToElement( new
{
type = "object", properties = new { }, required = Array.Empty<string>()
} );
public JsonElement OutputSchema => JsonSerializer.SerializeToElement( new
{
type = "object",
properties = new
{
SceneFile = new { type = "string", description = "Path to the scene file" },
GameObjects = new
{
type = "array",
items = new
{
type = "object",
properties = new
{
Id = new { type = "string", description = "GameObject ID" },
Name = new { type = "string", description = "GameObject name" },
Position =
new
{
type = "object",
properties =
new
{
x = new { type = "number" },
y = new { type = "number" },
z = new { type = "number" }
}
},
Rotation =
new
{
type = "object",
properties =
new
{
pitch = new { type = "number" },
yaw = new { type = "number" },
roll = new { type = "number" }
}
},
Components = new { type = "integer", description = "Number of components" },
ChildrenCount =
new { type = "integer", description = "Number of child GameObjects" },
Enabled =
new { type = "boolean", description = "Is the GameObject enabled?" },
Tags = new
{
type = "array",
items = new { type = "string" },
description = "List of tags assigned to the GameObject"
}
},
required = new[]
{
"Id", "Name", "Position", "Rotation", "Components", "ChildrenCount", "Enabled", "Tags"
}
}
}
},
required = new[] { "SceneFile", "GameObjects" }
} );
public async Task<CallToolResult> ExecuteAsync( Dictionary<string, object> arguments, string sessionId )
{
var result = new CallToolResult();
try
{
var rootGameObjects = SceneEditorSession.Active.Scene.Children;
if ( rootGameObjects == null || rootGameObjects.Count == 0 )
{
result.Content.Add( new TextContent { Text = "No GameObjects found in the scene." } );
return result;
}
result.Content.Add( new TextContent
{
Text = $"Found {rootGameObjects.Count} root GameObjects in the scene."
} );
var gameObjectsList = new List<GameObjectInfo>();
foreach ( var gameObject in rootGameObjects )
{
gameObjectsList.Add( new GameObjectInfo
{
Id = gameObject.Id,
Name = gameObject.Name,
Position = gameObject.WorldPosition,
Rotation = gameObject.WorldRotation,
Components = gameObject.Components.Count,
ChildrenCount = gameObject.Children.Count,
Enabled = gameObject.Enabled,
Tags = gameObject.Tags.ToList(),
} );
}
result.StructuredContent = new
{
SceneFile = SceneEditorSession.Active.Scene.Source.ResourcePath, GameObjects = gameObjectsList
};
}
catch ( Exception ex )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = $"Error listing GameObjects: {ex.Message}" } );
}
return result;
}
public class GameObjectInfo
{
public Guid Id { get; set; }
public string Name { get; set; }
public Vector3 Position { get; set; }
public Angles Rotation { get; set; }
public int Components { get; set; }
public int ChildrenCount { get; set; }
public bool Enabled { get; set; }
public List<string> Tags { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Editor;
using Sandbox;
namespace Braxnet.Commands.Tools;
[MCPTool( "duplicate_game_object", "Duplicate Game Object",
"Duplicate a GameObject in the current scene" )]
public class DuplicateGameObjectTool : IMCPTool
{
public string Name => "duplicate_game_object";
public string Title => "Duplicate Game Object";
public string Description => "Duplicate a GameObject in the current scene";
public JsonElement InputSchema => JsonSerializer.SerializeToElement( new
{
type = "object",
properties = new
{
id = new { type = "string", description = "The ID of the GameObject to duplicate" },
position =
new
{
type = "object",
properties =
new
{
x = new { type = "number", description = "X position" },
y = new { type = "number", description = "Y position" },
z = new { type = "number", description = "Z position" }
},
description = "Position to place the duplicated GameObject"
},
rotation = new
{
type = "object",
properties =
new
{
pitch = new { type = "number", description = "Pitch rotation" },
yaw = new { type = "number", description = "Yaw rotation" },
roll = new { type = "number", description = "Roll rotation" }
},
description = "Rotation of the duplicated GameObject"
},
parentId = new
{
type = "string",
description = "Optional parent GameObject ID to set the duplicated GameObject's parent"
}
},
required = new[] { "id" }
} );
public JsonElement OutputSchema => default;
public async Task<CallToolResult> ExecuteAsync( Dictionary<string, object> arguments, string sessionId )
{
var result = new CallToolResult();
if ( !arguments.TryGetValue( "id", out var idObj ) || idObj is not string id )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = "Invalid or missing 'id' argument." } );
return result;
}
if ( string.IsNullOrEmpty( id ) )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = "GameObject ID cannot be empty." } );
return result;
}
if ( !Guid.TryParse( id, out var guid ) )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = "Invalid GameObject ID format." } );
return result;
}
var gameObject = SceneEditorSession.Active.Scene.Directory.FindByGuid( guid );
if ( gameObject == null )
{
result.IsError = true;
result.Content.Add( new TextContent { Text = $"GameObject with ID '{id}' not found." } );
return result;
}
await GameTask.MainThread(); // Ensure this runs on the main thread
GameObject duplicate = null;
using ( SceneEditorSession.Active.UndoScope( "Duplicate GameObject" )
.WithGameObjectCreations().Push() )
{
var position = arguments.GetValueOrDefault( "position" ) as JsonElement?;
var rotation = arguments.GetValueOrDefault( "rotation" ) as JsonElement?;
var parentId = arguments.GetValueOrDefault( "parentId" ) as string;
duplicate = gameObject.Clone();
if ( position.HasValue && position.Value.ValueKind == JsonValueKind.Object )
{
var posX = position.Value.GetProperty( "x" ).GetSingle();
var posY = position.Value.GetProperty( "y" ).GetSingle();
var posZ = position.Value.GetProperty( "z" ).GetSingle();
duplicate.WorldPosition = new Vector3( posX, posY, posZ );
}
if ( rotation.HasValue && rotation.Value.ValueKind == JsonValueKind.Object )
{
var pitch = rotation.Value.GetProperty( "pitch" ).GetSingle();
var yaw = rotation.Value.GetProperty( "yaw" ).GetSingle();
var roll = rotation.Value.GetProperty( "roll" ).GetSingle();
duplicate.WorldRotation = new Angles( pitch, yaw, roll );
}
if ( !string.IsNullOrEmpty( parentId ) && Guid.TryParse( parentId, out var parentGuid ) )
{
var parentObject = SceneEditorSession.Active.Scene.Directory.FindByGuid( parentGuid );
if ( parentObject != null )
{
duplicate.SetParent( parentObject );
}
else
{
result.IsError = true;
result.Content.Add(
new TextContent { Text = $"Parent GameObject with ID '{parentId}' not found." } );
return result;
}
}
}
result.Content.Add( new TextContent { Text = $"Successfully duplicated GameObject: {gameObject.Name}" } );
result.StructuredContent = new
{
duplicatedGameObjectId = duplicate.Id,
duplicatedGameObjectName = duplicate.Name,
duplicatedGameObjectPosition =
new[] { duplicate.WorldPosition.x, duplicate.WorldPosition.y, duplicate.WorldPosition.z },
duplicatedGameObjectRotation = new[]
{
duplicate.WorldRotation.Pitch(), duplicate.WorldRotation.Yaw(), duplicate.WorldRotation.Roll()
}
};
return result;
}
}
using System;
using System.Numerics;
using Editor;
using IconRenderer;
using Sandbox;
internal class IconRendering : SceneRenderingWidget
{
public readonly CameraComponent camera;
public readonly SkinnedModelRenderer model;
public readonly DirectionalLight light;
public readonly SpriteRenderer mockBackground;
private float hoverTime;
public IconRendering( string modelName, string background ) : base( null )
{
Size = 512;
Scene = Scene.CreateEditorScene();
using ( Scene.Push() )
{
{
camera = new GameObject( true, "camera" ).GetOrAddComponent<CameraComponent>( false );
camera.BackgroundColor = Color.Black;
camera.Enabled = true;
}
{
light = new GameObject( true, "light" ).GetOrAddComponent<DirectionalLight>( false );
light.LightColor = Color.White;
light.Enabled = true;
}
{
model = new GameObject( true, "model" ).GetOrAddComponent<SkinnedModelRenderer>( false );
model.Model = Model.Load( modelName ?? "models/error.vmdl" );
model.Enabled = true;
}
{
mockBackground = new GameObject( true, "mockBackground" ).GetOrAddComponent<SpriteRenderer>( false );
mockBackground.Texture = Texture.Load( background );
mockBackground.Size = new Vector2( 512, 512 );
mockBackground.LocalPosition = new Vector3( -256, 0, 0 );
mockBackground.Enabled = true;
}
}
}
public override void OnDestroyed()
{
base.OnDestroyed();
Scene?.Destroy();
Scene = null;
}
public override void PreFrame()
{
Scene.EditorTick( RealTime.Now, RealTime.Delta );
}
}
namespace AltCurves;
/// <summary>
/// User options for snapping values to a grid
/// </summary>
public enum ValueSnapOptions
{
[Title( "0.1" )]
Tenth,
[Title( "0.5" )]
Half,
[Title( "1" )]
One,
[Title( "2" )]
Two,
[Title( "5" )]
Five,
[Title( "10" )]
Ten,
[Title( "50" )]
Fifty,
[Title( "100" )]
Hundred,
[Title( "Snap-To-Gridline" )]
Gridlines,
[Title( "User-provided amount" )]
Custom,
};
using Editor;
using System;
namespace AltCurves.GraphicsItems;
public partial class EditableAltCurve : GraphicsItem
{
/// <summary>
/// The draggable, invisible handle that represents a keyframe on the curve
/// The actual visible keyframe position can vary with snapping
/// </summary>
public class DragHandle : GraphicsItem
{
/// <summary>
/// Underlying keyframe that we're positioned for, updated on dragging
/// </summary>
public AltCurve.Keyframe Keyframe
{
get => _keyframe;
set
{
_keyframe = value;
StartDragKeyframe = _keyframe;
Position = _transform.CurveToWidgetPosition( new( _keyframe.Time, _keyframe.Value ) );
Update();
}
}
private AltCurve.Keyframe _keyframe;
/// <summary>
/// The keyframe when we started the last dragging operation
/// </summary>
public AltCurve.Keyframe StartDragKeyframe { get; private set; }
public CurveWidgetTransform Transform
{
get => _transform;
set
{
_transform = value;
Position = _transform.CurveToWidgetPosition( new( Keyframe.Time, Keyframe.Value ) );
Update();
}
}
private CurveWidgetTransform _transform;
/// <summary>
/// Called when we're left-clicked for selection logic
/// Params are index, isShift, isCtrl, isAlt
/// </summary>
public Action<int, bool, bool, bool> OnMouseDown { get; set; } = null;
public Action<int, bool, bool, bool> OnMouseUp { get; set; } = null;
/// <summary>
/// Called during drag operations, after the Keyframe has been updated for our new position
/// Parameter is the index and the pre-drag keyframe.
/// </summary>
public Action<int> OnDragging { get; set; } = null;
/// <summary>
/// Called when releasing the mouse, the drag has completed
/// </summary>
public Action<int> OnDragComplete { get; set; } = null;
private readonly int _index;
private Vector2 _mouseDownPos;
public DragHandle( int index, CurveWidgetTransform transform, AltCurve.Keyframe keyframe, GraphicsItem parent ) : base( parent )
{
_index = index;
_keyframe = keyframe;
StartDragKeyframe = keyframe;
HandlePosition = new( 0.5f );
Cursor = CursorShape.Finger;
Movable = true;
Clip = true;
HoverEvents = true;
Size = new( 14.0f );
_transform = transform;
Position = _transform.CurveToWidgetPosition( new( Keyframe.Time, Keyframe.Value ) );
}
protected override void OnMoved()
{
base.OnMoved();
_keyframe = _keyframe with { Time = _transform.WidgetToCurveX( Position.x ), Value = _transform.WidgetToCurveY( Position.y ) };
OnDragging?.Invoke( _index );
}
protected override void OnMousePressed( GraphicsMouseEvent e )
{
if ( e.LeftMouseButton )
{
StartDragKeyframe = Keyframe;
_mouseDownPos = e.ScreenPosition;
OnMouseDown?.Invoke( _index, e.HasShift, e.HasCtrl, e.HasAlt );
}
// Begin drag
base.OnMousePressed( e );
}
protected override void OnMouseReleased( GraphicsMouseEvent e )
{
base.OnMouseReleased( e );
// Either the user moved us from the starting location and we're completing a drag,
// or they just clicked us normally and the mouse up action should trigger
if ( e.LeftMouseButton )
{
if ( e.ScreenPosition != _mouseDownPos )
{
OnDragComplete?.Invoke( _index );
}
else
{
OnMouseUp?.Invoke( _index, e.HasShift, e.HasCtrl, e.HasAlt );
}
}
}
}
}
using Editor;
public static class MyEditorMenu
{
[Menu( "Editor", "Crosshair Maker/My Menu Option" )]
public static void OpenMyMenu()
{
EditorUtility.DisplayDialog( "It worked!", "This is being called from your library's editor code!" );
}
}
using System;
namespace Editor;
/// <summary>
/// Blender-style radial menu with button boxes arranged in a circle.
/// Opens on Ctrl+Right Click.
/// </summary>
public class PieMenu : Widget
{
public class PieOption
{
public string Text { get; set; }
public string Icon { get; set; }
public Action Action { get; set; }
public bool Enabled { get; set; } = true;
}
private List<PieOption> _options = new();
private int _hoveredIndex = -1;
private Vector2 _centerPosition;
private float _currentIndicatorAngle = -90f; // Start at top
private float _targetIndicatorAngle = -90f;
public float Radius { get; set; } = 180f;
public float CenterRadius { get; set; } = 30f;
public float CenterRingThickness { get; set; } = 10f;
public float ButtonPadding { get; set; } = 20f;
public float ButtonHeight { get; set; } = 28f;
// s&box themed colors
public Color ButtonColor { get; set; } = Theme.ButtonBackground;
public Color ButtonHoverColor { get; set; } = Theme.Blue;
public Color CenterRingColor { get; set; } = Theme.ControlBackground.Darken( 0.2f );
public Color IndicatorColor { get; set; } = Theme.Yellow;
// Track which button this menu should respond to
public MouseButtons TriggerButton { get; set; } = MouseButtons.Forward;
public PieMenu( Widget parent = null ) : base( parent )
{
WindowFlags = WindowFlags.Popup | WindowFlags.FramelessWindowHint | WindowFlags.NoDropShadowWindowHint;
TranslucentBackground = true;
}
public PieOption AddOption( string text, string icon = null, Action action = null )
{
var option = new PieOption
{
Text = text,
Icon = icon,
Action = action
};
_options.Add( option );
return option;
}
public void Clear()
{
_options.Clear();
_hoveredIndex = -1;
}
public void OpenAtCursor()
{
OpenAt( Application.CursorPosition );
}
public void OpenAt( Vector2 position )
{
if ( _options.Count == 0 ) return;
Paint.SetDefaultFont( 10, 500 );
float maxButtonWidth = 0;
foreach ( var option in _options )
{
var textSize = Paint.MeasureText( option.Text );
float buttonWidth = textSize.x + ButtonPadding * 2;
maxButtonWidth = Math.Max( maxButtonWidth, buttonWidth );
}
var size = (Radius + maxButtonWidth + 40) * 2;
var widgetSize = new Vector2( size );
Size = widgetSize;
MinimumSize = widgetSize;
MaximumSize = widgetSize;
Position = position - (widgetSize / 2f);
_centerPosition = widgetSize / 2;
_hoveredIndex = -1;
Show();
Focus();
MouseTracking = true;
var localPos = Application.CursorPosition;
UpdateHoveredOption( localPos );
}
protected override void OnPaint()
{
base.OnPaint();
if ( _options.Count == 0 ) return;
Paint.Antialiasing = true;
var center = LocalRect.Center;
int optionCount = _options.Count;
float angleStep = 360f / optionCount;
float startAngle = -90f;
// Draw button boxes positioned around the circle
for ( int i = 0; i < optionCount; i++ )
{
var option = _options[i];
float angle = startAngle + (i * angleStep);
float angleRad = angle * MathF.PI / 180f;
// Calculate button position
var buttonCenter = center + new Vector2(
MathF.Cos( angleRad ) * Radius,
MathF.Sin( angleRad ) * Radius
);
// Measure text width to size button
Paint.SetDefaultFont( 10, 500 );
var textSize = Paint.MeasureText( option.Text );
// Button width = text width + padding
float buttonWidth = textSize.x + ButtonPadding * 2;
var buttonRect = new Rect(
buttonCenter.x - buttonWidth / 2,
buttonCenter.y - ButtonHeight / 2,
buttonWidth,
ButtonHeight
);
var buttonColor = i == _hoveredIndex ? ButtonHoverColor : ButtonColor;
Paint.ClearPen();
Paint.SetBrush( buttonColor );
Paint.DrawRect( buttonRect, 3 );
// Draw text - centered
Paint.SetPen( Color.White );
Paint.SetDefaultFont( 10, 500 );
var textRect = buttonRect.Shrink( 10, 0 );
Paint.DrawText( textRect, option.Text, TextFlag.Center );
}
// Draw center ring (donut shape)
Paint.ClearPen();
Paint.SetBrush( CenterRingColor );
DrawRing( center, CenterRadius - CenterRingThickness, CenterRadius );
if ( _hoveredIndex >= 0 && _hoveredIndex < _options.Count )
{
// Much faster interpolation for snappier feel
float angleDiff = _targetIndicatorAngle - _currentIndicatorAngle;
// Handle wraparound (shortest path)
if ( angleDiff > 180f ) angleDiff -= 360f;
if ( angleDiff < -180f ) angleDiff += 360f;
_currentIndicatorAngle += angleDiff * 0.6f; // Increased from 0.3f for faster response
// Normalize angle
if ( _currentIndicatorAngle < 0 ) _currentIndicatorAngle += 360f;
if ( _currentIndicatorAngle >= 360f ) _currentIndicatorAngle -= 360f;
// Draw a colored arc segment on the ring
float arcSpan = 40f;
Paint.SetBrush( IndicatorColor );
DrawArcSegment( center, CenterRadius - CenterRingThickness, CenterRadius, _currentIndicatorAngle - arcSpan / 2, _currentIndicatorAngle + arcSpan / 2 );
}
}
/// <summary>
/// Draw a ring (donut shape)
/// </summary>
private void DrawRing( Vector2 center, float innerRadius, float outerRadius )
{
const int segments = 64;
var points = new List<Vector2>();
// Outer circle
for ( int i = 0; i <= segments; i++ )
{
float angle = (i / (float)segments) * 360f;
float rad = angle * MathF.PI / 180f;
points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * outerRadius );
}
// Inner circle (reverse)
for ( int i = segments; i >= 0; i-- )
{
float angle = (i / (float)segments) * 360f;
float rad = angle * MathF.PI / 180f;
points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * innerRadius );
}
Paint.DrawPolygon( points.ToArray() );
}
/// <summary>
/// Draw an arc segment on a ring
/// </summary>
private void DrawArcSegment( Vector2 center, float innerRadius, float outerRadius, float startAngle, float endAngle )
{
const int segments = 20;
float angleRange = endAngle - startAngle;
int segmentCount = Math.Max( 2, (int)(segments * angleRange / 360f) );
var points = new List<Vector2>();
// Inner arc
for ( int i = 0; i <= segmentCount; i++ )
{
float angle = startAngle + (angleRange * i / segmentCount);
float rad = angle * MathF.PI / 180f;
points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * innerRadius );
}
// Outer arc (reverse)
for ( int i = segmentCount; i >= 0; i-- )
{
float angle = startAngle + (angleRange * i / segmentCount);
float rad = angle * MathF.PI / 180f;
points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * outerRadius );
}
Paint.DrawPolygon( points.ToArray() );
}
protected override void OnMouseMove( MouseEvent e )
{
base.OnMouseMove( e );
UpdateHoveredOption( e.LocalPosition );
}
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
e.Accepted = true;
}
protected override void OnMouseReleased( MouseEvent e )
{
// Check if this is the button that opened this menu
if ( e.Button == TriggerButton )
{
ExecuteHoveredOptionAndClose();
e.Accepted = true;
return;
}
base.OnMouseReleased( e );
}
/// <summary>
/// Execute the currently hovered option and close the menu
/// </summary>
public void ExecuteHoveredOptionAndClose()
{
if ( _hoveredIndex >= 0 && _hoveredIndex < _options.Count )
{
var option = _options[_hoveredIndex];
if ( option.Enabled )
{
option.Action?.Invoke();
}
}
Close();
}
protected override void OnKeyPress( KeyEvent e )
{
base.OnKeyPress( e );
if ( e.Key == KeyCode.Escape )
{
Close();
e.Accepted = true;
}
}
private void UpdateHoveredOption( Vector2 mousePos )
{
var center = LocalRect.Center;
var offset = mousePos - center;
float distance = offset.Length;
// More forgiving selection area
if ( distance < 10f || distance > Radius + 250 )
{
if ( _hoveredIndex != -1 )
{
_hoveredIndex = -1;
Update();
}
return;
}
// Calculate angle from center to mouse
float mouseAngle = MathF.Atan2( offset.y, offset.x ) * 180f / MathF.PI;
// Find the closest button by angular distance
int optionCount = _options.Count;
float angleStep = 360f / optionCount;
float startAngle = -90f;
int closestIndex = -1;
float closestDistance = float.MaxValue;
for ( int i = 0; i < optionCount; i++ )
{
// Calculate the angle of this button's center
float buttonAngle = startAngle + (i * angleStep);
// Normalize both angles to 0-360
float normMouseAngle = mouseAngle;
if ( normMouseAngle < 0 ) normMouseAngle += 360f;
float normButtonAngle = buttonAngle;
if ( normButtonAngle < 0 ) normButtonAngle += 360f;
// Calculate angular distance (shortest path around the circle)
float angularDist = Math.Abs( normMouseAngle - normButtonAngle );
if ( angularDist > 180f ) angularDist = 360f - angularDist;
if ( angularDist < closestDistance )
{
closestDistance = angularDist;
closestIndex = i;
}
}
// Set target angle to the selected button's angle for smooth snapping
if ( closestIndex != -1 )
{
_targetIndicatorAngle = startAngle + (closestIndex * angleStep);
}
_hoveredIndex = closestIndex;
Update();
}
[Shortcut( "editor.paste.color", "CTRL+V", typeof( SceneViewWidget ) )]
public static void PasteFromClipboard()
{
var clipboard = EditorUtility.Clipboard.Paste();
if ( string.IsNullOrWhiteSpace( clipboard ) )
{
// Try GameObject paste if clipboard text is empty
PasteGameObject();
return;
}
// Try to parse as hex color first
if ( clipboard.StartsWith( "#" ) )
{
if ( TryParseHexColor( clipboard, out Color color ) )
{
PasteColor( color );
return;
}
else
{
Log.Warning( $"Failed to parse color from clipboard: {clipboard}" );
return;
}
}
// Try to handle GameObject paste
PasteGameObject();
}
private static void PasteColor( Color color )
{
using var scope = SceneEditorSession.Scope();
var selection = SceneEditorSession.Active.Selection;
// Get all MeshComponents and ModelRenderers from selected GameObjects
var meshComponents = selection.OfType<GameObject>()
.Select( x => x.GetComponent<MeshComponent>() )
.Where( x => x.IsValid() )
.ToList();
var modelRenderers = selection.OfType<GameObject>()
.Select( x => x.GetComponent<ModelRenderer>() )
.Where( x => x.IsValid() )
.ToList();
var totalCount = meshComponents.Count + modelRenderers.Count;
if ( totalCount == 0 )
{
Log.Info( "No mesh or model renderer components selected" );
return;
}
// Combine both lists for undo tracking
var allComponents = meshComponents.Cast<Component>()
.Concat( modelRenderers.Cast<Component>() )
.ToList();
using ( SceneEditorSession.Active.UndoScope( "Paste Color" )
.WithComponentChanges( allComponents )
.Push() )
{
// Apply color to MeshComponents
foreach ( var component in meshComponents )
{
component.Color = color;
}
// Apply color to ModelRenderers
foreach ( var component in modelRenderers )
{
component.Tint = color;
}
}
Log.Info( $"Applied color to {meshComponents.Count} mesh component(s) and {modelRenderers.Count} model renderer(s)" );
}
private static void PasteGameObject()
{
var session = SceneEditorSession.Active;
if ( session == null ) return;
// Get the active scene viewport
var sceneView = SceneViewWidget.Current;
if ( sceneView?.LastSelectedViewportWidget == null ) return;
var viewport = sceneView.LastSelectedViewportWidget;
// First, paste the standard way
EditorScene.Paste();
// Get the pasted objects
var pastedObjects = session.Selection.OfType<GameObject>().ToList();
if ( pastedObjects.Count == 0 ) return;
// Compute the average point of all pasted objects
Vector3 middlePoint = Vector3.Zero;
foreach ( var go in pastedObjects )
middlePoint += go.WorldPosition;
middlePoint /= pastedObjects.Count;
// Get mouse position in viewport and trace
var mousePosition = SceneViewportWidget.MousePosition;
var camera = viewport.Renderer.Camera;
if ( !camera.IsValid() ) return;
// Create ray from mouse position
var ray = camera.ScreenPixelToRay( mousePosition );
// Trace to find world position
var trace = session.Scene.Trace
.Ray( ray, 10000f )
.UseRenderMeshes( true )
.UsePhysicsWorld( false )
.Run();
if ( trace.Hit )
{
using ( session.UndoScope( "Paste at Mouse" ).Push() )
{
// Reposition all game objects relative to new center
foreach ( var go in pastedObjects )
{
Vector3 offset = go.WorldPosition - middlePoint;
go.WorldPosition = trace.HitPosition + offset;
}
Log.Info( $"Pasted {pastedObjects.Count} GameObject(s) at mouse position" );
}
}
}
static bool TryParseHexColor( string hex, out Color color )
{
color = default;
if ( string.IsNullOrEmpty( hex ) )
return false;
hex = hex.TrimStart( '#' );
if ( hex.Length != 6 && hex.Length != 8 )
return false;
try
{
int r = Convert.ToInt32( hex.Substring( 0, 2 ), 16 );
int g = Convert.ToInt32( hex.Substring( 2, 2 ), 16 );
int b = Convert.ToInt32( hex.Substring( 4, 2 ), 16 );
int a = hex.Length == 8 ? Convert.ToInt32( hex.Substring( 6, 2 ), 16 ) : 255;
color = new Color( r / 255f, g / 255f, b / 255f, a / 255f );
return true;
}
catch
{
return false;
}
}
public IReadOnlyList<PieOption> Options => _options.AsReadOnly();
}
using Editor;
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static Editor.Label;
internal sealed class MappingToolSettingsWindow : BaseWindow
{
private NavigationView Navigation { get; }
[Menu( "Editor", "Mapping Tools/Settings", Icon = "settings" )]
public static MappingToolSettingsWindow Open()
{
var window = new MappingToolSettingsWindow();
window.Show();
return window;
}
public MappingToolSettingsWindow()
{
SetModal( true, true );
Size = new Vector2( 640, 420 );
MinimumSize = Size;
TranslucentBackground = true;
NoSystemBackground = true;
WindowTitle = "Mapping Tool Settings";
SetWindowIcon( "settings" );
Layout = Layout.Column();
Layout.Margin = 4;
Layout.Spacing = 4;
Navigation = new NavigationView();
Layout.Add( Navigation );
BuildPages();
}
private void BuildPages()
{
Navigation.AddSectionHeader( "Mapping Tools" );
Navigation.AddPage( "General", "tune", new PageGeneral( this ) );
Navigation.AddPage( "Materials Gallery", "image", new PageMaterialBrowser( this ) );
Navigation.AddPage( "Render Pie Menu", "menu", new PageRenderPieMenu( this ) );
Navigation.AddPage( "Mapping Pie Menu", "edit", new PageMappingPieMenu( this ) );
Navigation.AddPage( "Auto-Save", "save", new PageAutoSave( this ) );
}
}
internal sealed class PageGeneral : Widget
{
public PageGeneral( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 32;
Layout.Spacing = 16;
Layout.Add( new Subtitle( "General Settings" ) );
Layout.Add( new Editor.Label( "General mapping workflow enhancements" ) { WordWrap = true } );
var sheet = new Editor.ControlSheet();
sheet.AddProperty( () => MappingToolSettings.DoubleClickToMeshMode );
Layout.Add( sheet );
Layout.AddStretchCell();
}
}
internal sealed class PageMaterialBrowser : Widget
{
private Layout _orgListLayout;
public PageMaterialBrowser( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 32;
Layout.Spacing = 16;
Layout.Add( new Subtitle( "Materials Gallery" ) );
var sheet = new Editor.ControlSheet();
// Add default org property
sheet.AddProperty( () => MappingToolSettings.DefaultOrganization );
Layout.Add( sheet );
// Organizations list section
Layout.Add( new Editor.Label.Title( "Additional Organizations" ) );
Layout.Add( new Editor.Label( "Add organizations to search for materials" ) { WordWrap = true } );
// Scrollable container for organizations
var scrollArea = new ScrollArea( this );
scrollArea.MinimumHeight = 150;
scrollArea.MaximumHeight = 200;
Layout.Add( scrollArea );
var container = new Widget( scrollArea );
_orgListLayout = container.Layout = Layout.Column();
_orgListLayout.Spacing = 4;
scrollArea.Canvas = container;
// Add organization controls
var addRow = Layout.AddRow();
addRow.Spacing = 8;
var orgInput = new LineEdit();
orgInput.PlaceholderText = "Enter organization name...";
orgInput.MinimumWidth = 200;
addRow.Add( orgInput, 1 );
var addButton = new Editor.Button( "Add", "add" );
addButton.Clicked += () =>
{
var org = orgInput.Text?.Trim();
if ( !string.IsNullOrEmpty( org ) )
{
MappingToolSettings.AddOrganization( org );
orgInput.Text = "";
RefreshOrgList();
}
};
addRow.Add( addButton );
var resetButton = new Editor.Button( "Reset to Defaults" );
resetButton.Clicked += () =>
{
MappingToolSettings.ResetMaterialGalleryDefaults();
RefreshOrgList();
};
Layout.Add( resetButton );
Layout.AddStretchCell();
RefreshOrgList();
}
private void RefreshOrgList()
{
// Clear existing items
_orgListLayout.Clear( true );
var orgs = MappingToolSettings.AdditionalOrganizations.ToList();
if ( orgs.Count == 0 )
{
var emptyLabel = new Editor.Label( "No additional organizations added" );
emptyLabel.SetStyles( "color: rgba(255,255,255,0.4); font-style: italic;" );
_orgListLayout.Add( emptyLabel );
return;
}
foreach ( var org in orgs )
{
var row = _orgListLayout.AddRow();
row.Spacing = 8;
// Org name label
var nameLabel = new Editor.Label( org );
nameLabel.MinimumWidth = 150;
row.Add( nameLabel, 1 );
// Remove button
var removeBtn = new Editor.Button.Primary( "", "close" );
removeBtn.MinimumWidth = 32;
removeBtn.MaximumWidth = 32;
removeBtn.ToolTip = $"Remove {org}";
removeBtn.Clicked += () =>
{
MappingToolSettings.RemoveOrganization( org );
RefreshOrgList();
};
row.Add( removeBtn );
}
}
}
internal sealed class PageRenderPieMenu : Widget
{
public PageRenderPieMenu( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 32;
Layout.Spacing = 16;
Layout.Add( new Subtitle( "Render Pie Menu" ) );
Layout.Add( new Editor.Label( "Quick access to render modes and viewport settings" ) { WordWrap = true } );
var sheet = new Editor.ControlSheet();
// Automatically generate UI from the settings properties
sheet.AddProperty( () => MappingToolSettings.PieMenuButton );
sheet.AddProperty( () => MappingToolSettings.PieMenuUseModifier );
sheet.AddProperty( () => MappingToolSettings.PieMenuModifierKey );
sheet.AddProperty( () => MappingToolSettings.PieMenuSize );
var resetButton = new Editor.Button( "Reset to Defaults" );
resetButton.Clicked += () =>
{
MappingToolSettings.ResetPieMenuDefaults();
};
Layout.Add( sheet );
Layout.Add( resetButton );
Layout.AddStretchCell();
}
}
internal sealed class PageMappingPieMenu : Widget
{
public PageMappingPieMenu( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 32;
Layout.Spacing = 16;
Layout.Add( new Subtitle( "Mapping Pie Menu" ) );
Layout.Add( new Editor.Label( "Quick access to mesh editing modes (Object, Vertex, Edge, Face)" ) { WordWrap = true } );
var sheet = new Editor.ControlSheet();
// Automatically generate UI from the settings properties
sheet.AddProperty( () => MappingToolSettings.MappingPieMenuButton );
sheet.AddProperty( () => MappingToolSettings.MappingPieMenuUseModifier );
sheet.AddProperty( () => MappingToolSettings.MappingPieMenuModifierKey );
sheet.AddProperty( () => MappingToolSettings.MappingPieMenuSize );
var resetButton = new Editor.Button( "Reset to Defaults" );
resetButton.Clicked += () =>
{
MappingToolSettings.ResetMappingPieMenuDefaults();
};
Layout.Add( sheet );
Layout.Add( resetButton );
Layout.AddStretchCell();
}
}
// Add this new page class
internal sealed class PageAutoSave : Widget
{
public PageAutoSave( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 32;
Layout.Spacing = 16;
Layout.Add( new Subtitle( "Auto-Save" ) );
Layout.Add( new Editor.Label( "Automatically create backup saves at regular intervals" ) { WordWrap = true } );
var sheet = new Editor.ControlSheet();
sheet.AddProperty( () => MappingToolSettings.AutoSaveEnabled );
sheet.AddProperty( () => MappingToolSettings.AutoSaveIntervalMinutes );
sheet.AddProperty( () => MappingToolSettings.AutoSaveMaxBackups );
sheet.AddProperty( () => MappingToolSettings.AutoSaveShowNotification );
Layout.Add( sheet );
// Force save button
var saveNowButton = new Editor.Button( "Save Backup Now", "save" );
saveNowButton.Clicked += () => AutoSave.ForceAutoSave();
Layout.Add( saveNowButton );
// Open folder button
var openFolderButton = new Editor.Button( "Open Autosave Folder", "folder_open" );
openFolderButton.Clicked += OpenAutoSaveFolder;
Layout.Add( openFolderButton );
var resetButton = new Editor.Button( "Reset to Defaults" );
resetButton.Clicked += () => MappingToolSettings.ResetAutoSaveDefaults();
Layout.Add( resetButton );
Layout.AddStretchCell();
}
private void OpenAutoSaveFolder()
{
var session = SceneEditorSession.Active;
if ( session?.Scene?.Source?.ResourcePath == null )
{
Log.Info( "No active scene" );
return;
}
var sceneDirectory = Path.GetDirectoryName( session.Scene.Source.ResourcePath );
var autoSaveFolder = Path.Combine( sceneDirectory, "autosave" );
var fullPath = Editor.FileSystem.ProjectTemporary.GetFullPath( autoSaveFolder );
// Create the folder if it doesn't exist
if ( !Directory.Exists( fullPath ) )
{
Directory.CreateDirectory( fullPath );
}
System.Diagnostics.Process.Start( "explorer.exe", fullPath );
}
}
/// <summary>
/// Settings for mapping tools including pie menu keybinds
/// </summary>
public static class MappingToolSettings
{
private const string PreferencePrefix = "MappingTools.";
// Render Pie Menu Settings
[Title( "Mouse Button" )]
[Description( "Mouse button to open the render pie menu" )]
public static MouseButtons PieMenuButton
{
get => (MouseButtons)EditorCookie.Get( PreferencePrefix + "PieMenuButton", (int)MouseButtons.Forward );
set => EditorCookie.Set( PreferencePrefix + "PieMenuButton", (int)value );
}
[Title( "Use Modifier Key" )]
[Description( "Whether to require a modifier key to open the render pie menu" )]
public static bool PieMenuUseModifier
{
get => EditorCookie.Get( PreferencePrefix + "PieMenuUseModifier", false );
set => EditorCookie.Set( PreferencePrefix + "PieMenuUseModifier", value );
}
[Title( "Modifier Key" )]
[Description( "Modifier key to open the render pie menu with" )]
public static KeyCode PieMenuModifierKey
{
get => (KeyCode)EditorCookie.Get( PreferencePrefix + "PieMenuModifierKey", (int)KeyCode.Control );
set => EditorCookie.Set( PreferencePrefix + "PieMenuModifierKey", (int)value );
}
[Title( "Menu Size" )]
[Description( "Radius of the render pie menu in pixels" )]
[Range( 100, 400 )]
public static float PieMenuSize
{
get => EditorCookie.Get( PreferencePrefix + "PieMenuSize", 180f );
set => EditorCookie.Set( PreferencePrefix + "PieMenuSize", value );
}
// Mapping Pie Menu Settings
[Title( "Mouse Button" )]
[Description( "Mouse button to open the mapping mode pie menu" )]
public static MouseButtons MappingPieMenuButton
{
get => (MouseButtons)EditorCookie.Get( PreferencePrefix + "MappingPieMenuButton", (int)MouseButtons.Back );
set => EditorCookie.Set( PreferencePrefix + "MappingPieMenuButton", (int)value );
}
[Title( "Use Modifier Key" )]
[Description( "Whether to require a modifier key to open the mapping pie menu" )]
public static bool MappingPieMenuUseModifier
{
get => EditorCookie.Get( PreferencePrefix + "MappingPieMenuUseModifier", false );
set => EditorCookie.Set( PreferencePrefix + "MappingPieMenuUseModifier", value );
}
[Title( "Modifier Key" )]
[Description( "Modifier key to open the mapping pie menu with" )]
public static KeyCode MappingPieMenuModifierKey
{
get => (KeyCode)EditorCookie.Get( PreferencePrefix + "MappingPieMenuModifierKey", (int)KeyCode.Control );
set => EditorCookie.Set( PreferencePrefix + "MappingPieMenuModifierKey", (int)value );
}
[Title( "Menu Size" )]
[Description( "Radius of the mapping pie menu in pixels" )]
[Range( 100, 400 )]
public static float MappingPieMenuSize
{
get => EditorCookie.Get( PreferencePrefix + "MappingPieMenuSize", 180f );
set => EditorCookie.Set( PreferencePrefix + "MappingPieMenuSize", value );
}
// Material Gallery Settings
[Title( "Default Organization" )]
[Description( "Primary organization to search for materials" )]
public static string DefaultOrganization
{
get => EditorCookie.Get( PreferencePrefix + "DefaultOrganization", "facepunch" );
set => EditorCookie.Set( PreferencePrefix + "DefaultOrganization", value );
}
public static IEnumerable<string> AdditionalOrganizations
{
get
{
var json = EditorCookie.Get( PreferencePrefix + "AdditionalOrganizations", "[]" );
return Json.Deserialize<List<string>>( json ) ?? new List<string>();
}
set
{
var json = Json.Serialize( value );
EditorCookie.Set( PreferencePrefix + "AdditionalOrganizations", json );
}
}
public static void AddOrganization( string org )
{
var orgs = AdditionalOrganizations.ToList();
if ( !orgs.Contains( org, StringComparer.OrdinalIgnoreCase ) )
{
orgs.Add( org );
AdditionalOrganizations = orgs;
}
}
public static void RemoveOrganization( string org )
{
var orgs = AdditionalOrganizations.ToList();
orgs.RemoveAll( o => o.Equals( org, StringComparison.OrdinalIgnoreCase ) );
AdditionalOrganizations = orgs;
}
public static void ResetPieMenuDefaults()
{
PieMenuButton = MouseButtons.Forward;
PieMenuUseModifier = false;
PieMenuModifierKey = KeyCode.Control;
PieMenuSize = 180f;
}
public static void ResetMappingPieMenuDefaults()
{
MappingPieMenuButton = MouseButtons.Back;
MappingPieMenuUseModifier = false;
MappingPieMenuModifierKey = KeyCode.Control;
MappingPieMenuSize = 180f;
}
public static void ResetMaterialGalleryDefaults()
{
DefaultOrganization = "facepunch";
AdditionalOrganizations = new List<string>();
}
// Add these to MappingToolSettings class:
// Auto-Save Settings
[Title( "Enable Auto-Save" )]
[Description( "Automatically save backups of your scene at regular intervals" )]
public static bool AutoSaveEnabled
{
get => EditorCookie.Get( PreferencePrefix + "AutoSaveEnabled", true );
set => EditorCookie.Set( PreferencePrefix + "AutoSaveEnabled", value );
}
[Title( "Interval (Minutes)" )]
[Description( "How often to create a backup save" )]
[Range( 1, 60 )]
public static float AutoSaveIntervalMinutes
{
get => EditorCookie.Get( PreferencePrefix + "AutoSaveIntervalMinutes", 5f );
set => EditorCookie.Set( PreferencePrefix + "AutoSaveIntervalMinutes", value );
}
[Title( "Maximum Backups" )]
[Description( "Maximum number of backup files to keep per scene (0 = unlimited)" )]
[Range( 0, 50 )]
public static int AutoSaveMaxBackups
{
get => EditorCookie.Get( PreferencePrefix + "AutoSaveMaxBackups", 10 );
set => EditorCookie.Set( PreferencePrefix + "AutoSaveMaxBackups", value );
}
[Title( "Show Notification" )]
[Description( "Show a toast notification when auto-save completes" )]
public static bool AutoSaveShowNotification
{
get => EditorCookie.Get( PreferencePrefix + "AutoSaveShowNotification", true );
set => EditorCookie.Set( PreferencePrefix + "AutoSaveShowNotification", value );
}
// Double-Click Settings
[Title( "Double-Click to Mesh Mode" )]
[Description( "Double-click on a GameObject with MeshComponent to enter Mesh Tool mode" )]
public static bool DoubleClickToMeshMode
{
get => EditorCookie.Get( PreferencePrefix + "DoubleClickToMeshMode", false );
set => EditorCookie.Set( PreferencePrefix + "DoubleClickToMeshMode", value );
}
public static void ResetAutoSaveDefaults()
{
AutoSaveEnabled = true;
AutoSaveIntervalMinutes = 5f;
AutoSaveMaxBackups = 10;
AutoSaveShowNotification = true;
}
}
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace GeneralGame.Editor;
/// <summary>
/// Helper for building asset context menus with Create Material/Texture options.
/// </summary>
public static class AssetContextMenuHelper
{
/// <summary>
/// Add asset-type specific options like Create Material, Create Texture, etc.
/// </summary>
public static void AddAssetTypeOptions(Menu menu, Asset asset)
{
if (asset == null) return;
var assetType = asset.AssetType;
if (assetType == null) return;
// Image files - can create Material, Texture, Sprite
if (assetType == AssetType.ImageFile)
{
menu.AddSeparator();
menu.AddOption("Create Material", "image", () => CreateMaterialFromImage(asset));
menu.AddOption("Create Texture", "texture", () => CreateTextureFromImage(asset));
menu.AddOption("Create Sprite", "emoji_emotions", () => CreateSpriteFromImage(asset));
}
// Shader files - can create Material
if (assetType == AssetType.Shader)
{
menu.AddSeparator();
menu.AddOption("Create Material", "image", () => CreateMaterialFromShader(asset));
}
// Mesh files (FBX, OBJ) - can create Model
var meshExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".fbx", ".obj", ".dmx", ".gltf", ".glb" };
if (meshExtensions.Contains(Path.GetExtension(asset.AbsolutePath)))
{
menu.AddSeparator();
menu.AddOption("Create Model", "view_in_ar", () => CreateModelFromMesh(asset));
}
}
private static void CreateTextureFromImage(Asset asset)
{
var assetName = asset.Name;
var fd = new FileDialog(null);
fd.Title = "Create Texture from Image..";
fd.Directory = Path.GetDirectoryName(asset.AbsolutePath);
fd.DefaultSuffix = ".vtex";
fd.SelectFile($"{assetName}.vtex");
fd.SetFindFile();
fd.SetModeSave();
fd.SetNameFilter("Texture File (*.vtex)");
if (!fd.Execute())
return;
var imagePath = asset.RelativePath;
// Create simple vtex JSON structure
var vtexContent = new Dictionary<string, object>
{
{ "Sequences", new object[]
{
new Dictionary<string, object>
{
{ "Source", imagePath },
{ "IsLooping", true }
}
}
}
};
var json = Json.Serialize(vtexContent);
File.WriteAllText(fd.SelectedFile, json);
AssetSystem.RegisterFile(fd.SelectedFile);
}
private static void CreateMaterialFromImage(Asset asset)
{
string[] types = new[] { "color", "ao", "normal", "metallic", "rough", "diff", "diffuse", "nrm", "spec", "selfillum", "mask" };
var assetName = asset.Name;
foreach (var t in types)
{
if (assetName.EndsWith($"_{t}"))
assetName = assetName.Substring(0, assetName.Length - (t.Length + 1));
}
var fd = new FileDialog(null);
fd.Title = "Create Material from Image..";
fd.Directory = Path.GetDirectoryName(asset.AbsolutePath);
fd.DefaultSuffix = ".vmat";
fd.SelectFile($"{assetName}.vmat");
fd.SetFindFile();
fd.SetModeSave();
fd.SetNameFilter("Material File (*.vmat)");
if (!fd.Execute())
return;
var assetPath = Path.GetDirectoryName(asset.AbsolutePath).NormalizeFilename(false);
var assetPeers = AssetSystem.All
.Where(x => x.AssetType == AssetType.ImageFile)
.Where(x => x.AbsolutePath.StartsWith(assetPath))
.ToArray();
var assetPeersWithSameBaseName = assetPeers
.Where(x => x.Name == assetName || x.Name.StartsWith(assetName + "_"))
.ToArray();
if (assetPeersWithSameBaseName.Length > 0)
{
assetPeers = assetPeersWithSameBaseName;
}
string texColor = assetPeers.Where(x => x.Name.Contains("_color") || x.Name.Contains("_diff")).Select(x => x.RelativePath).FirstOrDefault();
texColor ??= asset.RelativePath;
string texNormal = assetPeers.Where(x => x.Name.Contains("_nrm") || x.Name.Contains("_normal") || x.Name.Contains("_amb")).Select(x => x.RelativePath).FirstOrDefault() ?? "materials/default/default_normal.tga";
string texAo = assetPeers.Where(x => x.Name.Contains("_ao") || x.Name.Contains("_occ") || x.Name.Contains("_amb")).Select(x => x.RelativePath).FirstOrDefault() ?? "materials/default/default_ao.tga";
string texRough = assetPeers.Where(x => x.Name.Contains("_rough")).Select(x => x.RelativePath).FirstOrDefault() ?? "materials/default/default_rough.tga";
string texMetallic = assetPeers.Where(x => x.Name.Contains("_metallic")).Select(x => x.RelativePath).FirstOrDefault();
if (texMetallic != null)
{
texMetallic = $"\n\tF_METALNESS_TEXTURE 1\n\tF_SPECULAR 1\n\tTextureMetalness \"{texMetallic}\"";
}
string texSelfIllum = assetPeers.Where(x => x.Name.Contains("_selfillum")).Select(x => x.RelativePath).FirstOrDefault();
if (texSelfIllum != null)
{
texSelfIllum = $"\n\tF_SELF_ILLUM 1\n\tTextureSelfIllumMask \"{texSelfIllum}\"";
}
string tintMask = assetPeers.Where(x => x.Name.Contains("_mask")).Select(x => x.RelativePath).FirstOrDefault();
if (tintMask != null)
{
tintMask = $"\n\tF_TINT_MASK 1\n\tTextureTintMask \"{tintMask}\"";
}
var file = $@"
Layer0
{{
shader ""shaders/complex.shader_c""
TextureColor ""{texColor}""
TextureAmbientOcclusion ""{texAo}""
TextureNormal ""{texNormal}""
TextureRoughness ""{texRough}""{texMetallic}{texSelfIllum}{tintMask}
}}
";
File.WriteAllText(fd.SelectedFile, file);
AssetSystem.RegisterFile(fd.SelectedFile);
}
private static async void CreateSpriteFromImage(Asset asset)
{
var assetName = asset.Name;
var fd = new FileDialog(null);
fd.Title = "Create Sprite from Image..";
fd.Directory = Path.GetDirectoryName(asset.AbsolutePath);
fd.DefaultSuffix = ".sprite";
fd.SelectFile($"{assetName}.sprite");
fd.SetFindFile();
fd.SetModeSave();
fd.SetNameFilter("Sprite File (*.sprite)");
if (!fd.Execute())
return;
var path = Path.ChangeExtension(asset.Path, Path.GetExtension(asset.AbsolutePath));
var sprite = Sprite.FromTexture(Texture.Load(path));
var json = sprite.Serialize().ToJsonString();
File.WriteAllText(fd.SelectedFile, json);
var resultAsset = AssetSystem.RegisterFile(fd.SelectedFile);
while (!resultAsset.IsCompiledAndUpToDate)
{
await Task.Delay(10);
}
}
private static void CreateMaterialFromShader(Asset asset)
{
var assetName = asset.Name;
var fd = new FileDialog(null);
fd.Title = "Create Material from Shader..";
fd.Directory = Path.GetDirectoryName(asset.AbsolutePath);
fd.DefaultSuffix = ".vmat";
fd.SelectFile($"{assetName}.vmat");
fd.SetFindFile();
fd.SetModeSave();
fd.SetNameFilter("Material File (*.vmat)");
if (!fd.Execute())
return;
var shaderPath = asset.GetCompiledFile();
var file = $@"
Layer0
{{
shader ""{shaderPath}""
}}
";
File.WriteAllText(fd.SelectedFile, file);
AssetSystem.RegisterFile(fd.SelectedFile);
}
private static void CreateModelFromMesh(Asset asset)
{
var targetPath = EditorUtility.SaveFileDialog("Create Model..", "vmdl", Path.ChangeExtension(asset.AbsolutePath, "vmdl"));
if (targetPath == null)
return;
EditorUtility.CreateModelFromMeshFile(asset, targetPath);
}
}
using Editor;
using Sandbox;
using System;
using System.Linq;
using System.Reflection;
namespace SpriteTools.TilesetTool;
[Inspector( typeof( TilesetTool ) )]
public class TilesetToolInspector : InspectorWidget
{
public static TilesetToolInspector Active { get; private set; }
internal TilesetTool Tool;
StatusWidget Header;
ScrollArea scrollArea;
ControlSheet toolSheet;
ControlSheet mainSheet;
ControlSheet selectedSheet;
public TilesetToolInspector ( SerializedObject so ) : base( so )
{
if ( so.Targets.FirstOrDefault() is not TilesetTool tool ) return;
Tool = tool;
// Tool.UpdateInspector += UpdateHeader;
// Tool.UpdateInspector += UpdateSelectedSheet;
Layout = Layout.Column();
Layout.Margin = 4;
Layout.Spacing = 8;
Active = this;
Rebuild();
}
int lastBuildHash = 0;
[EditorEvent.Frame]
void Frame ()
{
int buildHash = 0;
if ( Tool.SelectedComponent.IsValid() )
{
buildHash += Tool.SelectedComponent.Layers.IndexOf( Tool?.SelectedLayer );
buildHash += Tool?.SelectedLayer?.TilesetResource?.ResourceId ?? 0;
}
if ( buildHash != lastBuildHash )
{
lastBuildHash = buildHash;
Rebuild();
}
}
[EditorEvent.Hotload]
void Rebuild ()
{
if ( Layout is null ) return;
Layout.Clear( true );
scrollArea = new ScrollArea( this );
scrollArea.Canvas = new Widget();
scrollArea.Canvas.Layout = Layout.Column();
scrollArea.Canvas.VerticalSizeMode = SizeMode.CanGrow;
scrollArea.Canvas.HorizontalSizeMode = SizeMode.Flexible;
scrollArea.Canvas.Layout.Spacing = 8;
Layout.Add( scrollArea );
Header = new StatusWidget( this );
scrollArea.Canvas.Layout.Add( Header );
UpdateHeader();
mainSheet = new ControlSheet();
scrollArea.Canvas.Layout.Add( mainSheet );
UpdateMainSheet();
selectedSheet = null;
UpdateSelectedSheet();
toolSheet = new ControlSheet();
scrollArea.Canvas.Layout.Add( toolSheet );
UpdateToolSheet();
// Preview = new Preview.Preview(this);
// scrollArea.Canvas.Layout.Add(Preview);
scrollArea.Canvas.Layout.AddStretchCell();
}
internal void UpdateHeader ()
{
Header.Text = "Paint Tiles";
Header.Color = ( false ) ? Theme.Red : Theme.Blue;
Header.Icon = ( false ) ? "warning" : "dashboard";
Header.Update();
}
internal void UpdateToolSheet ()
{
if ( !( Layout?.IsValid ?? false ) ) return;
if ( toolSheet is null ) return;
toolSheet?.Clear( true );
if ( Tool?.Settings is not null )
{
toolSheet.AddObject( Tool.Settings.GetSerialized(), x =>
{
return x.HasAttribute<PropertyAttribute>() && x.PropertyType != typeof( Action );
} );
}
}
internal void UpdateMainSheet ()
{
if ( !( Layout?.IsValid ?? false ) ) return;
if ( mainSheet is null ) return;
mainSheet?.Clear( true );
if ( Tool?.CurrentTool is not null )
{
var toolName = ( Tool.CurrentTool.GetType()?.GetCustomAttribute<TitleAttribute>()?.Value ?? "Unknown" ) + " Tool";
mainSheet.AddObject( Tool.CurrentTool.GetSerialized(), x => x.HasAttribute<PropertyAttribute>() && x.PropertyType != typeof( Action ) );
}
if ( Tool.SelectedComponent.IsValid() )
{
mainSheet.AddObject( Tool.SelectedComponent.GetSerialized(), x =>
{
if ( x.Name == nameof( TilesetComponent.Layers ) ) return true;
if ( !x.HasAttribute<PropertyAttribute>() ) return false;
if ( x.TryGetAttribute<FeatureAttribute>( out var feature ) && feature.Title == "Collision" ) return false;
if ( x.PropertyType == typeof( Action ) ) return false;
if ( x.PropertyType == typeof( TilesetComponent.ComponentControls ) ) return false;
return true;
} );
}
}
internal void UpdateSelectedSheet ()
{
if ( !( Layout?.IsValid ?? false ) ) return;
if ( selectedSheet is null || !( selectedSheet?.IsValid ?? false ) )
{
selectedSheet = new ControlSheet();
scrollArea.Canvas.Layout.Add( selectedSheet );
}
selectedSheet?.Clear( true );
if ( Tool.SelectedLayer is not null )
{
selectedSheet.AddObject( Tool.SelectedLayer.GetSerialized(), x => x.HasAttribute<PropertyAttribute>() && x.PropertyType != typeof( Action ) );
}
}
private class StatusWidget : Widget
{
public string Icon { get; set; }
public string Text { get; set; }
public string LeadText { get; set; }
public Color Color { get; set; }
TilesetToolInspector Inspector;
public StatusWidget ( TilesetToolInspector parent ) : base( parent )
{
Inspector = parent;
MinimumSize = 48;
Cursor = CursorShape.Finger;
SetSizeMode( SizeMode.Default, SizeMode.CanShrink );
}
protected override void OnPaint ()
{
var rect = new Rect( 0, Size );
Paint.ClearPen();
Paint.SetBrush( Theme.WindowBackground.Lighten( 0.9f ) );
Paint.DrawRect( rect );
rect.Left += 8;
Paint.SetPen( Color );
var iconRect = Paint.DrawIcon( rect, Icon, 24, TextFlag.LeftCenter );
rect.Top += 8;
rect.Left = iconRect.Right + 8;
Paint.SetPen( Color );
Paint.SetDefaultFont( 10, 500 );
var titleRect = Paint.DrawText( rect, Text, TextFlag.LeftTop );
rect.Top = titleRect.Bottom + 2;
Paint.SetPen( Color.WithAlpha( 0.6f ) );
Paint.SetDefaultFont( 8, 400 );
var preText = "Selected Component:";
if ( !Inspector.Tool.SelectedComponent.IsValid() )
preText = "No Tileset Component";
var selectedRect = Paint.DrawText( rect, preText, TextFlag.LeftTop );
if ( Inspector.Tool.SelectedComponent.IsValid() )
{
var name = Inspector.Tool.SelectedComponent.GameObject.Name;
var textPos = selectedRect.TopRight + new Vector2( 8, 0 );
var textRect = new Rect( textPos, Paint.MeasureText( name ) );
var boxRect = textRect.Grow( 4, 2, 18, 2 );
var isHovering = Paint.HasMouseOver;
var boxCol = isHovering ? Theme.ControlBackground.Lighten( 0.3f ) : Theme.ControlBackground.Darken( 0.2f );
var color = isHovering ? Color.Lighten( 0.2f ) : Color;
Paint.SetBrushAndPen( boxCol, Color.Transparent );
Paint.DrawRect( boxRect );
Paint.SetPen( color );
var drawnRect = Paint.DrawText( textPos, name );
var iconPos = drawnRect.TopRight + new Vector2( 2, 0 );
Paint.DrawIcon( Rect.FromPoints( iconPos, iconPos + 14 ), "expand_more", 14 );
}
}
protected override void OnMouseClick ( MouseEvent e )
{
base.OnMouseClick( e );
var components = SceneEditorSession.Active.Scene.GetAllComponents<TilesetComponent>();
Log.Info( components.Count() );
if ( components.Count() == 0 ) return;
var menu = new Menu();
foreach ( var tileset in components )
{
var option = menu.AddOption( tileset.GameObject.Name, null, () =>
{
Inspector.Tool.SelectedComponent = tileset;
Inspector.Tool.SelectedLayer = tileset.Layers.FirstOrDefault();
} );
option.Checkable = true;
option.Checked = tileset == Inspector.Tool.SelectedComponent;
}
menu.OpenAtCursor();
}
}
}