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() : 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 : IEnumerable where T : IActionPlug { private readonly Func> _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> 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 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 INode.Inputs => Inputs; [Hide] IEnumerable 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().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(), x => { var name = Enum.GetName( x )!; var member = typeof( AssignmentKind ).GetMember( name ).First(); var title = member.GetCustomAttribute()?.Value ?? name.ToTitleCase(); var group = member.GetCustomAttribute()?.Value ?? "Assignment"; var icon = member.GetCustomAttribute()?.Value; var desc = member.GetCustomAttribute()?.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 _userDataProperties = new(); [Hide] private readonly UserDataProperty _position; [Hide] public PlugCollection Inputs { get; } [Hide] public PlugCollection 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( nameof( Position ) ); Inputs = new PlugCollection( () => 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( () => 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 AddUserDataProperty( string name, T defaultValue = default ) { var property = new UserDataProperty( 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(); [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(); 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 _layer; [Hide] private readonly UserDataProperty _size; [Hide] private readonly UserDataProperty _color; [Hide] private readonly UserDataProperty _title; [Hide] private readonly UserDataProperty _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( nameof( Layer ) ); _size = AddUserDataProperty( 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; } }