using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Artemis.Core.Events; using Ninject; using Ninject.Parameters; namespace Artemis.Core; /// /// Represents a kind of node inside a /// public abstract class Node : BreakableModel, INode { /// public event EventHandler? Resetting; /// public event EventHandler>? PinAdded; /// public event EventHandler>? PinRemoved; /// public event EventHandler>? PinCollectionAdded; /// public event EventHandler>? PinCollectionRemoved; #region Properties & Fields private readonly List _outputPinBucket = new(); private readonly List _inputPinBucket = new(); private Guid _id; /// public Guid Id { get => _id; set => SetAndNotify(ref _id, value); } private string _name; /// public string Name { get => _name; protected set => SetAndNotify(ref _name, value); } private string _description; /// public string Description { get => _description; protected set => SetAndNotify(ref _description, value); } private double _x; /// public double X { get => _x; set => SetAndNotify(ref _x, value); } private double _y; /// public double Y { get => _y; set => SetAndNotify(ref _y, value); } /// public virtual bool IsExitNode => false; /// public virtual bool IsDefaultNode => false; private readonly List _pins = new(); /// public IReadOnlyCollection Pins => new ReadOnlyCollection(_pins); private readonly List _pinCollections = new(); /// public IReadOnlyCollection PinCollections => new ReadOnlyCollection(_pinCollections); /// public override string BrokenDisplayName => Name; #endregion #region Construtors /// /// Creates a new instance of the class with an empty name and description /// protected Node() { _name = string.Empty; _description = string.Empty; _id = Guid.NewGuid(); } /// /// Creates a new instance of the class with the provided name and description /// protected Node(string name, string description) { _name = name; _description = description; _id = Guid.NewGuid(); } #endregion #region Methods /// /// Creates a new input pin and adds it to the collection /// /// The name of the pin /// The type of value the pin will hold /// The newly created pin protected InputPin CreateInputPin(string name = "") { InputPin pin = new(this, name); _pins.Add(pin); PinAdded?.Invoke(this, new SingleValueEventArgs(pin)); return pin; } /// /// Creates a new input pin and adds it to the collection /// /// The type of value the pin will hold /// The name of the pin /// The newly created pin protected InputPin CreateInputPin(Type type, string name = "") { InputPin pin = new(this, type, name); _pins.Add(pin); PinAdded?.Invoke(this, new SingleValueEventArgs(pin)); return pin; } /// /// Creates a new output pin and adds it to the collection /// /// The name of the pin /// The type of value the pin will hold /// The newly created pin protected OutputPin CreateOutputPin(string name = "") { OutputPin pin = new(this, name); _pins.Add(pin); PinAdded?.Invoke(this, new SingleValueEventArgs(pin)); return pin; } /// /// Creates a new output pin and adds it to the collection /// /// The type of value the pin will hold /// The name of the pin /// The newly created pin protected OutputPin CreateOutputPin(Type type, string name = "") { OutputPin pin = new(this, type, name); _pins.Add(pin); PinAdded?.Invoke(this, new SingleValueEventArgs(pin)); return pin; } /// /// Creates or adds an output pin to the node using a bucket. /// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the /// editor. /// protected OutputPin CreateOrAddOutputPin(Type valueType, string displayName) { // Grab the first pin from the bucket that isn't on the node yet OutputPin? pin = _outputPinBucket.FirstOrDefault(p => !Pins.Contains(p)); if (Numeric.IsTypeCompatible(valueType)) valueType = typeof(Numeric); // If there is none, create a new one and add it to the bucket if (pin == null) { pin = CreateOutputPin(valueType, displayName); _outputPinBucket.Add(pin); } // If there was a pin in the bucket, update it's type and display name and reuse it else { pin.ChangeType(valueType); pin.Name = displayName; AddPin(pin); } return pin; } /// /// Creates or adds an input pin to the node using a bucket. /// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the /// editor. /// protected InputPin CreateOrAddInputPin(Type valueType, string displayName) { // Grab the first pin from the bucket that isn't on the node yet InputPin? pin = _inputPinBucket.FirstOrDefault(p => !Pins.Contains(p)); if (Numeric.IsTypeCompatible(valueType)) valueType = typeof(Numeric); // If there is none, create a new one and add it to the bucket if (pin == null) { pin = CreateInputPin(valueType, displayName); _inputPinBucket.Add(pin); } // If there was a pin in the bucket, update it's type and display name and reuse it else { pin.ChangeType(valueType); pin.Name = displayName; AddPin(pin); } return pin; } /// /// Removes the provided from the node and it's collection /// /// The pin to remove /// if the pin was removed; otherwise . protected bool RemovePin(Pin pin) { bool isRemoved = _pins.Remove(pin); if (isRemoved) { pin.DisconnectAll(); PinRemoved?.Invoke(this, new SingleValueEventArgs(pin)); } return isRemoved; } /// /// Adds an existing to the collection. /// /// The pin to add protected void AddPin(Pin pin) { if (pin.Node != this) throw new ArtemisCoreException("Can't add a pin to a node that belongs to a different node than the one it's being added to."); if (_pins.Contains(pin)) return; _pins.Add(pin); PinAdded?.Invoke(this, new SingleValueEventArgs(pin)); } /// /// Creates a new input pin collection and adds it to the collection /// /// The type of value the pins of this collection will hold /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting input pin collection protected InputPinCollection CreateInputPinCollection(string name = "", int initialCount = 1) { InputPinCollection pin = new(this, name, initialCount); _pinCollections.Add(pin); PinCollectionAdded?.Invoke(this, new SingleValueEventArgs(pin)); return pin; } /// /// Creates a new input pin collection and adds it to the collection /// /// The type of value the pins of this collection will hold /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting input pin collection protected InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1) { InputPinCollection pin = new(this, type, name, initialCount); _pinCollections.Add(pin); PinCollectionAdded?.Invoke(this, new SingleValueEventArgs(pin)); return pin; } /// /// Creates a new output pin collection and adds it to the collection /// /// The type of value the pins of this collection will hold /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting output pin collection protected OutputPinCollection CreateOutputPinCollection(string name = "", int initialCount = 1) { OutputPinCollection pin = new(this, name, initialCount); _pinCollections.Add(pin); PinCollectionAdded?.Invoke(this, new SingleValueEventArgs(pin)); return pin; } /// /// Removes the provided from the node and it's /// collection /// /// The pin collection to remove /// if the pin collection was removed; otherwise . protected bool RemovePinCollection(PinCollection pinCollection) { bool isRemoved = _pinCollections.Remove(pinCollection); if (isRemoved) { foreach (IPin pin in pinCollection) pin.DisconnectAll(); PinCollectionRemoved?.Invoke(this, new SingleValueEventArgs(pinCollection)); } return isRemoved; } /// /// Called when the node was loaded from storage or newly created /// /// The script the node is contained in public virtual void Initialize(INodeScript script) { } /// /// Evaluates the value of the output pins of this node /// public abstract void Evaluate(); /// public virtual void Reset() { foreach (IPin pin in _pins) pin.Reset(); foreach (IPinCollection pinCollection in _pinCollections) pinCollection.Reset(); Resetting?.Invoke(this, EventArgs.Empty); } /// public void TryInitialize(INodeScript script) { TryOrBreak(() => Initialize(script), "Failed to initialize"); } /// public void TryEvaluate() { TryOrBreak(Evaluate, "Failed to evaluate"); } /// /// Called whenever the node must show it's custom view model, if , no custom view model is used /// /// /// The custom view model, if , no custom view model is used public virtual ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript) { return null; } /// /// Serializes the object into a string /// /// The serialized object public virtual string SerializeStorage() { return string.Empty; } /// /// Deserializes the object and sets it /// /// The serialized object public virtual void DeserializeStorage(string serialized) { } #endregion } /// /// Represents a kind of node inside a containing storage value of type /// . /// /// The type of value the node stores public abstract class Node : Node { private TStorage? _storage; /// protected Node() { } /// protected Node(string name, string description) : base(name, description) { } /// /// Gets or sets the storage object of this node, this is saved across sessions /// public TStorage? Storage { get => _storage; set { if (SetAndNotify(ref _storage, value)) StorageModified?.Invoke(this, EventArgs.Empty); } } /// /// Occurs whenever the storage of this node was modified. /// public event EventHandler? StorageModified; /// public override string SerializeStorage() { return CoreJson.SerializeObject(Storage, true); } /// public override void DeserializeStorage(string serialized) { Storage = CoreJson.DeserializeObject(serialized) ?? default(TStorage); } } /// /// Represents a kind of node inside a containing storage value of type /// and a view model of type . /// /// The type of value the node stores /// The type of view model the node uses public abstract class Node : Node where TViewModel : ICustomNodeViewModel { /// protected Node() { } /// protected Node(string name, string description) : base(name, description) { } [Inject] internal IKernel Kernel { get; set; } = null!; /// /// Called when a view model is required /// /// public virtual TViewModel GetViewModel(NodeScript nodeScript) { // Limit to one constructor, there's no need to have more and it complicates things anyway ConstructorInfo[] constructors = typeof(TViewModel).GetConstructors(); if (constructors.Length != 1) throw new ArtemisCoreException("Node VMs must have exactly one constructor"); // Find the ScriptConfiguration parameter, it is required by the base constructor so its there for sure ParameterInfo? configurationParameter = constructors.First().GetParameters().FirstOrDefault(p => GetType().IsAssignableFrom(p.ParameterType)); if (configurationParameter?.Name == null) throw new ArtemisCoreException($"Couldn't find a valid constructor argument on {typeof(TViewModel).Name} with type {GetType().Name}"); return Kernel.Get(new ConstructorArgument(configurationParameter.Name, this), new ConstructorArgument("script", nodeScript)); } /// /// public override ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript) { return GetViewModel(nodeScript); } }