Core - Fix data model and event condition bugs found by the tests (#910)

* Core - Fix removing dynamic children and checking which properties are in use

- DataModel.RemoveDynamicChild compared the value of each child with the
  child that was passed in, so it never removed anything.
- IsPropertyInUse with includeChildren matched paths by their start, so
  a path to "Integers" made "Integer" count as in use. Children are now
  matched on what follows a dot.

* Core - Fix data model paths around saving, removed children and stored types

- The constructors saved the path before setting it up, while it was
  still invalid, so a new path stored nothing and didn't follow its data
  model leaving and returning to the store until it was saved.
- Removing the dynamic child a path points to left the path valid and
  reading a default value, it now becomes invalid like when the child
  was never there.
- The stored type of a path is now also looked up in all loaded
  assemblies, Type.GetType only found it in Artemis.Core and the .NET
  libraries, not in plugins or SkiaSharp.

* Core - Only let event conditions react to what happens after they start watching

- A trigger from before the condition watched its event, for example
  long before the profile was activated, counted as new and played the
  element once. Triggers now count from the moment the condition starts
  watching its path.
- A condition on a plain value treated the first value it saw as a
  change and played the element on the first frame. The first value is
  now taken as the starting value.
This commit is contained in:
RobertBeekman authored and GitHub committed 2026-10-20 13:46:06 +02:00
1 parent a25d0f0c9c
commit 6df81b19b1
7 files changed
+108 -50

No files matched your search

@@ -17,8 +17,10 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
private NodeScript<bool> _script;
private DefaultNode _startNode;
private DataModelPath? _eventPath;
private DateTime _lastProcessedTrigger;
// Only what happens after the condition starts watching its path counts, see StartWatching
private DateTime _lastProcessedTrigger = DateTime.Now;
private object? _lastProcessedValue;
private bool _hasProcessedValue;
private EventOverlapMode _overlapMode;
private EventToggleOffMode _toggleOffMode;
private EventTriggerMode _triggerMode;
@@ -64,7 +66,11 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
public DataModelPath? EventPath
{
get => _eventPath;
set => SetAndNotify(ref _eventPath, value);
set
{
if (SetAndNotify(ref _eventPath, value))
StartWatching();
}
}
/// <summary>
@@ -243,6 +249,15 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
Script.AddNode(newStartNode);
}
private void StartWatching()
{
// Triggers and values from before the condition watched the path don't count, otherwise for example an event that
// triggered long before the profile was activated would still play the element once
_lastProcessedTrigger = DateTime.Now;
_lastProcessedValue = null;
_hasProcessedValue = false;
}
private bool Evaluate()
{
if (EventPath == null)
@@ -258,6 +273,14 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
}
else if (_startNode is EventConditionValueChangedStartNode valueChangedNode)
{
// The first value seen is where the value starts, not a change
if (!_hasProcessedValue)
{
_lastProcessedValue = value;
_hasProcessedValue = true;
return false;
}
if (Equals(value, _lastProcessedValue))
return false;
@@ -29,8 +29,8 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
_segments = [];
Save();
Initialize();
Save();
SubscribeToDataModelStore();
}
@@ -47,8 +47,8 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
_segments = [];
Save();
Initialize();
Save();
SubscribeToDataModelStore();
}
@@ -67,8 +67,8 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
_segments = [];
Save();
Initialize();
Save();
SubscribeToDataModelStore();
}
@@ -165,11 +165,20 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
// Fall back to stored type
if (!string.IsNullOrWhiteSpace(Entity.Type))
return Type.GetType(Entity.Type);
return FindType(Entity.Type);
return null;
}
private static Type? FindType(string fullName)
{
// The type is stored without its assembly, Type.GetType only finds it in Artemis.Core and the .NET libraries. Types
// of plugins and other libraries (like SKColor) are found by looking through the loaded assemblies
return Type.GetType(fullName) ?? AppDomain.CurrentDomain.GetAssemblies()
.Select(assembly => assembly.GetType(fullName))
.FirstOrDefault(type => type != null);
}
/// <summary>
/// Gets the property description of the property this path points to
/// </summary>
@@ -286,8 +286,16 @@ public class DataModelPathSegment : IDisposable
private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.DynamicChild.BaseValue == _dynamicDataModel)
// Like when a child is added, initialize the path again so it no longer points to the removed child
if (e.Key == Identifier)
{
DataModelPath.Invalidate();
DataModelPath.Initialize();
}
else if (e.DynamicChild.BaseValue == _dynamicDataModel)
{
DataModelPath.Invalidate();
}
}
/// <inheritdoc />