mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-20 04:10:00 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
735 lines
16 KiB
C#
735 lines
16 KiB
C#
using Editor.NodeEditor;
|
|
using Facepunch.ActionGraphs;
|
|
using Sandbox;
|
|
using Sandbox.ActionGraphs;
|
|
using Sandbox.Internal;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
|
|
namespace Editor.ActionGraphs;
|
|
|
|
public class EditorNode : INode
|
|
{
|
|
[Hide]
|
|
public INodeType Type => new LibraryNodeType( Definition );
|
|
|
|
[Hide]
|
|
public EditorActionGraph Graph { get; }
|
|
|
|
[Hide]
|
|
public Node Node { get; }
|
|
|
|
[Hide]
|
|
public NodeDefinition Definition => Node.Definition;
|
|
|
|
[Hide]
|
|
public event Action Changed;
|
|
|
|
[Hide]
|
|
public string Identifier { get; }
|
|
|
|
[Hide]
|
|
public string ErrorMessage => string.Join( Environment.NewLine,
|
|
Node.GetMessages()
|
|
.Where( x => x.IsError )
|
|
.Select( FormatMessage ) );
|
|
|
|
private static string FormatProperty( Node.Property property )
|
|
{
|
|
return property.Definition.Display.Title;
|
|
}
|
|
|
|
private static string FormatInput( Node.Input input )
|
|
{
|
|
return input.Definition.Display.Title;
|
|
}
|
|
|
|
private static string FormatOutput( Node.Output output )
|
|
{
|
|
return output.Definition.Display.Title;
|
|
}
|
|
|
|
private string FormatMessage( ValidationMessage message )
|
|
{
|
|
return message.Context switch
|
|
{
|
|
Link link when link.Target.Node == Node => $"{FormatInput( link.Target )}: {message.Value}",
|
|
Node.Property property when property.Node == Node => $"{FormatProperty( property )}: {message.Value}",
|
|
Node.Input input when input.Node == Node => $"{FormatInput( input )}: {message.Value}",
|
|
Node.Output output when output.Node == Node => $"{FormatOutput( output )}: {message.Value}",
|
|
_ => message.Value
|
|
};
|
|
}
|
|
|
|
[Hide]
|
|
public virtual Sandbox.DisplayInfo DisplayInfo => OverrideTitle is null ? Node.GetDisplayInfo() : Node.GetDisplayInfo() with { Name = OverrideTitle };
|
|
|
|
[Hide]
|
|
public bool CanClone => Node.Definition != EditorNodeLibrary.Input && Node.Definition != EditorNodeLibrary.Output;
|
|
|
|
[Hide]
|
|
public bool CanRemove
|
|
{
|
|
get
|
|
{
|
|
if ( Node.Definition == EditorNodeLibrary.Input )
|
|
{
|
|
// Allow deleting duplicate input nodes
|
|
|
|
return Node.ActionGraph.Nodes.Values.Count( x => x.Definition == EditorNodeLibrary.Input ) > 1;
|
|
}
|
|
|
|
if ( Node.Definition == EditorNodeLibrary.Output )
|
|
{
|
|
var name = GetOutputNodeName( Node );
|
|
|
|
if ( name == ParameterNames.Signal && Node.Inputs.Values.Count( x => !x.IsSignal ) == 0 )
|
|
{
|
|
// Allow deleting primary output signal if there are no value outputs
|
|
|
|
return true;
|
|
}
|
|
|
|
return Node.ActionGraph.Nodes.Values.Count( x => x.Definition == EditorNodeLibrary.Output
|
|
&& GetOutputNodeName( x ) == name ) > 1;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static string GetOutputNodeName( Node node )
|
|
{
|
|
return node.Properties.TryGetValue( ParameterNames.Name, out var nameProperty )
|
|
? nameProperty.GetValueOrDefault<string>()
|
|
: null;
|
|
}
|
|
|
|
public Color GetPrimaryColor( GraphView view )
|
|
{
|
|
if ( Node.HasErrors() )
|
|
{
|
|
return Theme.Red;
|
|
}
|
|
|
|
if ( Node.Definition.Identifier is "scene.ref" or "resource.ref" && !HasTitleBar )
|
|
{
|
|
return view.GetHandleConfig( Outputs.First( x => x.Identifier == "_result" ).Type ).Color;
|
|
}
|
|
|
|
var baseColor = Node.Kind switch
|
|
{
|
|
_ when Node.Definition.Identifier.StartsWith( "var." ) => Color.Lerp( new Color( 0.7f, 0.7f, 0.7f ), Color.Parse( "#811EFC" )!.Value, 0.5f ),
|
|
NodeKind.Action => Node.Binding.IsAsync ? ActionGraphTheme.AsyncActionColor : ActionGraphTheme.ActionColor,
|
|
NodeKind.Expression => ActionGraphTheme.ExpressionColor,
|
|
_ => throw new NotImplementedException()
|
|
};
|
|
|
|
return baseColor;
|
|
}
|
|
|
|
private static GameObject GetTargetObject( Node node )
|
|
{
|
|
switch ( node.Definition.Identifier )
|
|
{
|
|
case "scene.ref":
|
|
return SceneReferenceHelper.ResolveTargetComponent( node )?.GameObject
|
|
?? SceneReferenceHelper.ResolveTargetGameObject( node );
|
|
|
|
default:
|
|
if ( node.Inputs.Values.FirstOrDefault( x => x.IsTarget ) is { Link.Source.Node: { } sourceNode } && sourceNode.Parent == node )
|
|
{
|
|
return GetTargetObject( sourceNode );
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static Pixmap DefaultAssetThumbnail { get; } = Pixmap.FromFile( "common/document_sm.png" );
|
|
|
|
private static Pixmap GetNodeThumbnail( Node node )
|
|
{
|
|
switch ( node.Definition.Identifier )
|
|
{
|
|
case "resource.ref":
|
|
if ( node.Properties.TryGetValue( "value", out var valueProperty )
|
|
&& valueProperty.Value is Resource resource
|
|
&& AssetSystem.FindByPath( resource.ResourcePath ) is { } asset )
|
|
{
|
|
return asset.GetAssetThumb();
|
|
}
|
|
|
|
return DefaultAssetThumbnail;
|
|
|
|
default:
|
|
if ( GetTargetObject( node ) is { } go )
|
|
{
|
|
return go.GetGameObjectThumb();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
[Hide] public Pixmap Thumbnail { get; private set; }
|
|
|
|
private void UpdateThumbnail()
|
|
{
|
|
Thumbnail = GetNodeThumbnail( Node );
|
|
}
|
|
|
|
[Hide]
|
|
public string OverrideTitle { get; set; }
|
|
|
|
public Vector2 Position
|
|
{
|
|
get => _position.Value;
|
|
set => _position.Value = value;
|
|
}
|
|
|
|
[Hide] public Vector2 ExpandSize => default;
|
|
|
|
[Hide] public bool AutoSize => true;
|
|
|
|
[Hide] private bool _forceChange;
|
|
|
|
public class PlugCollection<T> : IEnumerable<T>
|
|
where T : IActionPlug
|
|
{
|
|
private readonly Func<IEnumerable<(Node Node, string Name, int Index)>> _getKeys;
|
|
private readonly Func<(Node Node, string Name, int Index), T> _createPlug;
|
|
|
|
private readonly Dictionary<(Node Node, string Name, int Index), T> _plugs = new();
|
|
|
|
private (Node Node, string Name, int Index)[] _sortedKeys;
|
|
|
|
public int Count => _plugs.Count;
|
|
|
|
public PlugCollection( Func<IEnumerable<(Node Node, string Name, int Index)>> getKeys, Func<(Node Node, string Name, int Index), T> createPlug )
|
|
{
|
|
_getKeys = getKeys;
|
|
_createPlug = createPlug;
|
|
}
|
|
|
|
public bool Update()
|
|
{
|
|
_sortedKeys = _getKeys().ToArray();
|
|
var changed = false;
|
|
|
|
foreach ( var key in _plugs.Keys.Where( x => !_sortedKeys.Contains( x ) ).ToArray() )
|
|
{
|
|
_plugs.Remove( key );
|
|
changed = true;
|
|
}
|
|
|
|
foreach ( var key in _sortedKeys )
|
|
{
|
|
if ( _plugs.TryGetValue( key, out var plug ) )
|
|
{
|
|
if ( plug.LastType != plug.Type )
|
|
{
|
|
plug.LastType = plug.Type;
|
|
changed = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_plugs[key] = _createPlug( key );
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
public T this[Node node, string name, int index] => _plugs.GetValueOrDefault( (node, name, index) );
|
|
public T this[Node node, string name] => _plugs.GetValueOrDefault( (node, name, 0) );
|
|
public T this[Node.IParameter parameter] => this[parameter.Node, parameter.Name];
|
|
|
|
public IEnumerator<T> GetEnumerator()
|
|
{
|
|
return (_sortedKeys ?? Enumerable.Empty<(Node Node, string Name, int Index)>())
|
|
.Select( x => this[x.Node, x.Name, x.Index] )
|
|
.GetEnumerator();
|
|
}
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
}
|
|
|
|
[Hide]
|
|
IEnumerable<IPlugIn> INode.Inputs => Inputs;
|
|
|
|
[Hide]
|
|
IEnumerable<IPlugOut> INode.Outputs => Outputs;
|
|
|
|
void INode.OnPaint( Rect rect )
|
|
{
|
|
|
|
}
|
|
|
|
private bool TryGetDeclaringType( out TypeDescription typeDesc )
|
|
{
|
|
if ( Node.Properties.TryGetValue( ParameterNames.Type, out var typeProperty ) && typeProperty.Value is Type type )
|
|
{
|
|
typeDesc = GlobalGameNamespace.TypeLibrary.GetType( type );
|
|
return typeDesc != null;
|
|
}
|
|
|
|
if ( !Node.Inputs.TryGetValue( ParameterNames.Target, out var targetInput ) || targetInput.SourceType is null )
|
|
{
|
|
typeDesc = null;
|
|
return false;
|
|
}
|
|
|
|
typeDesc = GlobalGameNamespace.TypeLibrary.GetType( targetInput.SourceType );
|
|
return typeDesc != null;
|
|
}
|
|
|
|
[Hide]
|
|
public Action GoToDefinition
|
|
{
|
|
get
|
|
{
|
|
if ( GetTargetObject( Node ) is { } go )
|
|
{
|
|
if ( SceneEditorSession.Resolve( go.Scene ) is { } session )
|
|
{
|
|
return () =>
|
|
{
|
|
session.MakeActive();
|
|
session.BringToFront();
|
|
session.Selection.Set( go );
|
|
|
|
EditorWindow.Focus();
|
|
|
|
SceneEditorMenus.Frame();
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
switch ( Definition.Identifier )
|
|
{
|
|
case "graph":
|
|
{
|
|
var graphPath = Node.Properties["graph"].Value as string;
|
|
|
|
if ( string.IsNullOrEmpty( graphPath ) )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( ResourceLibrary.TryGet( graphPath, out ActionGraphResource graphResource ) )
|
|
{
|
|
return () => ActionGraphView.Open( graphResource.Graph );
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
case "property":
|
|
{
|
|
if ( !TryGetDeclaringType( out var typeDesc ) )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( Node.Properties.Name.Value is not string propertyName )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( typeDesc.GetProperty( propertyName ) is not ISourcePathProvider
|
|
{
|
|
Path: not null
|
|
} property )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return () => CodeEditor.OpenFile( property );
|
|
}
|
|
|
|
case "call":
|
|
{
|
|
if ( !TryGetDeclaringType( out var typeDesc ) )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( Node.Properties.Name.Value is not string methodName )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( typeDesc.GetMethod( methodName ) is not ISourcePathProvider { Path: not null } method )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return () => CodeEditor.OpenFile( method );
|
|
}
|
|
|
|
default:
|
|
{
|
|
if ( Definition.Attributes.OfType<SourceLocationAttribute>().FirstOrDefault() is not
|
|
{ Path: not null } sourceLocation )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return () => CodeEditor.OpenFile( sourceLocation );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnDoubleClick( MouseEvent e )
|
|
{
|
|
if ( GoToDefinition is { } action )
|
|
{
|
|
e.Accepted = true;
|
|
action();
|
|
}
|
|
}
|
|
|
|
[Hide]
|
|
public bool HasTitleBar
|
|
{
|
|
get
|
|
{
|
|
return !Definition.IsOperator() && (Definition.Identifier is not "scene.ref" and not "resource.ref" || Outputs.Count > 1);
|
|
}
|
|
}
|
|
|
|
public virtual NodeUI CreateUI( GraphView view )
|
|
{
|
|
return new NodeUI( view, this );
|
|
}
|
|
|
|
public Menu CreateContextMenu( NodeUI node )
|
|
{
|
|
if ( Node.Definition == EditorNodeLibrary.Property
|
|
&& Node.Properties.TryGetValue( ParameterNames.Kind, out var kindProperty ) )
|
|
{
|
|
var menu = new Menu( "Assignment Kind" ) { Icon = "login" };
|
|
|
|
menu.AddOptions( Enum.GetValues<AssignmentKind>(), x =>
|
|
{
|
|
var name = Enum.GetName( x )!;
|
|
var member = typeof( AssignmentKind ).GetMember( name ).First();
|
|
|
|
var title = member.GetCustomAttribute<Facepunch.ActionGraphs.TitleAttribute>()?.Value ?? name.ToTitleCase();
|
|
var group = member.GetCustomAttribute<Facepunch.ActionGraphs.GroupAttribute>()?.Value ?? "Assignment";
|
|
var icon = member.GetCustomAttribute<Facepunch.ActionGraphs.IconAttribute>()?.Value;
|
|
var desc = member.GetCustomAttribute<Facepunch.ActionGraphs.DescriptionAttribute>()?.Value;
|
|
|
|
return new Menu.PathElement[]
|
|
{
|
|
new ( group, IsHeading: true, Order: (int) x ),
|
|
new( title, icon, desc, Order: (int) x )
|
|
};
|
|
}, x =>
|
|
{
|
|
kindProperty.Value = x;
|
|
MarkDirty();
|
|
} );
|
|
|
|
return menu;
|
|
}
|
|
|
|
if ( Node.Definition == EditorNodeLibrary.Variable
|
|
&& Node.Properties.TryGetValue( ParameterNames.Variable, out var varProperty )
|
|
&& varProperty.Value is Variable variable )
|
|
{
|
|
var menu = new Menu( "Edit Variable" ) { Icon = "storage" };
|
|
|
|
menu.AddMenu( "Rename", "label" )
|
|
.AddLineEdit( "Name", variable.Name, autoFocus: true, onSubmit: name =>
|
|
{
|
|
try
|
|
{
|
|
variable.Name = name;
|
|
|
|
Graph.MarkDirty( variable.References
|
|
.Select( x => x.Node ) );
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
Log.Error( e );
|
|
}
|
|
} );
|
|
|
|
var typeMenu = TypeControlWidget.CreateMenu( action: type =>
|
|
{
|
|
variable.Type = type;
|
|
} );
|
|
|
|
typeMenu.Title = "Change Type";
|
|
typeMenu.Icon = "edit";
|
|
|
|
menu.AddMenu( typeMenu );
|
|
|
|
return menu;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
[Hide]
|
|
private readonly List<IUserDataProperty> _userDataProperties = new();
|
|
|
|
[Hide]
|
|
private readonly UserDataProperty<Vector2> _position;
|
|
|
|
[Hide]
|
|
public PlugCollection<ActionInputPlug> Inputs { get; }
|
|
|
|
[Hide]
|
|
public PlugCollection<ActionOutputPlug> Outputs { get; }
|
|
|
|
private static bool IsHiddenInput( Node.Input input )
|
|
{
|
|
if ( input.Link?.Source is not { } output )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( output.GetLabel()?.StartsWith( "__" ) ?? false )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( input.IsTarget && output.Node.Parent == input.Node )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public EditorNode( EditorActionGraph graph, Node node )
|
|
{
|
|
Graph = graph;
|
|
Node = node;
|
|
Identifier = $"{node.Id}";
|
|
|
|
_position = AddUserDataProperty<Vector2>( nameof( Position ) );
|
|
|
|
Inputs = new PlugCollection<ActionInputPlug>(
|
|
() => node.Inputs
|
|
.Where( x => !IsHiddenInput( x.Value ) )
|
|
.Select( x => x.Value )
|
|
.SelectMany( x =>
|
|
x.LinkArray?
|
|
.Select( ( _, i ) => (node, x.Name, i) )
|
|
.Concat( new (Node, string, int)[] { (node, x.Name, x.LinkArray.Count) } ) ??
|
|
new (Node, string, int)[] { (node, x.Name, 0) } ),
|
|
key => new ActionInputPlug( this, key.Node.Inputs, key.Name, key.Index ) );
|
|
Outputs = new PlugCollection<ActionOutputPlug>(
|
|
() => node.Outputs
|
|
.Where( x => ShouldIncludeOutput( x.Value ) )
|
|
.SelectMany( x => x.Value.GetExpandedOutputs( true ) )
|
|
.Select( x => (x.Node, x.Name, 0) ),
|
|
key => new ActionOutputPlug( this, key.Node.Outputs, key.Name, 0 ) );
|
|
|
|
Update();
|
|
}
|
|
|
|
private static bool ShouldIncludeOutput( Node.Output output )
|
|
{
|
|
if ( output.Node.Definition != EditorNodeLibrary.Input )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( output.Node.ActionGraph.Inputs.Values.FirstOrDefault( x => x.IsTarget )?.Name == output.Name )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( output.Type == typeof( CancellationToken ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected UserDataProperty<T> AddUserDataProperty<T>( string name, T defaultValue = default )
|
|
{
|
|
var property = new UserDataProperty<T>( Node.UserData, name, defaultValue );
|
|
|
|
_userDataProperties.Add( property );
|
|
|
|
return property;
|
|
}
|
|
|
|
public void MarkDirty()
|
|
{
|
|
_forceChange = true;
|
|
Graph.MarkDirty( this );
|
|
}
|
|
|
|
public void InvalidateUserData()
|
|
{
|
|
foreach ( var property in _userDataProperties )
|
|
{
|
|
property.Invalidate();
|
|
}
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
var changed = _forceChange;
|
|
|
|
_forceChange = false;
|
|
|
|
changed |= Inputs.Update();
|
|
changed |= Outputs.Update();
|
|
changed |= UpdateProperties();
|
|
|
|
if ( UpdateMessages() )
|
|
{
|
|
changed = true;
|
|
|
|
IsReachable = !_messages.Any( x => x is { Level: MessageLevel.Warning, Value: "Node is unreachable." } && x.Context == Node );
|
|
}
|
|
|
|
if ( changed )
|
|
{
|
|
UpdateThumbnail();
|
|
Changed?.Invoke();
|
|
}
|
|
}
|
|
|
|
private bool UpdateProperties()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
[Hide]
|
|
private ValidationMessage[] _messages = Array.Empty<ValidationMessage>();
|
|
|
|
[Hide]
|
|
public bool IsReachable { get; private set; } = true;
|
|
|
|
private bool UpdateMessages()
|
|
{
|
|
var oldMessages = _messages;
|
|
var newMessages = _messages = Node.GetMessages().ToArray();
|
|
|
|
if ( oldMessages.Length != newMessages.Length )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
for ( var i = 0; i < oldMessages.Length; i++ )
|
|
{
|
|
if ( !oldMessages[i].Equals( newMessages[i] ) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public class RerouteEditorNode : EditorNode, IRerouteNode
|
|
{
|
|
public string Comment
|
|
{
|
|
get => Node.UserData["Comment"]?.GetValue<string>();
|
|
set => Node.UserData["Comment"] = value;
|
|
}
|
|
|
|
public RerouteEditorNode( EditorActionGraph graph, Node node )
|
|
: base( graph, node )
|
|
{
|
|
}
|
|
|
|
public override NodeUI CreateUI( GraphView view )
|
|
{
|
|
return new RerouteUI( view, this );
|
|
}
|
|
}
|
|
|
|
public class CommentEditorNode : EditorNode, ICommentNode
|
|
{
|
|
[Hide]
|
|
private readonly UserDataProperty<int> _layer;
|
|
|
|
[Hide]
|
|
private readonly UserDataProperty<Vector2> _size;
|
|
|
|
[Hide]
|
|
private readonly UserDataProperty<CommentColor> _color;
|
|
|
|
[Hide]
|
|
private readonly UserDataProperty<string> _title;
|
|
|
|
[Hide]
|
|
private readonly UserDataProperty<string> _description;
|
|
|
|
[Hide]
|
|
public override Sandbox.DisplayInfo DisplayInfo => new()
|
|
{
|
|
Name = Title,
|
|
Description = Description,
|
|
Icon = "notes"
|
|
};
|
|
|
|
public CommentEditorNode( EditorActionGraph graph, Node node )
|
|
: base( graph, node )
|
|
{
|
|
_layer = AddUserDataProperty<int>( nameof( Layer ) );
|
|
_size = AddUserDataProperty<Vector2>( nameof( Size ) );
|
|
_color = AddUserDataProperty( nameof( Color ), CommentColor.Green );
|
|
_title = AddUserDataProperty( nameof( Title ), "Unnamed" );
|
|
_description = AddUserDataProperty( nameof( Description ), "" );
|
|
}
|
|
|
|
public override NodeUI CreateUI( GraphView view )
|
|
{
|
|
return new CommentUI( view, this );
|
|
}
|
|
|
|
[Hide]
|
|
public int Layer
|
|
{
|
|
get => _layer.Value;
|
|
set => _layer.Value = value;
|
|
}
|
|
|
|
[Hide]
|
|
public Vector2 Size
|
|
{
|
|
get => _size.Value;
|
|
set => _size.Value = value;
|
|
}
|
|
|
|
public CommentColor Color
|
|
{
|
|
get => _color.Value;
|
|
set => _color.Value = value;
|
|
}
|
|
|
|
public string Title
|
|
{
|
|
get => _title.Value;
|
|
set => _title.Value = value;
|
|
}
|
|
|
|
public string Description
|
|
{
|
|
get => _description.Value;
|
|
set => _description.Value = value;
|
|
}
|
|
}
|