mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
454 lines
14 KiB
C#
454 lines
14 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Represents a node script
|
|
/// </summary>
|
|
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<INode> nodes = Nodes.Where(n => n.GetType() == e.TypeRegistration.NodeData.Type).ToList();
|
|
foreach (INode node in nodes)
|
|
RemoveNode(node);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public event EventHandler<SingleValueEventArgs<INode>>? NodeAdded;
|
|
|
|
/// <inheritdoc />
|
|
public event EventHandler<SingleValueEventArgs<INode>>? NodeRemoved;
|
|
|
|
#region Properties & Fields
|
|
|
|
/// <summary>
|
|
/// Gets the entity used to store this script.
|
|
/// </summary>
|
|
public NodeScriptEntity Entity { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public string Name { get; }
|
|
|
|
/// <inheritdoc />
|
|
public string Description { get; }
|
|
|
|
private readonly List<INode> _nodes = new();
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<INode> Nodes => new ReadOnlyCollection<INode>(_nodes);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the exit node of the script
|
|
/// </summary>
|
|
protected INode ExitNode { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a boolean indicating whether the exit node is connected to any other nodes
|
|
/// </summary>
|
|
public abstract bool ExitNodeConnected { get; }
|
|
|
|
/// <inheritdoc />
|
|
public abstract Type ResultType { get; }
|
|
|
|
/// <inheritdoc />
|
|
public object? Context { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of the <see cref="NodeScript" /> class with a name, description and optional context
|
|
/// </summary>
|
|
/// <param name="name">The name of the node script</param>
|
|
/// <param name="description">The description of the node script</param>
|
|
/// <param name="context">
|
|
/// The context of the node script, usually a <see cref="Profile" /> or
|
|
/// <see cref="ProfileConfiguration" />
|
|
/// </param>
|
|
/// <param name="defaultNodes">A list of default nodes to add to the node script.</param>
|
|
protected NodeScript(string name, string description, object? context = null, List<DefaultNode>? 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<DefaultNode>? 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
|
|
|
|
/// <inheritdoc />
|
|
public void Run()
|
|
{
|
|
lock (_nodes)
|
|
{
|
|
foreach (INode node in _nodes)
|
|
node.Reset();
|
|
}
|
|
|
|
ExitNode.TryEvaluate();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void AddNode(INode node)
|
|
{
|
|
lock (_nodes)
|
|
{
|
|
_nodes.Add(node);
|
|
}
|
|
|
|
NodeAdded?.Invoke(this, new SingleValueEventArgs<INode>(node));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RemoveNode(INode node)
|
|
{
|
|
lock (_nodes)
|
|
{
|
|
_nodes.Remove(node);
|
|
}
|
|
|
|
NodeRemoved?.Invoke(this, new SingleValueEventArgs<INode>(node));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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
|
|
|
|
/// <inheritdoc />
|
|
public void Load()
|
|
{
|
|
lock (_nodes)
|
|
{
|
|
// Remove nodes no longer on the entity
|
|
List<INode> 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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void LoadConnections()
|
|
{
|
|
List<INode> 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<IPin> toDisconnect = nodePin.ConnectedTo.Where(c => !c.IsTypeCompatible(nodePin.Type)).ToList();
|
|
foreach (IPin pin in toDisconnect)
|
|
pin.DisconnectFrom(nodePin);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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<IPin> 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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a node script with a result value of type <typeparamref name="T" />
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of result value</typeparam>
|
|
public class NodeScript<T> : NodeScript, INodeScript<T>
|
|
{
|
|
#region Properties & Fields
|
|
|
|
/// <inheritdoc />
|
|
public T? Result => ((ExitNode<T>) ExitNode).Value;
|
|
|
|
/// <inheritdoc />
|
|
public override bool ExitNodeConnected => ((ExitNode<T>) ExitNode).Input.ConnectedTo.Any();
|
|
|
|
/// <inheritdoc />
|
|
public override Type ResultType => typeof(T);
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <inheritdoc />
|
|
public NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List<DefaultNode>? defaultNodes = null)
|
|
: base(name, description, entity, context, defaultNodes)
|
|
{
|
|
ExitNode = new ExitNode<T>(name, description);
|
|
AddNode(ExitNode);
|
|
|
|
Load();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public NodeScript(string name, string description, object? context = null, List<DefaultNode>? defaultNodes = null)
|
|
: base(name, description, context, defaultNodes)
|
|
{
|
|
ExitNode = new ExitNode<T>(name, description);
|
|
AddNode(ExitNode);
|
|
|
|
Save();
|
|
}
|
|
|
|
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
|
|
private NodeScript(string name, string description, INode exitNode)
|
|
: base(name, description)
|
|
{
|
|
ExitNode = exitNode;
|
|
AddNode(ExitNode);
|
|
}
|
|
|
|
#endregion
|
|
} |