1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00
2023-03-30 23:13:39 +02:00

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
}