From d2e06076222bc8ae605e61fa106a332f05e03d13 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 23 Aug 2022 20:35:25 +0200 Subject: [PATCH] Nodes - Fixed string format node not outputting numerics Data model event node - Renamed to Data Model-Event Value Cycle Data model event node - Added new data model event node that outputs the latest event data Event conditions - Performance improvements --- .../Profile/Conditions/EventCondition.cs | 2 + src/Artemis.Core/Utilities/Numeric.cs | 15 ++ .../Internal/EventConditionEventStartNode.cs | 61 +++--- src/Artemis.Core/VisualScripting/Node.cs | 64 +++++++ src/Artemis.UI/Services/UpdateService.cs | 2 +- .../Artemis.VisualScripting.csproj | 4 + .../DataModel/DataModelEventCycleNode.cs | 167 +++++++++++++++++ .../Nodes/DataModel/DataModelEventNode.cs | 177 ++++++------------ .../DataModelEventCycleNodeCustomView.axaml | 18 ++ ...DataModelEventCycleNodeCustomView.axaml.cs | 17 ++ .../DataModelEventCycleNodeCustomViewModel.cs | 104 ++++++++++ .../DataModelEventNodeCustomView.axaml | 1 + .../DataModelEventNodeCustomViewModel.cs | 2 + .../DisplayValueNodeCustomViewModel.cs | 10 +- .../Nodes/Text/StringFormatNode.cs | 4 +- 15 files changed, 488 insertions(+), 160 deletions(-) create mode 100644 src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventCycleNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomView.axaml create mode 100644 src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomView.axaml.cs create mode 100644 src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index a517cd0e6..d39a0f7f5 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -146,6 +146,8 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition valueChangedNode.UpdateOutputPins(EventPath); } + if (!Script.Nodes.Contains(_startNode)) + Script.AddNode(_startNode); Script.Save(); } diff --git a/src/Artemis.Core/Utilities/Numeric.cs b/src/Artemis.Core/Utilities/Numeric.cs index 38cfcb057..e5dca7fd5 100644 --- a/src/Artemis.Core/Utilities/Numeric.cs +++ b/src/Artemis.Core/Utilities/Numeric.cs @@ -48,6 +48,14 @@ public readonly struct Numeric : IComparable, IConvertible { _value = value; } + + /// + /// Creates a new instance of from a + /// + public Numeric(long value) + { + _value = value; + } /// /// Creates a new instance of from an @@ -60,6 +68,7 @@ public readonly struct Numeric : IComparable, IConvertible int value => value, double value => (float) value, byte value => value, + long value => value, Numeric value => value, _ => ParseFloatOrDefault(pathValue?.ToString()) }; @@ -150,6 +159,11 @@ public readonly struct Numeric : IComparable, IConvertible { return (byte) Math.Clamp(p._value, 0, 255); } + + public static implicit operator long(Numeric p) + { + return (long) p._value; + } public static bool operator >(Numeric a, Numeric b) { @@ -264,6 +278,7 @@ public readonly struct Numeric : IComparable, IConvertible type == typeof(float) || type == typeof(double) || type == typeof(int) || + type == typeof(long) || type == typeof(byte); } diff --git a/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs b/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs index 7fed345d3..21a264caa 100644 --- a/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs +++ b/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs @@ -1,6 +1,7 @@ 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; @@ -11,13 +12,12 @@ namespace Artemis.Core.Internal; internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode { internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305"); - private readonly List _pinBucket = new(); - private readonly Dictionary _propertyPins; + private readonly Dictionary, OutputPin> _propertyPins; private IDataModelEvent? _dataModelEvent; public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation") { - _propertyPins = new Dictionary(); + _propertyPins = new Dictionary, OutputPin>(); } public void CreatePins(IDataModelEvent? dataModelEvent) @@ -33,40 +33,22 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode if (dataModelEvent == null) return; - foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType - .GetProperties(BindingFlags.Instance | BindingFlags.Public) + foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute)))) - _propertyPins.Add(propertyInfo, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize())); - } - - /// - /// 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. - /// - private OutputPin CreateOrAddOutputPin(Type valueType, string displayName) - { - // Grab the first pin from the bucket that isn't on the node yet - OutputPin? pin = _pinBucket.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); - _pinBucket.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); - } + // 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(); - return pin; + _propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize())); + } } public override void Evaluate() @@ -74,13 +56,12 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode if (_dataModelEvent?.LastEventArgumentsUntyped == null) return; - foreach ((PropertyInfo propertyInfo, OutputPin outputPin) in _propertyPins) + foreach ((Func propertyAccessor, OutputPin outputPin) in _propertyPins) { - if (outputPin.ConnectedTo.Any()) - { - object value = propertyInfo.GetValue(_dataModelEvent.LastEventArgumentsUntyped) ?? outputPin.Type.GetDefault()!; - outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value; - } + if (!outputPin.ConnectedTo.Any()) + continue; + object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!; + outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value; } } } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index 53eb78d25..bcca27806 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Ninject; using Ninject.Parameters; @@ -15,6 +16,9 @@ public abstract class Node : CorePropertyChanged, INode public event EventHandler? Resetting; #region Properties & Fields + + private readonly List _outputPinBucket = new(); + private readonly List _inputPinBucket = new(); private Guid _id; @@ -160,6 +164,66 @@ public abstract class Node : CorePropertyChanged, INode OnPropertyChanged(nameof(Pins)); 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 diff --git a/src/Artemis.UI/Services/UpdateService.cs b/src/Artemis.UI/Services/UpdateService.cs index 735e88b06..f4cada5f7 100644 --- a/src/Artemis.UI/Services/UpdateService.cs +++ b/src/Artemis.UI/Services/UpdateService.cs @@ -14,7 +14,7 @@ namespace Artemis.UI.Services; public class UpdateService : IUpdateService { - private const double UPDATE_CHECK_INTERVAL = 10000; // once per hour + private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour private readonly PluginSetting _autoUpdate; private readonly PluginSetting _checkForUpdates; diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj index 94dd26ad9..8dce3324f 100644 --- a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -33,6 +33,10 @@ DataModelNodeCustomView.axaml + + DataModelEventNodeCustomView.axaml + Code + diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventCycleNode.cs b/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventCycleNode.cs new file mode 100644 index 000000000..cab064242 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventCycleNode.cs @@ -0,0 +1,167 @@ +using Artemis.Core; +using Artemis.Core.Events; +using Artemis.Storage.Entities.Profile; +using Artemis.VisualScripting.Nodes.DataModel.Screens; + +namespace Artemis.VisualScripting.Nodes.DataModel; + +[Node("Data Model-Event Value Cycle", "Cycles through provided values each time the select event fires.", "Data Model", OutputType = typeof(object))] +public class DataModelEventCycleNode : Node, IDisposable +{ + private int _currentIndex; + private Type _currentType; + private DataModelPath? _dataModelPath; + private object? _lastPathValue; + private DateTime _lastTrigger; + private bool _updating; + + public DataModelEventCycleNode() : base("Data Model-Event Value Cycle", "Cycles through provided values each time the select event fires.") + { + _currentType = typeof(object); + + CycleValues = CreateInputPinCollection(typeof(object), "", 0); + Output = CreateOutputPin(typeof(object)); + + CycleValues.PinAdded += CycleValuesOnPinAdded; + CycleValues.PinRemoved += CycleValuesOnPinRemoved; + CycleValues.Add(CycleValues.CreatePin()); + + // Monitor storage for changes + StorageModified += (_, _) => UpdateDataModelPath(); + } + + public INodeScript? Script { get; set; } + + public InputPinCollection CycleValues { get; } + public OutputPin Output { get; } + + public override void Initialize(INodeScript script) + { + Script = script; + + if (Storage == null) + return; + + UpdateDataModelPath(); + } + + public override void Evaluate() + { + object? pathValue = _dataModelPath?.GetValue(); + bool hasTriggered = pathValue is IDataModelEvent dataModelEvent + ? EvaluateEvent(dataModelEvent) + : EvaluateValue(pathValue); + + if (hasTriggered) + { + _currentIndex++; + + if (_currentIndex >= CycleValues.Count()) + _currentIndex = 0; + } + + object? outputValue = CycleValues.ElementAt(_currentIndex).PinValue; + if (Output.Type.IsInstanceOfType(outputValue)) + Output.Value = outputValue; + else if (Output.Type.IsValueType) + Output.Value = Output.Type.GetDefault()!; + } + + private bool EvaluateEvent(IDataModelEvent dataModelEvent) + { + if (dataModelEvent.LastTrigger <= _lastTrigger) + return false; + + _lastTrigger = dataModelEvent.LastTrigger; + return true; + } + + private bool EvaluateValue(object? pathValue) + { + if (Equals(pathValue, _lastPathValue)) + return false; + + _lastPathValue = pathValue; + return true; + } + + private void CycleValuesOnPinAdded(object? sender, SingleValueEventArgs e) + { + e.Value.PinConnected += OnPinConnected; + e.Value.PinDisconnected += OnPinDisconnected; + } + + private void CycleValuesOnPinRemoved(object? sender, SingleValueEventArgs e) + { + e.Value.PinConnected -= OnPinConnected; + e.Value.PinDisconnected -= OnPinDisconnected; + } + + private void OnPinDisconnected(object? sender, SingleValueEventArgs e) + { + ProcessPinDisconnected(); + } + + private void OnPinConnected(object? sender, SingleValueEventArgs e) + { + ProcessPinConnected(e.Value); + } + + private void ProcessPinConnected(IPin source) + { + if (_updating) + return; + + try + { + _updating = true; + + // No need to change anything if the types haven't changed + if (_currentType != source.Type) + ChangeCurrentType(source.Type); + } + finally + { + _updating = false; + } + } + + private void UpdateDataModelPath() + { + DataModelPath? old = _dataModelPath; + _dataModelPath = Storage != null ? new DataModelPath(Storage) : null; + old?.Dispose(); + } + + private void ChangeCurrentType(Type type) + { + CycleValues.ChangeType(type); + Output.ChangeType(type); + + _currentType = type; + } + + private void ProcessPinDisconnected() + { + if (_updating) + return; + try + { + // If there's still a connected pin, stick to the current type + if (CycleValues.Any(v => v.ConnectedTo.Any())) + return; + + ChangeCurrentType(typeof(object)); + } + finally + { + _updating = false; + } + } + + /// + public void Dispose() + { + _dataModelPath?.Dispose(); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs b/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs index 12a5131bb..d64731202 100644 --- a/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs +++ b/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs @@ -1,39 +1,35 @@ -using Artemis.Core; +using System.Linq.Expressions; +using System.Reflection; +using Artemis.Core; using Artemis.Core.Events; +using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; using Artemis.VisualScripting.Nodes.DataModel.Screens; +using Humanizer; namespace Artemis.VisualScripting.Nodes.DataModel; -[Node("Data Model-Event", "Responds to a data model event trigger", "Data Model", OutputType = typeof(object))] +[Node("Data Model-Event", "Outputs the latest values of a data model event.", "Data Model", OutputType = typeof(object))] public class DataModelEventNode : Node, IDisposable { - private int _currentIndex; - private Type _currentType; + private readonly Dictionary, OutputPin> _propertyPins; private DataModelPath? _dataModelPath; - private object? _lastPathValue; - private DateTime _lastTrigger; - private bool _updating; - - public DataModelEventNode() : base("Data Model-Event", "Responds to a data model event trigger") + private IDataModelEvent? _dataModelEvent; + + public DataModelEventNode() : base("Data Model-Event", "Outputs the latest values of a data model event.") { - _currentType = typeof(object); - - CycleValues = CreateInputPinCollection(typeof(object), "", 0); - Output = CreateOutputPin(typeof(object)); - - CycleValues.PinAdded += CycleValuesOnPinAdded; - CycleValues.PinRemoved += CycleValuesOnPinRemoved; - CycleValues.Add(CycleValues.CreatePin()); + _propertyPins = new Dictionary, OutputPin>(); + + TimeSinceLastTrigger = CreateOutputPin("Time since trigger"); + TriggerCount = CreateOutputPin("Trigger count"); // Monitor storage for changes StorageModified += (_, _) => UpdateDataModelPath(); } public INodeScript? Script { get; set; } - - public InputPinCollection CycleValues { get; } - public OutputPin Output { get; } + public OutputPin TimeSinceLastTrigger { get; } + public OutputPin TriggerCount { get; } public override void Initialize(INodeScript script) { @@ -45,84 +41,53 @@ public class DataModelEventNode : Node pins = Pins.Skip(2).ToList(); + 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())); + } + } + public override void Evaluate() { object? pathValue = _dataModelPath?.GetValue(); - bool hasTriggered = pathValue is IDataModelEvent dataModelEvent - ? EvaluateEvent(dataModelEvent) - : EvaluateValue(pathValue); - - if (hasTriggered) - { - _currentIndex++; - - if (_currentIndex >= CycleValues.Count()) - _currentIndex = 0; - } - - object? outputValue = CycleValues.ElementAt(_currentIndex).PinValue; - if (Output.Type.IsInstanceOfType(outputValue)) - Output.Value = outputValue; - else if (Output.Type.IsValueType) - Output.Value = Output.Type.GetDefault()!; - } - - private bool EvaluateEvent(IDataModelEvent dataModelEvent) - { - if (dataModelEvent.LastTrigger <= _lastTrigger) - return false; - - _lastTrigger = dataModelEvent.LastTrigger; - return true; - } - - private bool EvaluateValue(object? pathValue) - { - if (Equals(pathValue, _lastPathValue)) - return false; - - _lastPathValue = pathValue; - return true; - } - - private void CycleValuesOnPinAdded(object? sender, SingleValueEventArgs e) - { - e.Value.PinConnected += OnPinConnected; - e.Value.PinDisconnected += OnPinDisconnected; - } - - private void CycleValuesOnPinRemoved(object? sender, SingleValueEventArgs e) - { - e.Value.PinConnected -= OnPinConnected; - e.Value.PinDisconnected -= OnPinDisconnected; - } - - private void OnPinDisconnected(object? sender, SingleValueEventArgs e) - { - ProcessPinDisconnected(); - } - - private void OnPinConnected(object? sender, SingleValueEventArgs e) - { - ProcessPinConnected(e.Value); - } - - private void ProcessPinConnected(IPin source) - { - if (_updating) + if (pathValue is not IDataModelEvent dataModelEvent) return; - try + TimeSinceLastTrigger.Value = new Numeric(dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds); + TriggerCount.Value = new Numeric(dataModelEvent.TriggerCount); + + foreach ((Func propertyAccessor, OutputPin outputPin) in _propertyPins) { - _updating = true; - - // No need to change anything if the types haven't changed - if (_currentType != source.Type) - ChangeCurrentType(source.Type); - } - finally - { - _updating = false; + if (!outputPin.ConnectedTo.Any()) + continue; + object value = dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!; + outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value; } } @@ -131,32 +96,10 @@ public class DataModelEventNode : Node v.ConnectedTo.Any())) - return; - - ChangeCurrentType(typeof(object)); - } - finally - { - _updating = false; - } + object? pathValue = _dataModelPath?.GetValue(); + if (pathValue is IDataModelEvent dataModelEvent) + CreatePins(dataModelEvent); } /// diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomView.axaml b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomView.axaml new file mode 100644 index 000000000..f98ebc0d9 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomView.axaml @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomView.axaml.cs new file mode 100644 index 000000000..3df2bafcc --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomView.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.DataModel.Screens; + +public class DataModelEventCycleNodeCustomView : ReactiveUserControl +{ + public DataModelEventCycleNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomViewModel.cs new file mode 100644 index 000000000..6e129a434 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventCycleNodeCustomViewModel.cs @@ -0,0 +1,104 @@ +using System.Collections.ObjectModel; +using System.Reactive.Disposables; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.Core.Services; +using Artemis.Storage.Entities.Profile; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; +using Artemis.UI.Shared.VisualScripting; +using ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.DataModel.Screens; + +public class DataModelEventCycleNodeCustomViewModel : CustomNodeViewModel +{ + private readonly DataModelEventCycleNode _cycleNode; + private readonly INodeEditorService _nodeEditorService; + private DataModelPath? _dataModelPath; + private ObservableCollection? _modules; + private bool _updating; + + public DataModelEventCycleNodeCustomViewModel(DataModelEventCycleNode cycleNode, INodeScript script, ISettingsService settingsService, INodeEditorService nodeEditorService) : base(cycleNode, script) + { + _cycleNode = cycleNode; + _nodeEditorService = nodeEditorService; + + ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); + ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); + Modules = new ObservableCollection(); + + this.WhenActivated(d => + { + // Set up extra modules + if (_cycleNode.Script?.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) + Modules = new ObservableCollection {scriptProfile.Configuration.Module}; + else if (_cycleNode.Script?.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) + Modules = new ObservableCollection {profileConfiguration.Module}; + + // Subscribe to node changes + _cycleNode.WhenAnyValue(n => n.Storage).Subscribe(UpdateDataModelPath).DisposeWith(d); + this.WhenAnyValue(vm => vm.DataModelPath).Subscribe(ApplyDataModelPath).DisposeWith(d); + + Disposable.Create(() => + { + _dataModelPath?.Dispose(); + _dataModelPath = null; + }).DisposeWith(d); + }); + } + + public PluginSetting ShowFullPaths { get; } + public PluginSetting ShowDataModelValues { get; } + + public ObservableCollection? Modules + { + get => _modules; + set => this.RaiseAndSetIfChanged(ref _modules, value); + } + + public DataModelPath? DataModelPath + { + get => _dataModelPath; + set => this.RaiseAndSetIfChanged(ref _dataModelPath, value); + } + + private void UpdateDataModelPath(DataModelPathEntity? entity) + { + try + { + if (_updating) + return; + + _updating = true; + + DataModelPath? old = DataModelPath; + DataModelPath = entity != null ? new DataModelPath(entity) : null; + old?.Dispose(); + } + finally + { + _updating = false; + } + } + + private void ApplyDataModelPath(DataModelPath? path) + { + try + { + if (_updating) + return; + if (path?.Path == _cycleNode.Storage?.Path) + return; + + _updating = true; + + path?.Save(); + _nodeEditorService.ExecuteCommand(Script, new UpdateStorage(_cycleNode, path?.Entity, "event")); + } + finally + { + _updating = false; + } + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventNodeCustomView.axaml b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventNodeCustomView.axaml index 7ab248004..5f988e54a 100644 --- a/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventNodeCustomView.axaml +++ b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventNodeCustomView.axaml @@ -12,6 +12,7 @@ Modules="{CompiledBinding Modules}" ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}" ShowFullPath="{CompiledBinding ShowFullPaths.Value}" + FilterTypes="{CompiledBinding FilterTypes}" IsEventPicker="True" VerticalAlignment="Top" MaxWidth="300"/> diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventNodeCustomViewModel.cs index 8d7a8f66f..c3ec5b539 100644 --- a/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/DataModel/Screens/DataModelEventNodeCustomViewModel.cs @@ -63,6 +63,8 @@ public class DataModelEventNodeCustomViewModel : CustomNodeViewModel set => this.RaiseAndSetIfChanged(ref _dataModelPath, value); } + public List FilterTypes => new() {typeof(IDataModelEvent)}; + private void UpdateDataModelPath(DataModelPathEntity? entity) { try diff --git a/src/Artemis.VisualScripting/Nodes/Static/Screens/DisplayValueNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/Static/Screens/DisplayValueNodeCustomViewModel.cs index c15f5459d..fc0c0b977 100644 --- a/src/Artemis.VisualScripting/Nodes/Static/Screens/DisplayValueNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/Static/Screens/DisplayValueNodeCustomViewModel.cs @@ -32,6 +32,14 @@ public class DisplayValueNodeCustomViewModel : CustomNodeViewModel private void Update(object? sender, EventArgs e) { - CurrentValue = _node.Input.Value; + try + { + CurrentValue = _node.Input.Value; + } + catch (Exception ex) + { + // Don't crash the timer on exceptions and display the messages as a bit of a nice to have + CurrentValue = ex.Message; + } } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Text/StringFormatNode.cs b/src/Artemis.VisualScripting/Nodes/Text/StringFormatNode.cs index b3c64e016..ef498bd59 100644 --- a/src/Artemis.VisualScripting/Nodes/Text/StringFormatNode.cs +++ b/src/Artemis.VisualScripting/Nodes/Text/StringFormatNode.cs @@ -21,7 +21,9 @@ public class StringFormatNode : Node public override void Evaluate() { - Output.Value = string.Format(Format.Value ?? string.Empty, Values.Values.ToArray()); + // Convert numerics to floats beforehand to allow string.Format to format them + object[] values = Values.Values.Select(v => v is Numeric n ? (float) n : v).ToArray(); + Output.Value = string.Format(Format.Value ?? string.Empty, values); } #endregion