// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. //#define DEBUG_INVOKE_METHODS_SEARCHING //#define DEBUG_FIELDS_SEARCHING using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using FlaxEditor.Content; using FlaxEditor.GUI.Drag; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEditor.Surface.ContextMenu; using FlaxEditor.Surface.Elements; using FlaxEngine; using FlaxEngine.GUI; using Object = FlaxEngine.Object; namespace FlaxEditor.Surface { /// /// The Visject Surface implementation for the Visual Script editor. /// /// [HideInEditor] public class VisualScriptSurface : VisjectSurface { private readonly GroupArchetype _methodOverridesGroupArchetype = new GroupArchetype { GroupID = 16, Name = "Method Overrides", Color = new Color(109, 160, 24), Archetypes = new List(), }; internal static class NodesCache { private static readonly object _locker = new object(); private static int _version; private static Task _task; private static VisjectCM _taskContextMenu; private static Dictionary, GroupArchetype> _cache; public static void Wait() { _task?.Wait(); } public static void Clear() { Wait(); if (_cache != null && _cache.Count != 0) { OnCodeEditingTypesCleared(); } } public static void Get(VisjectCM contextMenu) { Wait(); lock (_locker) { if (_cache == null) _cache = new Dictionary, GroupArchetype>(); contextMenu.LockChildrenRecursive(); // Check if has cached groups if (_cache.Count != 0) { // Check if context menu doesn;t have the recent cached groups if (!contextMenu.Groups.Any(g => g.Archetype.Tag is int asInt && asInt == _version)) { var groups = contextMenu.Groups.Where(g => g.Archetype.Tag is int).ToArray(); foreach (var g in groups) contextMenu.RemoveGroup(g); foreach (var g in _cache.Values) contextMenu.AddGroup(g); } } else { // Remove any old groups from context menu var groups = contextMenu.Groups.Where(g => g.Archetype.Tag is int).ToArray(); foreach (var g in groups) contextMenu.RemoveGroup(g); // Register for scripting types reload Editor.Instance.CodeEditing.TypesCleared += OnCodeEditingTypesCleared; // Run caching on an async _task = Task.Run(OnActiveContextMenuShowAsync); _taskContextMenu = contextMenu; } contextMenu.UnlockChildrenRecursive(); } } private static void OnActiveContextMenuShowAsync() { Profiler.BeginEvent("Setup Visual Script Context Menu (async)"); #if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING var searchStartTime = DateTime.Now; var searchHitsCount = 0; #endif foreach (var scriptType in Editor.Instance.CodeEditing.All.Get()) { if (!SurfaceUtils.IsValidVisualScriptType(scriptType)) continue; // Skip Newtonsoft.Json stuff var scriptTypeTypeName = scriptType.TypeName; if (scriptTypeTypeName.StartsWith("Newtonsoft.Json.")) continue; var scriptTypeName = scriptType.Name; // Enum if (scriptType.IsEnum) { // Create node archetype var node = (NodeArchetype)Archetypes.Constants.Nodes[10].Clone(); node.DefaultValues[0] = Activator.CreateInstance(scriptType.Type); node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = scriptTypeName; node.Description = scriptTypeTypeName; var attributes = scriptType.GetAttributes(false); var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute); if (tooltipAttribute != null) node.Description += "\n" + tooltipAttribute.Text; // Create group archetype var groupKey = new KeyValuePair(scriptTypeName, 2); if (!_cache.TryGetValue(groupKey, out var group)) { group = new GroupArchetype { GroupID = groupKey.Value, Name = groupKey.Key, Color = new Color(243, 156, 18), Tag = _version, Archetypes = new List(), }; _cache.Add(groupKey, group); } // Add node to the group ((IList)group.Archetypes).Add(node); continue; } // Structure if (scriptType.IsValueType) { if (scriptType.IsVoid) continue; // Create group archetype var groupKey = new KeyValuePair(scriptTypeName, 4); if (!_cache.TryGetValue(groupKey, out var group)) { group = new GroupArchetype { GroupID = groupKey.Value, Name = groupKey.Key, Color = new Color(155, 89, 182), Tag = _version, Archetypes = new List(), }; _cache.Add(groupKey, group); } var attributes = scriptType.GetAttributes(false); var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute); // Create Pack node archetype var node = (NodeArchetype)Archetypes.Packing.Nodes[6].Clone(); node.DefaultValues[0] = scriptTypeTypeName; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = "Pack " + scriptTypeName; node.Description = scriptTypeTypeName; if (tooltipAttribute != null) node.Description += "\n" + tooltipAttribute.Text; ((IList)group.Archetypes).Add(node); // Create Unpack node archetype node = (NodeArchetype)Archetypes.Packing.Nodes[13].Clone(); node.DefaultValues[0] = scriptTypeTypeName; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = "Unpack " + scriptTypeName; node.Description = scriptTypeTypeName; if (tooltipAttribute != null) node.Description += "\n" + tooltipAttribute.Text; ((IList)group.Archetypes).Add(node); } foreach (var member in scriptType.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)) { if (member.IsGeneric) continue; if (member.IsMethod) { // Skip methods not declared in this type if (member.Type is MethodInfo m && m.GetBaseDefinition().DeclaringType != m.DeclaringType) continue; var name = member.Name; if (name == "ToString") continue; // Skip if searching by name doesn't return a match var members = scriptType.GetMembers(name, MemberTypes.Method, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); if (!members.Contains(member)) continue; // Check if method is valid for Visual Script usage if (SurfaceUtils.IsValidVisualScriptInvokeMethod(member, out var parameters)) { // Create node archetype var node = (NodeArchetype)Archetypes.Function.Nodes[3].Clone(); node.DefaultValues[0] = scriptTypeTypeName; node.DefaultValues[1] = name; node.DefaultValues[2] = parameters.Length; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = SurfaceUtils.GetMethodDisplayName((string)node.DefaultValues[1]); node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member); node.SubTitle = string.Format(" (in {0})", scriptTypeName); node.Tag = member; // Create group archetype var groupKey = new KeyValuePair(scriptTypeName, 16); if (!_cache.TryGetValue(groupKey, out var group)) { group = new GroupArchetype { GroupID = groupKey.Value, Name = groupKey.Key, Color = new Color(109, 160, 24), Tag = _version, Archetypes = new List(), }; _cache.Add(groupKey, group); } // Add node to the group ((IList)group.Archetypes).Add(node); #if DEBUG_INVOKE_METHODS_SEARCHING Editor.LogWarning(scriptTypeTypeName + " -> " + member.GetSignature()); searchHitsCount++; #endif } } else if (member.IsField) { var name = member.Name; // Skip if searching by name doesn't return a match var members = scriptType.GetMembers(name, MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); if (!members.Contains(member)) continue; // Check if field is valid for Visual Script usage if (SurfaceUtils.IsValidVisualScriptField(member)) { if (member.HasGet) { // Create node archetype var node = (NodeArchetype)Archetypes.Function.Nodes[6].Clone(); node.DefaultValues[0] = scriptTypeTypeName; node.DefaultValues[1] = name; node.DefaultValues[2] = member.ValueType.TypeName; node.DefaultValues[3] = member.IsStatic; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = "Get " + name; node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member); node.SubTitle = string.Format(" (in {0})", scriptTypeName); // Create group archetype var groupKey = new KeyValuePair(scriptTypeName, 16); if (!_cache.TryGetValue(groupKey, out var group)) { group = new GroupArchetype { GroupID = groupKey.Value, Name = groupKey.Key, Color = new Color(109, 160, 24), Tag = _version, Archetypes = new List(), }; _cache.Add(groupKey, group); } // Add node to the group ((IList)group.Archetypes).Add(node); #if DEBUG_FIELDS_SEARCHING Editor.LogWarning(scriptTypeTypeName + " -> Get " + member.GetSignature()); searchHitsCount++; #endif } if (member.HasSet) { // Create node archetype var node = (NodeArchetype)Archetypes.Function.Nodes[7].Clone(); node.DefaultValues[0] = scriptTypeTypeName; node.DefaultValues[1] = name; node.DefaultValues[2] = member.ValueType.TypeName; node.DefaultValues[3] = member.IsStatic; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = "Set " + name; node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member); node.SubTitle = string.Format(" (in {0})", scriptTypeName); // Create group archetype var groupKey = new KeyValuePair(scriptTypeName, 16); if (!_cache.TryGetValue(groupKey, out var group)) { group = new GroupArchetype { GroupID = groupKey.Value, Name = groupKey.Key, Color = new Color(109, 160, 24), Tag = _version, Archetypes = new List(), }; _cache.Add(groupKey, group); } // Add node to the group ((IList)group.Archetypes).Add(node); #if DEBUG_FIELDS_SEARCHING Editor.LogWarning(scriptTypeTypeName + " -> Set " + member.GetSignature()); searchHitsCount++; #endif } } } } } // Add group to context menu (on a main thread) FlaxEngine.Scripting.InvokeOnUpdate(() => { #if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING var addStartTime = DateTime.Now; #endif lock (_locker) { _taskContextMenu.AddGroups(_cache.Values); _taskContextMenu = null; } #if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING Editor.LogError($"Added items to VisjectCM in: {(DateTime.Now - addStartTime).TotalMilliseconds} ms"); #endif }); #if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING Editor.LogError($"Collected {searchHitsCount} items in: {(DateTime.Now - searchStartTime).TotalMilliseconds} ms"); #endif Profiler.EndEvent(); lock (_locker) { _task = null; } } private static void OnCodeEditingTypesCleared() { Wait(); lock (_locker) { _cache.Clear(); _version++; } Editor.Instance.CodeEditing.TypesCleared -= OnCodeEditingTypesCleared; } } private DragActors _dragActors; /// /// The script that is edited by the surface. /// public VisualScript Script; /// /// The list of nodes with breakpoints set. /// public readonly List Breakpoints = new List(); /// public VisualScriptSurface(IVisjectSurfaceOwner owner, Action onSave, FlaxEditor.Undo undo) : base(owner, onSave, undo, null, null, true) { _supportsImplicitCastFromObjectToBoolean = true; DragHandlers.Add(_dragActors = new DragActors(ValidateDragActor)); } private bool ValidateDragActor(ActorNode actor) { return true; } /// public override void OnNodeBreakpointEdited(SurfaceNode node) { base.OnNodeBreakpointEdited(node); if (node.Breakpoint.Set && node.Breakpoint.Enabled) Breakpoints.Add(node); else Breakpoints.Remove(node); } /// public override string GetTypeName(ScriptType type) { if (type.Type == typeof(void)) return "Impulse"; return base.GetTypeName(type); } /// public override bool GetBoxDebuggerTooltip(Box box, out string text) { if (Editor.Instance.Simulation.IsDuringBreakpointHang) { // Find any local variable from the current scope that matches this box var state = Windows.Assets.VisualScriptWindow.GetLocals(); if (state.Locals != null) { for (int i = 0; i < state.Locals.Length; i++) { ref var local = ref state.Locals[i]; if (local.BoxId == box.ID && local.NodeId == box.ParentNode.ID) { text = $"{local.Value ?? string.Empty} ({local.ValueTypeName})"; return true; } } } // Evaluate the value using the Visual Scripting backend if (box.CurrentType != typeof(void)) { var local = new Editor.VisualScriptLocal { NodeId = box.ParentNode.ID, BoxId = box.ID, }; var script = ((Windows.Assets.VisualScriptWindow)box.Surface.Owner).Asset; if (Editor.Internal_EvaluateVisualScriptLocal(Object.GetUnmanagedPtr(script), ref local)) { text = $"{local.Value ?? string.Empty} ({local.ValueTypeName})"; return true; } } } text = null; return false; } /// public override bool CanSetParameters => true; /// public override bool CanUseNodeType(NodeArchetype nodeArchetype) { return (nodeArchetype.Flags & NodeFlags.VisualScriptGraph) != 0 && base.CanUseNodeType(nodeArchetype); } /// protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[2]; } /// protected override void OnShowPrimaryMenu(VisjectCM activeCM, Vector2 location, Box startBox) { Profiler.BeginEvent("Setup Visual Script Context Menu"); // Update nodes for method overrides activeCM.RemoveGroup(_methodOverridesGroupArchetype); if (Script && !Script.WaitForLoaded(100)) { var nodes = (List)_methodOverridesGroupArchetype.Archetypes; nodes.Clear(); // Find methods to override from the inherited types var scriptMeta = Script.Meta; var baseType = TypeUtils.GetType(scriptMeta.BaseTypename); var members = baseType.GetMembers(BindingFlags.Public | BindingFlags.Instance); for (var i = 0; i < members.Length; i++) { ref var member = ref members[i]; if (SurfaceUtils.IsValidVisualScriptOverrideMethod(member, out var parameters)) { var name = member.Name; // Skip already added override method nodes bool isAlreadyAdded = false; foreach (var n in Nodes) { if (n.GroupArchetype.GroupID == 16 && n.Archetype.TypeID == 3 && (string)n.Values[0] == name && (int)n.Values[1] == parameters.Length) { isAlreadyAdded = true; break; } } if (isAlreadyAdded) continue; var node = (NodeArchetype)Archetypes.Function.Nodes[2].Clone(); node.Flags &= ~NodeFlags.NoSpawnViaGUI; var attributes = member.GetAttributes(true); var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute); if (tooltipAttribute != null) node.Description = tooltipAttribute.Text; node.DefaultValues[0] = name; node.DefaultValues[1] = parameters.Length; node.Title = "Override " + name; nodes.Add(node); } } activeCM.AddGroup(_methodOverridesGroupArchetype); } // Update nodes for invoke methods (async) NodesCache.Get(activeCM); Profiler.EndEvent(); base.OnShowPrimaryMenu(activeCM, location, startBox); activeCM.VisibleChanged += OnActiveContextMenuVisibleChanged; } private void OnActiveContextMenuVisibleChanged(Control activeCM) { NodesCache.Wait(); } /// protected override bool ValidateDragItem(AssetItem assetItem) { if (assetItem.IsOfType()) return true; if (assetItem.IsOfType()) return true; return base.ValidateDragItem(assetItem); } /// protected override void HandleDragDropAssets(List objects, DragDropEventArgs args) { for (int i = 0; i < objects.Count; i++) { var assetItem = objects[i]; SurfaceNode node = null; if (assetItem.IsOfType()) { node = Context.SpawnNode(7, 16, args.SurfaceLocation, new object[] { assetItem.ID, string.Empty, }); } else if (assetItem.IsOfType()) { node = Context.SpawnNode(7, 18, args.SurfaceLocation, new object[] { assetItem.ID, }); } if (node != null) { args.SurfaceLocation.X += node.Width + 10; } } base.HandleDragDropAssets(objects, args); } /// public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data) { var args = new DragDropEventArgs { SurfaceLocation = _rootControl.PointFromParent(ref location) }; // Drag actors if (_dragActors.HasValidDrag) { for (int i = 0; i < _dragActors.Objects.Count; i++) { var actorNode = _dragActors.Objects[i]; var node = Context.SpawnNode(7, 21, args.SurfaceLocation, new object[] { actorNode.ID }); args.SurfaceLocation.X += node.Width + 10; } DragHandlers.OnDragDrop(args); return _dragActors.Effect; } return base.OnDragDrop(ref location, data); } /// public override void OnDestroy() { if (IsDisposing) return; NodesCache.Wait(); base.OnDestroy(); } } }