using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Events; using Artemis.Core.Internal; using Artemis.Storage.Entities.Profile.Nodes; using JetBrains.Annotations; namespace Artemis.Core; /// /// Represents a node script /// public abstract class NodeScript : CorePropertyChanged, INodeScript { private void NodeTypeStoreOnNodeTypeAdded(object? sender, NodeTypeStoreEvent e) { if (Entity.Nodes.Any(n => e.TypeRegistration.MatchesEntity(n))) Load(); } private void NodeTypeStoreOnNodeTypeRemoved(object? sender, NodeTypeStoreEvent e) { List nodes = Nodes.Where(n => n.GetType() == e.TypeRegistration.NodeData.Type).ToList(); foreach (INode node in nodes) RemoveNode(node); } /// public event EventHandler>? NodeAdded; /// public event EventHandler>? NodeRemoved; #region Properties & Fields /// /// Gets the entity used to store this script. /// public NodeScriptEntity Entity { get; private set; } /// public string Name { get; } /// public string Description { get; } private readonly List _nodes = new(); /// public IEnumerable Nodes => new ReadOnlyCollection(_nodes); /// /// Gets or sets the exit node of the script /// protected INode ExitNode { get; set; } /// /// Gets a boolean indicating whether the exit node is connected to any other nodes /// public abstract bool ExitNodeConnected { get; } /// public abstract Type ResultType { get; } /// public object? Context { get; set; } #endregion #region Constructors /// /// Creates a new instance of the class with a name, description and optional context /// /// The name of the node script /// The description of the node script /// /// The context of the node script, usually a or /// /// /// A list of default nodes to add to the node script. protected NodeScript(string name, string description, object? context = null, List? defaultNodes = null) { Name = name; Description = description; Context = context; Entity = new NodeScriptEntity(); ExitNode = null!; NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded; NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved; if (defaultNodes != null) foreach (DefaultNode defaultNode in defaultNodes) AddNode(defaultNode); } internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List? defaultNodes = null) { Name = name; Description = description; Entity = entity; Context = context; ExitNode = null!; NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded; NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved; if (defaultNodes != null) foreach (DefaultNode defaultNode in defaultNodes) AddNode(defaultNode); } #endregion #region Methods /// public void Run() { lock (_nodes) { foreach (INode node in _nodes) node.Reset(); } ExitNode.TryEvaluate(); } /// public void AddNode(INode node) { lock (_nodes) { _nodes.Add(node); } NodeAdded?.Invoke(this, new SingleValueEventArgs(node)); } /// public void RemoveNode(INode node) { lock (_nodes) { _nodes.Remove(node); } NodeRemoved?.Invoke(this, new SingleValueEventArgs(node)); } /// public void Dispose() { NodeTypeStore.NodeTypeAdded -= NodeTypeStoreOnNodeTypeAdded; NodeTypeStore.NodeTypeRemoved -= NodeTypeStoreOnNodeTypeRemoved; lock (_nodes) { foreach (INode node in _nodes) { if (node is IDisposable disposable) disposable.Dispose(); } } } #endregion #region Implementation of IStorageModel /// public void Load() { lock (_nodes) { // Remove nodes no longer on the entity List removeNodes = _nodes.Where(n => Entity.Nodes.All(e => e.Id != n.Id)).ToList(); foreach (INode removeNode in removeNodes) { RemoveNode(removeNode); if (removeNode is IDisposable disposable) disposable.Dispose(); } } // Create missing nodes nodes foreach (NodeEntity nodeEntity in Entity.Nodes) { INode? node = Nodes.FirstOrDefault(n => n.Id == nodeEntity.Id); // If the node already exists, apply the entity to it if (node != null) { LoadExistingNode(node, nodeEntity); } else { INode? loaded = LoadNode(nodeEntity); if (loaded != null) AddNode(loaded); } } LoadConnections(); } internal void LoadFromEntity(NodeScriptEntity entity) { Entity = entity; Load(); } private void LoadExistingNode(INode node, NodeEntity nodeEntity) { node.Id = nodeEntity.Id; node.X = nodeEntity.X; node.Y = nodeEntity.Y; // Restore pin collections foreach (NodePinCollectionEntity entityNodePinCollection in nodeEntity.PinCollections) { IPinCollection? collection = node.PinCollections.ElementAtOrDefault(entityNodePinCollection.Id); if (collection == null) continue; while (collection.Count() > entityNodePinCollection.Amount) collection.Remove(collection.Last()); while (collection.Count() < entityNodePinCollection.Amount) collection.Add(collection.CreatePin()); } } private INode? LoadNode(NodeEntity nodeEntity) { NodeTypeRegistration? nodeTypeRegistration = NodeTypeStore.Get(nodeEntity); if (nodeTypeRegistration == null) return null; // Create the node INode node = nodeTypeRegistration.NodeData.CreateNode(this, nodeEntity); LoadExistingNode(node, nodeEntity); return node; } /// public void LoadConnections() { List nodes = Nodes.ToList(); foreach (NodeConnectionEntity nodeConnectionEntity in Entity.Connections.OrderBy(p => p.SourcePinCollectionId)) { INode? source = nodes.FirstOrDefault(n => n.Id == nodeConnectionEntity.SourceNode); if (source == null) continue; INode? target = nodes.FirstOrDefault(n => n.Id == nodeConnectionEntity.TargetNode); if (target == null) continue; IPin? sourcePin = nodeConnectionEntity.SourcePinCollectionId == -1 ? source.Pins.ElementAtOrDefault(nodeConnectionEntity.SourcePinId) : source.PinCollections.ElementAtOrDefault(nodeConnectionEntity.SourcePinCollectionId)?.ElementAtOrDefault(nodeConnectionEntity.SourcePinId); IPin? targetPin = nodeConnectionEntity.TargetPinCollectionId == -1 ? target.Pins.ElementAtOrDefault(nodeConnectionEntity.TargetPinId) : target.PinCollections.ElementAtOrDefault(nodeConnectionEntity.TargetPinCollectionId)?.ElementAtOrDefault(nodeConnectionEntity.TargetPinId); // Ensure both nodes have the required pins if (sourcePin == null || targetPin == null) continue; // Ensure the connection is valid if (sourcePin.Direction == targetPin.Direction) continue; // Clear existing connections on input pins, we don't want none of that now if (targetPin.Direction == PinDirection.Input) while (targetPin.ConnectedTo.Any()) targetPin.DisconnectFrom(targetPin.ConnectedTo[0]); if (sourcePin.Direction == PinDirection.Input) while (sourcePin.ConnectedTo.Any()) sourcePin.DisconnectFrom(sourcePin.ConnectedTo[0]); // Only connect the nodes if they aren't already connected (LoadConnections may be called twice or more) // Type checking is done later when all connections are in place if (!targetPin.ConnectedTo.Contains(sourcePin)) targetPin.ConnectTo(sourcePin); if (!sourcePin.ConnectedTo.Contains(targetPin)) sourcePin.ConnectTo(targetPin); } // With all connections restored, ensure types match (connecting pins may affect types so the check is done afterwards) foreach (INode node in nodes) { foreach (IPin nodePin in node.Pins.Concat(node.PinCollections.SelectMany(p => p))) { List toDisconnect = nodePin.ConnectedTo.Where(c => !c.IsTypeCompatible(nodePin.Type)).ToList(); foreach (IPin pin in toDisconnect) pin.DisconnectFrom(nodePin); } } } /// public void Save() { Entity.Name = Name; Entity.Description = Description; Entity.Nodes.Clear(); foreach (INode node in Nodes) { NodeEntity nodeEntity = new() { Id = node.Id, PluginId = NodeTypeStore.GetPlugin(node)?.Guid ?? Constants.CorePlugin.Guid, Type = node.GetType().Name, X = node.X, Y = node.Y, Name = node.Name, Description = node.Description, IsExitNode = node.IsExitNode }; if (node is Node nodeImplementation) nodeEntity.Storage = nodeImplementation.SerializeStorage(); int collectionId = 0; foreach (IPinCollection nodePinCollection in node.PinCollections) { nodeEntity.PinCollections.Add(new NodePinCollectionEntity { Id = collectionId, Direction = (int) nodePinCollection.Direction, Amount = nodePinCollection.Count() }); collectionId++; } Entity.Nodes.Add(nodeEntity); } // Store connections Entity.Connections.Clear(); foreach (INode node in Nodes) { SavePins(node, -1, node.Pins); int pinCollectionId = 0; foreach (IPinCollection pinCollection in node.PinCollections) { SavePins(node, pinCollectionId, pinCollection); pinCollectionId++; } } } private void SavePins(INode node, int collectionId, IEnumerable pins) { int sourcePinId = 0; foreach (IPin sourcePin in pins) { if (sourcePin.Direction == PinDirection.Output) { sourcePinId++; continue; } foreach (IPin targetPin in sourcePin.ConnectedTo) { int targetPinCollectionId = -1; int targetPinId; IPinCollection? targetCollection = targetPin.Node.PinCollections.FirstOrDefault(c => c.Contains(targetPin)); if (targetCollection != null) { targetPinCollectionId = targetPin.Node.PinCollections.IndexOf(targetCollection); targetPinId = targetCollection.ToList().IndexOf(targetPin); } else { targetPinId = targetPin.Node.Pins.IndexOf(targetPin); } Entity.Connections.Add(new NodeConnectionEntity { SourceType = sourcePin.Type.Name, SourceNode = node.Id, SourcePinCollectionId = collectionId, SourcePinId = sourcePinId, TargetType = targetPin.Type.Name, TargetNode = targetPin.Node.Id, TargetPinCollectionId = targetPinCollectionId, TargetPinId = targetPinId }); } sourcePinId++; } } #endregion } /// /// Represents a node script with a result value of type /// /// The type of result value public class NodeScript : NodeScript, INodeScript { #region Properties & Fields /// public T? Result => ((ExitNode) ExitNode).Value; /// public override bool ExitNodeConnected => ((ExitNode) ExitNode).Input.ConnectedTo.Any(); /// public override Type ResultType => typeof(T); #endregion #region Constructors /// public NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List? defaultNodes = null) : base(name, description, entity, context, defaultNodes) { ExitNode = new ExitNode(name, description); AddNode(ExitNode); Load(); } /// public NodeScript(string name, string description, object? context = null, List? defaultNodes = null) : base(name, description, context, defaultNodes) { ExitNode = new ExitNode(name, description); AddNode(ExitNode); Save(); } [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] private NodeScript(string name, string description, INode exitNode) : base(name, description) { ExitNode = exitNode; AddNode(ExitNode); } #endregion }