diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
index d42d584d5..c23015925 100644
--- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings
+++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
@@ -93,5 +93,6 @@
TrueTrueTrue
- True
- True
\ No newline at end of file
+ True
+ True
+ True
\ No newline at end of file
diff --git a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs
index c88cbd625..b108d81f1 100644
--- a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs
+++ b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs
@@ -1,13 +1,8 @@
-using System.Collections.Specialized;
-using SkiaSharp;
-
-namespace Artemis.Core;
+namespace Artemis.Core;
///
public class ColorGradientLayerProperty : LayerProperty
{
- private ColorGradient? _subscribedGradient;
-
internal ColorGradientLayerProperty()
{
KeyframesSupported = false;
@@ -22,29 +17,6 @@ public class ColorGradientLayerProperty : LayerProperty
return p.CurrentValue;
}
- #region Overrides of LayerProperty
-
- ///
- protected override void OnCurrentValueSet()
- {
- // Don't allow color gradients to be null
- if (BaseValue == null!)
- BaseValue = new ColorGradient(DefaultValue);
-
- if (!ReferenceEquals(_subscribedGradient, BaseValue))
- {
- if (_subscribedGradient != null)
- _subscribedGradient.CollectionChanged -= SubscribedGradientOnPropertyChanged;
- _subscribedGradient = BaseValue;
- _subscribedGradient.CollectionChanged += SubscribedGradientOnPropertyChanged;
- }
-
- CreateDataBindingRegistrations();
- base.OnCurrentValueSet();
- }
-
- #endregion
-
///
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
@@ -60,33 +32,12 @@ public class ColorGradientLayerProperty : LayerProperty
if (BaseValue == null!)
BaseValue = new ColorGradient(DefaultValue);
- base.OnInitialize();
+ DataBinding.RegisterDataBindingProperty(() => CurrentValue, value =>
+ {
+ if (value != null)
+ CurrentValue = value;
+ }, "Value");
}
#endregion
-
- private void CreateDataBindingRegistrations()
- {
- DataBinding.ClearDataBindingProperties();
- if (CurrentValue == null!)
- return;
-
- for (int index = 0; index < CurrentValue.Count; index++)
- {
- int stopIndex = index;
-
- void Setter(SKColor value)
- {
- CurrentValue[stopIndex].Color = value;
- }
-
- DataBinding.RegisterDataBindingProperty(() => CurrentValue[stopIndex].Color, Setter, $"Color #{stopIndex + 1}");
- }
- }
-
- private void SubscribedGradientOnPropertyChanged(object? sender, NotifyCollectionChangedEventArgs args)
- {
- if (CurrentValue.Count != DataBinding.Properties.Count)
- CreateDataBindingRegistrations();
- }
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs
index d39a0f7f5..f198b5397 100644
--- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs
+++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs
@@ -1,7 +1,7 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Internal;
-using Artemis.Core.VisualScripting.Internal;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
@@ -14,12 +14,12 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly EventConditionEntity _entity;
+ private NodeScript _script;
+ private DefaultNode _startNode;
private DataModelPath? _eventPath;
private DateTime _lastProcessedTrigger;
private object? _lastProcessedValue;
private EventOverlapMode _overlapMode;
- private NodeScript _script;
- private IEventConditionNode _startNode;
private EventToggleOffMode _toggleOffMode;
private EventTriggerMode _triggerMode;
private bool _wasMet;
@@ -34,7 +34,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
_entity = new EventConditionEntity();
_displayName = profileElement.GetType().Name;
_startNode = new EventConditionEventStartNode {X = -300};
- _script = new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
+ _script = new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile, new List {_startNode});
}
internal EventCondition(EventConditionEntity entity, RenderProfileElement profileElement)
@@ -96,109 +96,6 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
set => SetAndNotify(ref _toggleOffMode, value);
}
- ///
- /// Updates the event node, applying the selected event
- ///
- public void UpdateEventNode()
- {
- if (EventPath == null)
- return;
-
- Type? pathType = EventPath.GetPropertyType();
- if (pathType == null)
- return;
-
- // Create an event node if the path type is a data model event
- if (pathType.IsAssignableTo(typeof(IDataModelEvent)))
- {
- EventConditionEventStartNode eventNode;
- // Ensure the start node is an event node
- if (_startNode is not EventConditionEventStartNode node)
- {
- eventNode = new EventConditionEventStartNode();
- ReplaceStartNode(eventNode);
- _startNode = eventNode;
- }
- else
- {
- eventNode = node;
- }
-
- IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
- eventNode.CreatePins(dataModelEvent);
- }
- // Create a value changed node if the path type is a regular value
- else
- {
- // Ensure the start nod is a value changed node
- EventConditionValueChangedStartNode valueChangedNode;
- // Ensure the start node is an event node
- if (_startNode is not EventConditionValueChangedStartNode node)
- {
- valueChangedNode = new EventConditionValueChangedStartNode();
- ReplaceStartNode(valueChangedNode);
- }
- else
- {
- valueChangedNode = node;
- }
-
- valueChangedNode.UpdateOutputPins(EventPath);
- }
-
- if (!Script.Nodes.Contains(_startNode))
- Script.AddNode(_startNode);
- Script.Save();
- }
-
- ///
- /// Gets the start node of the event script, if any
- ///
- /// The start node of the event script, if any.
- public INode GetStartNode()
- {
- return _startNode;
- }
-
- private void ReplaceStartNode(IEventConditionNode newStartNode)
- {
- if (Script.Nodes.Contains(_startNode))
- Script.RemoveNode(_startNode);
-
- _startNode = newStartNode;
- if (!Script.Nodes.Contains(_startNode))
- Script.AddNode(_startNode);
- }
-
- private bool Evaluate()
- {
- if (EventPath == null)
- return false;
-
- object? value = EventPath.GetValue();
- if (_startNode is EventConditionEventStartNode)
- {
- if (value is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger)
- return false;
-
- _lastProcessedTrigger = dataModelEvent.LastTrigger;
- }
- else if (_startNode is EventConditionValueChangedStartNode valueChangedNode)
- {
- if (Equals(value, _lastProcessedValue))
- return false;
-
- valueChangedNode.UpdateValues(value, _lastProcessedValue);
- _lastProcessedValue = value;
- }
-
- if (!Script.ExitNodeConnected)
- return true;
-
- Script.Run();
- return Script.Result;
- }
-
///
public IConditionEntity Entity => _entity;
@@ -264,6 +161,116 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
Script?.Dispose();
EventPath?.Dispose();
}
+
+ ///
+ /// Updates the event node, applying the selected event
+ ///
+ public void UpdateEventNode(bool updateScript)
+ {
+ if (EventPath == null)
+ return;
+
+ Type? pathType = EventPath.GetPropertyType();
+ if (pathType == null)
+ return;
+
+ // Create an event node if the path type is a data model event
+ if (pathType.IsAssignableTo(typeof(IDataModelEvent)))
+ {
+ EventConditionEventStartNode eventNode;
+ // Ensure the start node is an event node
+ if (_startNode is not EventConditionEventStartNode node)
+ {
+ eventNode = new EventConditionEventStartNode();
+ if (updateScript)
+ ReplaceStartNode(eventNode);
+ _startNode = eventNode;
+ }
+ else
+ {
+ eventNode = node;
+ }
+
+ IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
+ eventNode.CreatePins(dataModelEvent);
+ }
+ // Create a value changed node if the path type is a regular value
+ else
+ {
+ // Ensure the start nod is a value changed node
+ EventConditionValueChangedStartNode valueChangedNode;
+ // Ensure the start node is an event node
+ if (_startNode is not EventConditionValueChangedStartNode node)
+ {
+ valueChangedNode = new EventConditionValueChangedStartNode();
+ if (updateScript)
+ ReplaceStartNode(valueChangedNode);
+ _startNode = valueChangedNode;
+ }
+ else
+ {
+ valueChangedNode = node;
+ }
+
+ valueChangedNode.UpdateOutputPins(EventPath);
+ }
+
+ // Script can be null if called before load
+ if (!updateScript)
+ return;
+ if (!Script.Nodes.Contains(_startNode))
+ {
+ Script.AddNode(_startNode);
+ Script.LoadConnections();
+ }
+ Script.Save();
+ }
+
+ ///
+ /// Gets the start node of the event script, if any
+ ///
+ /// The start node of the event script, if any.
+ public INode GetStartNode()
+ {
+ return _startNode;
+ }
+
+ private void ReplaceStartNode(DefaultNode newStartNode)
+ {
+ if (Script.Nodes.Contains(_startNode))
+ Script.RemoveNode(_startNode);
+ if (!Script.Nodes.Contains(newStartNode))
+ Script.AddNode(newStartNode);
+ }
+
+ private bool Evaluate()
+ {
+ if (EventPath == null)
+ return false;
+
+ object? value = EventPath.GetValue();
+ if (_startNode is EventConditionEventStartNode)
+ {
+ if (value is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger)
+ return false;
+
+ _lastProcessedTrigger = dataModelEvent.LastTrigger;
+ }
+ else if (_startNode is EventConditionValueChangedStartNode valueChangedNode)
+ {
+ if (Equals(value, _lastProcessedValue))
+ return false;
+
+ valueChangedNode.UpdateValues(value, _lastProcessedValue);
+ _lastProcessedValue = value;
+ }
+
+ if (!Script.ExitNodeConnected)
+ return true;
+
+ Script.Run();
+ return Script.Result;
+ }
#region Storage
@@ -276,11 +283,13 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
if (_entity.EventPath != null)
EventPath = new DataModelPath(_entity.EventPath);
-
+ UpdateEventNode(false);
+
+ string name = $"Activate {_displayName}";
+ string description = $"Whether or not the event should activate the {_displayName}";
Script = _entity.Script != null
- ? new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile)
- : new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
- UpdateEventNode();
+ ? new NodeScript(name, description, _entity.Script, ProfileElement.Profile, new List {_startNode})
+ : new NodeScript(name, description, ProfileElement.Profile, new List {_startNode});
}
///
@@ -311,71 +320,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
///
public void LoadNodeScript()
{
+ UpdateEventNode(true);
Script.Load();
-
- // The load action may have created an event node, use that one over the one we have here
- INode? existingEventNode = Script.Nodes.FirstOrDefault(n => n.Id == EventConditionEventStartNode.NodeId || n.Id == EventConditionValueChangedStartNode.NodeId);
- if (existingEventNode != null)
- _startNode = (IEventConditionNode) existingEventNode;
-
- UpdateEventNode();
- Script.LoadConnections();
}
#endregion
-}
-
-///
-/// Represents a mode for render elements to start their timeline when display conditions events are fired.
-///
-public enum EventTriggerMode
-{
- ///
- /// Play the timeline once.
- ///
- Play,
-
- ///
- /// Toggle repeating the timeline.
- ///
- Toggle
-}
-
-///
-/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before
-/// the timeline finishes.
-///
-public enum EventOverlapMode
-{
- ///
- /// Stop the current run and restart the timeline
- ///
- Restart,
-
- ///
- /// Play another copy of the timeline on top of the current run
- ///
- Copy,
-
- ///
- /// Ignore subsequent event fires until the timeline finishes
- ///
- Ignore
-}
-
-///
-/// Represents a mode for render elements when toggling off the event when using
-/// .
-///
-public enum EventToggleOffMode
-{
- ///
- /// When the event toggles the condition off, finish the the current run of the main timeline
- ///
- Finish,
-
- ///
- /// When the event toggles the condition off, skip to the end segment of the timeline
- ///
- SkipToEnd
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventOverlapMode.cs b/src/Artemis.Core/Models/Profile/Conditions/EventOverlapMode.cs
new file mode 100644
index 000000000..56068a797
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/EventOverlapMode.cs
@@ -0,0 +1,23 @@
+namespace Artemis.Core;
+
+///
+/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before
+/// the timeline finishes.
+///
+public enum EventOverlapMode
+{
+ ///
+ /// Stop the current run and restart the timeline
+ ///
+ Restart,
+
+ ///
+ /// Play another copy of the timeline on top of the current run
+ ///
+ Copy,
+
+ ///
+ /// Ignore subsequent event fires until the timeline finishes
+ ///
+ Ignore
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventToggleOffMode.cs b/src/Artemis.Core/Models/Profile/Conditions/EventToggleOffMode.cs
new file mode 100644
index 000000000..c8db3aa6a
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/EventToggleOffMode.cs
@@ -0,0 +1,18 @@
+namespace Artemis.Core;
+
+///
+/// Represents a mode for render elements when toggling off the event when using
+/// .
+///
+public enum EventToggleOffMode
+{
+ ///
+ /// When the event toggles the condition off, finish the the current run of the main timeline
+ ///
+ Finish,
+
+ ///
+ /// When the event toggles the condition off, skip to the end segment of the timeline
+ ///
+ SkipToEnd
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventTriggerMode.cs b/src/Artemis.Core/Models/Profile/Conditions/EventTriggerMode.cs
new file mode 100644
index 000000000..cfe31816d
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/EventTriggerMode.cs
@@ -0,0 +1,17 @@
+namespace Artemis.Core;
+
+///
+/// Represents a mode for render elements to start their timeline when display conditions events are fired.
+///
+public enum EventTriggerMode
+{
+ ///
+ /// Play the timeline once.
+ ///
+ Play,
+
+ ///
+ /// Toggle repeating the timeline.
+ ///
+ Toggle
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs
index 13a5c9e27..81c4b719a 100644
--- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs
+++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs
@@ -35,7 +35,7 @@ public class DataBindingProperty : IDataBindingProperty
}
///
- public void SetValue(object? value)
+ public void SetValue(object value)
{
// Numeric has a bunch of conversion, this seems the cheapest way to use them :)
switch (value)
diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs
index 7d5dc49bd..7e4ce8d06 100644
--- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs
+++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs
@@ -27,5 +27,5 @@ public interface IDataBindingProperty
/// Sets the value of the property this registration points to
///
/// A value matching the type of
- void SetValue(object? value);
+ void SetValue(object value);
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs
index 19acfa645..cf36770e1 100644
--- a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs
+++ b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs
@@ -22,6 +22,7 @@ public class PluginFeatureAttribute : Attribute
/// The plugins display icon that's shown in the settings see for
/// available icons
///
+ [Obsolete("Feature icons are no longer shown in the UI.")]
public string? Icon { get; set; }
///
diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
index 35798a064..32d5775ec 100644
--- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
+++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
@@ -31,20 +31,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
- Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
-
- if (Icon != null) return;
- if (typeof(DeviceProvider).IsAssignableFrom(featureType))
- Icon = "Devices";
- else if (typeof(Module).IsAssignableFrom(featureType))
- Icon = "VectorRectangle";
- else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType))
- Icon = "Brush";
- else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType))
- Icon = "AutoAwesome";
- else
- Icon = "Plugin";
}
internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance)
@@ -56,19 +43,8 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
- Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
Instance = instance;
-
- if (Icon != null) return;
- Icon = Instance switch
- {
- DeviceProvider => "Devices",
- Module => "VectorRectangle",
- LayerBrushProvider => "Brush",
- LayerEffectProvider => "AutoAwesome",
- _ => "Plugin"
- };
}
///
@@ -110,17 +86,6 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
set => SetAndNotify(ref _description, value);
}
- ///
- /// The plugins display icon that's shown in the settings see for
- /// available icons
- ///
- [JsonProperty]
- public string? Icon
- {
- get => _icon;
- set => SetAndNotify(ref _icon, value);
- }
-
///
/// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled.
/// Note: always if this is the plugin's only feature
@@ -142,20 +107,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
get => _instance;
internal set => SetAndNotify(ref _instance, value);
}
-
- ///
- /// Gets a string representing either a full path pointing to an svg or the markdown icon
- ///
- public string? ResolvedIcon
- {
- get
- {
- if (Icon == null)
- return null;
- return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon;
- }
- }
-
+
internal PluginFeatureEntity Entity { get; }
///
diff --git a/src/Artemis.Core/Services/Input/InputProvider.cs b/src/Artemis.Core/Services/Input/InputProvider.cs
index f04b1a1f9..c51a25d63 100644
--- a/src/Artemis.Core/Services/Input/InputProvider.cs
+++ b/src/Artemis.Core/Services/Input/InputProvider.cs
@@ -8,6 +8,13 @@ namespace Artemis.Core.Services;
///
public abstract class InputProvider : IDisposable
{
+ public InputProvider()
+ {
+ ProviderName = GetType().FullName ?? throw new InvalidOperationException("Input provider must have a type with a name");
+ }
+
+ internal string ProviderName { get; set; }
+
///
/// Called when the input service requests a event
///
diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs
index 187f0db4b..48ae607f6 100644
--- a/src/Artemis.Core/Services/Input/InputService.cs
+++ b/src/Artemis.Core/Services/Input/InputService.cs
@@ -10,12 +10,18 @@ internal class InputService : IInputService
{
private readonly ILogger _logger;
private readonly IRgbService _rgbService;
+ private ArtemisDevice? _firstKeyboard;
+ private ArtemisDevice? _firstMouse;
+ private int _keyboardCount;
+ private int _mouseCount;
public InputService(ILogger logger, IRgbService rgbService)
{
_logger = logger;
_rgbService = rgbService;
+ _rgbService.DeviceAdded += RgbServiceOnDevicesModified;
+ _rgbService.DeviceRemoved += RgbServiceOnDevicesModified;
BustIdentifierCache();
}
@@ -132,8 +138,6 @@ internal class InputService : IInputService
private readonly Dictionary, ArtemisDevice> _deviceCache = new();
private List _devices = new();
- private ArtemisDevice? _cachedFallbackKeyboard;
- private ArtemisDevice? _cachedFallbackMouse;
private ArtemisDevice? _identifyingDevice;
public void IdentifyDevice(ArtemisDevice device)
@@ -164,13 +168,29 @@ internal class InputService : IInputService
if (provider == null) throw new ArgumentNullException(nameof(provider));
if (identifier == null) throw new ArgumentNullException(nameof(identifier));
+ // We will almost always only have zero or one of each
+ if (type == InputDeviceType.Keyboard)
+ {
+ if (_keyboardCount == 0)
+ return null;
+ if (_keyboardCount == 1)
+ return _firstKeyboard;
+ }
+
+ if (type == InputDeviceType.Mouse)
+ {
+ if (_mouseCount == 0)
+ return null;
+ if (_mouseCount == 1)
+ return _firstMouse;
+ }
+
// Try cache first
ArtemisDevice? cacheMatch = GetDeviceFromCache(provider, identifier);
if (cacheMatch != null)
return cacheMatch;
- string providerName = provider.GetType().FullName!;
- ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, providerName) && Equals(i.Identifier, identifier)));
+ ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, provider.ProviderName) && Equals(i.Identifier, identifier)));
// If a match was found cache it to speed up the next event and return the match
if (match != null)
@@ -179,34 +199,16 @@ internal class InputService : IInputService
return match;
}
- // If there is no match, apply our fallback type
- if (type == InputDeviceType.None)
- return null;
if (type == InputDeviceType.Keyboard)
- {
- if (_cachedFallbackKeyboard != null)
- return _cachedFallbackKeyboard;
- _cachedFallbackKeyboard = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
- return _cachedFallbackKeyboard;
- }
-
+ return _firstKeyboard;
if (type == InputDeviceType.Mouse)
- {
- if (_cachedFallbackMouse != null)
- return _cachedFallbackMouse;
- _cachedFallbackMouse = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
- return _cachedFallbackMouse;
- }
-
+ return _firstMouse;
return null;
}
public void BustIdentifierCache()
{
_deviceCache.Clear();
- _cachedFallbackKeyboard = null;
- _cachedFallbackMouse = null;
-
_devices = _rgbService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
}
@@ -220,12 +222,7 @@ internal class InputService : IInputService
_deviceCache.TryGetValue(new Tuple(provider, identifier), out ArtemisDevice? device);
return device;
}
-
- private void SurfaceConfigurationChanged(object? sender, SurfaceConfigurationEventArgs e)
- {
- BustIdentifierCache();
- }
-
+
private void InputProviderOnIdentifierReceived(object? sender, InputProviderIdentifierEventArgs e)
{
// Don't match if there is no device or if the device type differs from the event device type
@@ -236,16 +233,24 @@ internal class InputService : IInputService
if (!(sender is InputProvider inputProvider))
return;
- string providerName = inputProvider.GetType().FullName!;
-
// Remove existing identification
- _identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == providerName);
- _identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(providerName, e.Identifier));
+ _identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == inputProvider.ProviderName);
+ _identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(inputProvider.ProviderName, e.Identifier));
StopIdentify();
OnDeviceIdentified();
}
+ private void RgbServiceOnDevicesModified(object? sender, DeviceEventArgs args)
+ {
+ _firstKeyboard = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
+ _firstMouse = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
+ _keyboardCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard);
+ _mouseCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse);
+
+ BustIdentifierCache();
+ }
+
#endregion
#region Keyboard
@@ -379,6 +384,7 @@ internal class InputService : IInputService
private readonly HashSet _pressedButtons = new();
+
private void InputProviderOnMouseButtonDataReceived(object? sender, InputProviderMouseButtonEventArgs e)
{
bool foundLedId = InputKeyUtilities.MouseButtonLedIdMap.TryGetValue(e.Button, out LedId ledId);
diff --git a/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs b/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs
index 6b4d367f5..7857f533d 100644
--- a/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs
+++ b/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs
@@ -23,7 +23,10 @@ internal class DataBindingExitNode : Node, IExitNode
public void ApplyToDataBinding()
{
foreach ((IDataBindingProperty? property, object? pendingValue) in _propertyValues)
- property.SetValue(pendingValue);
+ {
+ if (pendingValue != null)
+ property.SetValue(pendingValue);
+ }
}
public override void Evaluate()
diff --git a/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs b/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs
index 21a264caa..14f1e0562 100644
--- a/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs
+++ b/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs
@@ -1,23 +1,20 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using Artemis.Core.Modules;
-using Artemis.Core.VisualScripting.Internal;
-using Humanizer;
namespace Artemis.Core.Internal;
-internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
+internal class EventConditionEventStartNode : DefaultNode
{
internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305");
- private readonly Dictionary, OutputPin> _propertyPins;
+ private readonly ObjectOutputPins _objectOutputPins;
private IDataModelEvent? _dataModelEvent;
public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation")
{
- _propertyPins = new Dictionary, OutputPin>();
+ _objectOutputPins = new ObjectOutputPins(this);
+ }
+
+ public void SetDataModelEvent(IDataModelEvent? dataModelEvent)
+ {
}
public void CreatePins(IDataModelEvent? dataModelEvent)
@@ -25,30 +22,8 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
if (_dataModelEvent == dataModelEvent)
return;
- while (Pins.Any())
- RemovePin((Pin) Pins.First());
- _propertyPins.Clear();
-
_dataModelEvent = dataModelEvent;
- if (dataModelEvent == null)
- return;
-
- foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
- .Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
- {
- // Expect an IDataModelEvent
- ParameterExpression eventParameter = Expression.Parameter(typeof(DataModelEventArgs), "event");
- // Cast it to the actual event type
- UnaryExpression eventCast = Expression.Convert(eventParameter, propertyInfo.DeclaringType!);
- // Access the property
- MemberExpression accessor = Expression.Property(eventCast, propertyInfo);
- // Cast the property to an object (sadly boxing)
- UnaryExpression objectCast = Expression.Convert(accessor, typeof(object));
- // Compile the resulting expression
- Func expression = Expression.Lambda>(objectCast, eventParameter).Compile();
-
- _propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
- }
+ _objectOutputPins.ChangeType(dataModelEvent?.ArgumentsType);
}
public override void Evaluate()
@@ -56,12 +31,6 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
return;
- foreach ((Func propertyAccessor, OutputPin outputPin) in _propertyPins)
- {
- if (!outputPin.ConnectedTo.Any())
- continue;
- object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
- outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
- }
+ _objectOutputPins.SetCurrentValue(_dataModelEvent.LastEventArgumentsUntyped);
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs b/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs
index abf571979..c334681bc 100644
--- a/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs
+++ b/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs
@@ -1,9 +1,8 @@
using System;
-using Artemis.Core.VisualScripting.Internal;
namespace Artemis.Core.Internal;
-internal class EventConditionValueChangedStartNode : DefaultNode, IEventConditionNode
+internal class EventConditionValueChangedStartNode : DefaultNode
{
internal static readonly Guid NodeId = new("F9A270DB-A231-4800-BAB3-DC1F96856756");
private object? _newValue;
diff --git a/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs b/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs
deleted file mode 100644
index e47ad33a7..000000000
--- a/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Artemis.Core.VisualScripting.Internal;
-
-internal interface IEventConditionNode : INode
-{
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs
index 1c77626f8..7a02555ac 100644
--- a/src/Artemis.Core/VisualScripting/NodeScript.cs
+++ b/src/Artemis.Core/VisualScripting/NodeScript.cs
@@ -35,7 +35,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
#region Properties & Fields
- internal NodeScriptEntity Entity { get; private set; }
+ ///
+ /// Gets the entity used to store this script.
+ ///
+ public NodeScriptEntity Entity { get; private set; }
///
public string Name { get; }
@@ -77,7 +80,8 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
/// The context of the node script, usually a or
///
///
- protected NodeScript(string name, string description, object? context = null)
+ /// 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;
@@ -87,9 +91,13 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
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)
+ internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List? defaultNodes = null)
{
Name = name;
Description = description;
@@ -99,6 +107,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded;
NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved;
+
+ if (defaultNodes != null)
+ foreach (DefaultNode defaultNode in defaultNodes)
+ AddNode(defaultNode);
}
#endregion
@@ -410,8 +422,9 @@ public class NodeScript : NodeScript, INodeScript
#region Constructors
- internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null)
- : base(name, description, entity, context)
+ ///
+ 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);
@@ -420,8 +433,8 @@ public class NodeScript : NodeScript, INodeScript
}
///
- public NodeScript(string name, string description, object? context = null)
- : base(name, description, context)
+ public NodeScript(string name, string description, object? context = null, List? defaultNodes = null)
+ : base(name, description, context, defaultNodes)
{
ExitNode = new ExitNode(name, description);
AddNode(ExitNode);
diff --git a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs b/src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModel.cs
similarity index 100%
rename from src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs
rename to src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModel.cs
diff --git a/src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModelPosition.cs b/src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModelPosition.cs
new file mode 100644
index 000000000..74616dd8e
--- /dev/null
+++ b/src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModelPosition.cs
@@ -0,0 +1,32 @@
+namespace Artemis.Core;
+
+///
+/// Represents the position of a node's custom view model.
+///
+public enum CustomNodeViewModelPosition
+{
+ ///
+ /// Puts the view model above the pins.
+ ///
+ AbovePins,
+
+ ///
+ /// Puts the view model between the pins, vertically aligned to the top.
+ ///
+ BetweenPinsTop,
+
+ ///
+ /// Puts the view model between the pins, vertically aligned to the center.
+ ///
+ BetweenPinsCenter,
+
+ ///
+ /// Puts the view model between the pins, vertically aligned to the bottom.
+ ///
+ BetweenPinsBottom,
+
+ ///
+ /// Puts the view model below the pins.
+ ///
+ BelowPins
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs b/src/Artemis.Core/VisualScripting/Nodes/DefaultNode.cs
similarity index 60%
rename from src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs
rename to src/Artemis.Core/VisualScripting/Nodes/DefaultNode.cs
index 103aa4e12..733b4e97b 100644
--- a/src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs
+++ b/src/Artemis.Core/VisualScripting/Nodes/DefaultNode.cs
@@ -1,18 +1,11 @@
using System;
-namespace Artemis.Core.Internal;
-
-///
-/// Represents a kind of node that cannot be deleted inside a .
-///
-public interface IDefaultNode : INode
-{
-}
+namespace Artemis.Core;
///
/// Represents a kind of node that cannot be deleted inside a .
///
-public abstract class DefaultNode : Node, IDefaultNode
+public abstract class DefaultNode : Node
{
#region Constructors
@@ -20,8 +13,6 @@ public abstract class DefaultNode : Node, IDefaultNode
protected DefaultNode(Guid id, string name, string description = "") : base(name, description)
{
Id = id;
- Name = name;
- Description = description;
}
#endregion
diff --git a/src/Artemis.Core/VisualScripting/Nodes/ICustomViewModelNode.cs b/src/Artemis.Core/VisualScripting/Nodes/ICustomViewModelNode.cs
new file mode 100644
index 000000000..b16623935
--- /dev/null
+++ b/src/Artemis.Core/VisualScripting/Nodes/ICustomViewModelNode.cs
@@ -0,0 +1,18 @@
+namespace Artemis.Core;
+
+///
+/// Represents a node that has a custom view model.
+///
+public interface ICustomViewModelNode
+{
+ ///
+ /// Gets or sets the position of the node's custom view model.
+ ///
+ CustomNodeViewModelPosition ViewModelPosition { get; }
+
+ ///
+ /// 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
+ ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript);
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Nodes/Node.cs
similarity index 71%
rename from src/Artemis.Core/VisualScripting/Node.cs
rename to src/Artemis.Core/VisualScripting/Nodes/Node.cs
index 7e2161580..e451f05aa 100644
--- a/src/Artemis.Core/VisualScripting/Node.cs
+++ b/src/Artemis.Core/VisualScripting/Nodes/Node.cs
@@ -2,10 +2,7 @@
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;
@@ -132,7 +129,7 @@ public abstract class Node : BreakableModel, INode
/// The name of the pin
/// The type of value the pin will hold
/// The newly created pin
- protected InputPin CreateInputPin(string name = "")
+ public InputPin CreateInputPin(string name = "")
{
InputPin pin = new(this, name);
_pins.Add(pin);
@@ -146,7 +143,7 @@ public abstract class Node : BreakableModel, INode
/// The type of value the pin will hold
/// The name of the pin
/// The newly created pin
- protected InputPin CreateInputPin(Type type, string name = "")
+ public InputPin CreateInputPin(Type type, string name = "")
{
InputPin pin = new(this, type, name);
_pins.Add(pin);
@@ -160,7 +157,7 @@ public abstract class Node : BreakableModel, INode
/// The name of the pin
/// The type of value the pin will hold
/// The newly created pin
- protected OutputPin CreateOutputPin(string name = "")
+ public OutputPin CreateOutputPin(string name = "")
{
OutputPin pin = new(this, name);
_pins.Add(pin);
@@ -174,7 +171,7 @@ public abstract class Node : BreakableModel, INode
/// The type of value the pin will hold
/// The name of the pin
/// The newly created pin
- protected OutputPin CreateOutputPin(Type type, string name = "")
+ public OutputPin CreateOutputPin(Type type, string name = "")
{
OutputPin pin = new(this, type, name);
_pins.Add(pin);
@@ -187,7 +184,7 @@ public abstract class Node : BreakableModel, INode
/// 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)
+ public 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));
@@ -217,7 +214,7 @@ public abstract class Node : BreakableModel, INode
/// 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)
+ public 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));
@@ -247,7 +244,7 @@ public abstract class Node : BreakableModel, INode
///
/// The pin to remove
/// if the pin was removed; otherwise .
- protected bool RemovePin(Pin pin)
+ public bool RemovePin(Pin pin)
{
bool isRemoved = _pins.Remove(pin);
if (isRemoved)
@@ -263,7 +260,7 @@ public abstract class Node : BreakableModel, INode
/// Adds an existing to the collection.
///
/// The pin to add
- protected void AddPin(Pin pin)
+ public 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.");
@@ -281,7 +278,7 @@ public abstract class Node : BreakableModel, INode
/// 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)
+ public InputPinCollection CreateInputPinCollection(string name = "", int initialCount = 1)
{
InputPinCollection pin = new(this, name, initialCount);
_pinCollections.Add(pin);
@@ -296,7 +293,7 @@ public abstract class Node : BreakableModel, INode
/// 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)
+ public InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1)
{
InputPinCollection pin = new(this, type, name, initialCount);
_pinCollections.Add(pin);
@@ -311,7 +308,7 @@ public abstract class Node : BreakableModel, INode
/// 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)
+ public OutputPinCollection CreateOutputPinCollection(string name = "", int initialCount = 1)
{
OutputPinCollection pin = new(this, name, initialCount);
_pinCollections.Add(pin);
@@ -325,7 +322,7 @@ public abstract class Node : BreakableModel, INode
///
/// The pin collection to remove
/// if the pin collection was removed; otherwise .
- protected bool RemovePinCollection(PinCollection pinCollection)
+ public bool RemovePinCollection(PinCollection pinCollection)
{
bool isRemoved = _pinCollections.Remove(pinCollection);
if (isRemoved)
@@ -339,7 +336,8 @@ public abstract class Node : BreakableModel, INode
}
///
- /// Called when the node was loaded from storage or newly created
+ /// Called when the node was loaded from storage or newly created, at this point pin connections aren't reestablished
+ /// yet.
///
/// The script the node is contained in
public virtual void Initialize(INodeScript script)
@@ -374,16 +372,6 @@ public abstract class Node : BreakableModel, INode
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
///
@@ -402,102 +390,4 @@ public abstract class Node : BreakableModel, INode
}
#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);
- }
}
\ No newline at end of file
diff --git a/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs
new file mode 100644
index 000000000..81d9e7eac
--- /dev/null
+++ b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs
@@ -0,0 +1,52 @@
+using System;
+using Artemis.Core;
+
+///
+/// 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);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs
new file mode 100644
index 000000000..1b4109714
--- /dev/null
+++ b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs
@@ -0,0 +1,58 @@
+using System.Linq;
+using System.Reflection;
+using Artemis.Core;
+using Ninject;
+using Ninject.Parameters;
+
+///
+/// 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, ICustomViewModelNode 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));
+ }
+
+ ///
+ /// Gets or sets the position of the node's custom view model.
+ ///
+ public CustomNodeViewModelPosition ViewModelPosition { get; protected set; } = CustomNodeViewModelPosition.BetweenPinsTop;
+
+ ///
+ ///
+ public ICustomNodeViewModel GetCustomViewModel(NodeScript nodeScript)
+ {
+ return GetViewModel(nodeScript);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/VisualScripting/InputPin.cs b/src/Artemis.Core/VisualScripting/Pins/InputPin.cs
similarity index 100%
rename from src/Artemis.Core/VisualScripting/InputPin.cs
rename to src/Artemis.Core/VisualScripting/Pins/InputPin.cs
diff --git a/src/Artemis.Core/VisualScripting/InputPinCollection.cs b/src/Artemis.Core/VisualScripting/Pins/InputPinCollection.cs
similarity index 100%
rename from src/Artemis.Core/VisualScripting/InputPinCollection.cs
rename to src/Artemis.Core/VisualScripting/Pins/InputPinCollection.cs
diff --git a/src/Artemis.Core/VisualScripting/Pins/ObjectOutputPins.cs b/src/Artemis.Core/VisualScripting/Pins/ObjectOutputPins.cs
new file mode 100644
index 000000000..fb9c874f7
--- /dev/null
+++ b/src/Artemis.Core/VisualScripting/Pins/ObjectOutputPins.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using Artemis.Core.Modules;
+using Humanizer;
+
+namespace Artemis.Core;
+
+///
+/// Represents a collection of output pins for a node capable of outputting the properties of an object or value type.
+///
+public class ObjectOutputPins
+{
+ private readonly Dictionary, OutputPin> _propertyPins;
+ private OutputPin? _valueTypePin;
+
+ ///
+ /// Creates an instance of the class.
+ ///
+ /// The node the object output was created for.
+ public ObjectOutputPins(Node node)
+ {
+ Node = node;
+ _propertyPins = new Dictionary, OutputPin>();
+ }
+
+ ///
+ /// Gets the node the object output was created for.
+ ///
+ public Node Node { get; }
+
+ ///
+ /// Gets the current type the node's pins are set up for.
+ ///
+ public Type? CurrentType { get; private set; }
+
+ ///
+ /// Gets a read only collection of the pins outputting the object of this object node.
+ ///
+ public ReadOnlyCollection Pins => _valueTypePin != null ? new ReadOnlyCollection(new List {_valueTypePin}) : _propertyPins.Values.ToList().AsReadOnly();
+
+ ///
+ /// Change the current type and create pins on the node to reflect this.
+ ///
+ /// The type to change the collection to.
+ public void ChangeType(Type? type)
+ {
+ if (type == CurrentType)
+ return;
+ CurrentType = type;
+
+ // Remove current pins
+ foreach ((Func