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
This commit is contained in:
Robert committed 2022-08-23 20:35:25 +02:00
1 parent 3efb97eedd
commit d2e0607622
15 files changed
+488 -160

No files matched your search

@@ -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<DataModelPathEntity, DataModelEventCycleNodeCustomViewModel>, 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<IPin> e)
{
e.Value.PinConnected += OnPinConnected;
e.Value.PinDisconnected += OnPinDisconnected;
}
private void CycleValuesOnPinRemoved(object? sender, SingleValueEventArgs<IPin> e)
{
e.Value.PinConnected -= OnPinConnected;
e.Value.PinDisconnected -= OnPinDisconnected;
}
private void OnPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
{
ProcessPinDisconnected();
}
private void OnPinConnected(object? sender, SingleValueEventArgs<IPin> 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;
}
}
/// <inheritdoc />
public void Dispose()
{
_dataModelPath?.Dispose();
}
}
@@ -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<DataModelPathEntity, DataModelEventNodeCustomViewModel>, IDisposable
{
private int _currentIndex;
private Type _currentType;
private readonly Dictionary<Func<DataModelEventArgs, object>, 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<Func<DataModelEventArgs, object>, OutputPin>();
TimeSinceLastTrigger = CreateOutputPin<Numeric>("Time since trigger");
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
// Monitor storage for changes
StorageModified += (_, _) => UpdateDataModelPath();
}
public INodeScript? Script { get; set; }
public InputPinCollection CycleValues { get; }
public OutputPin Output { get; }
public OutputPin<Numeric> TimeSinceLastTrigger { get; }
public OutputPin<Numeric> TriggerCount { get; }
public override void Initialize(INodeScript script)
{
@@ -45,84 +41,53 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
UpdateDataModelPath();
}
private void CreatePins(IDataModelEvent? dataModelEvent)
{
if (_dataModelEvent == dataModelEvent)
return;
List<IPin> 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<DataModelEventArgs, object> expression = Expression.Lambda<Func<DataModelEventArgs, object>>(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<IPin> e)
{
e.Value.PinConnected += OnPinConnected;
e.Value.PinDisconnected += OnPinDisconnected;
}
private void CycleValuesOnPinRemoved(object? sender, SingleValueEventArgs<IPin> e)
{
e.Value.PinConnected -= OnPinConnected;
e.Value.PinDisconnected -= OnPinDisconnected;
}
private void OnPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
{
ProcessPinDisconnected();
}
private void OnPinConnected(object? sender, SingleValueEventArgs<IPin> 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<DataModelEventArgs, object> 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<DataModelPathEntity, DataModelEventNodeCu
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;
}
object? pathValue = _dataModelPath?.GetValue();
if (pathValue is IDataModelEvent dataModelEvent)
CreatePins(dataModelEvent);
}
/// <inheritdoc />
@@ -0,0 +1,18 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.DataModel.Screens"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.DataModelPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.DataModel.Screens.DataModelEventCycleNodeCustomView"
x:DataType="screens:DataModelEventCycleNodeCustomViewModel">
<dataModelPicker:DataModelPickerButton Classes="condensed"
DataModelPath="{CompiledBinding DataModelPath}"
Modules="{CompiledBinding Modules}"
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
IsEventPicker="True"
VerticalAlignment="Top"
MaxWidth="300"/>
</UserControl>
@@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
public class DataModelEventCycleNodeCustomView : ReactiveUserControl<DataModelEventCycleNodeCustomViewModel>
{
public DataModelEventCycleNodeCustomView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -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<Module>? _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<Module>();
this.WhenActivated(d =>
{
// Set up extra modules
if (_cycleNode.Script?.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null)
Modules = new ObservableCollection<Module> {scriptProfile.Configuration.Module};
else if (_cycleNode.Script?.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null)
Modules = new ObservableCollection<Module> {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<bool> ShowFullPaths { get; }
public PluginSetting<bool> ShowDataModelValues { get; }
public ObservableCollection<Module>? 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<DataModelPathEntity>(_cycleNode, path?.Entity, "event"));
}
finally
{
_updating = false;
}
}
}
@@ -12,6 +12,7 @@
Modules="{CompiledBinding Modules}"
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
FilterTypes="{CompiledBinding FilterTypes}"
IsEventPicker="True"
VerticalAlignment="Top"
MaxWidth="300"/>
@@ -63,6 +63,8 @@ public class DataModelEventNodeCustomViewModel : CustomNodeViewModel
set => this.RaiseAndSetIfChanged(ref _dataModelPath, value);
}
public List<Type> FilterTypes => new() {typeof(IDataModelEvent)};
private void UpdateDataModelPath(DataModelPathEntity? entity)
{
try
@@ -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;
}
}
}
@@ -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