From 3d92c9185fbe95e1e408238bf21f2e42b2285646 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 2 Oct 2020 19:49:37 +0200 Subject: [PATCH] Dynamic data models - WIP --- .../Models/Profile/DataModel/DataModelPath.cs | 61 ++++++++- .../Profile/DataModel/DataModelPathPart.cs | 122 +++++++++++++++++- .../DataModel/DataModelPathPartType.cs | 5 + .../Plugins/DataModelExpansions/DataModel.cs | 16 ++- .../DataModels/DynamicDataModel.cs | 15 +++ .../PluginDataModelExpansion.cs | 22 +++- 6 files changed, 227 insertions(+), 14 deletions(-) create mode 100644 src/Plugins/Artemis.Plugins.DataModelExpansions.TestData/DataModels/DynamicDataModel.cs diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs index 0cce661d3..a5a7bfa71 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using Artemis.Core.DataModelExpansions; namespace Artemis.Core @@ -10,17 +11,51 @@ namespace Artemis.Core /// public class DataModelPath { - private readonly LinkedList _parts; + private readonly List _parts; - internal DataModelPath() + // TODO: Make internal + public DataModelPath(DataModel dataModel, string path) { - _parts = new LinkedList(); + if (dataModel == null) + throw new ArgumentNullException(nameof(dataModel)); + if (path == null) + throw new ArgumentNullException(nameof(path)); + + _parts = new List(); + + DataModel = dataModel; + DataModelGuid = dataModel.PluginInfo.Guid; + + Initialize(path); + } + + private void Initialize(string path) + { + var parts = path.Split("."); + foreach (var identifier in parts) + _parts.Add(new DataModelPathPart(this, identifier)); + + var parameter = Expression.Parameter(typeof(DataModel), "dm"); + Expression expression = Expression.Convert(parameter, DataModel.GetType()); + Expression nullCondition = null; + DataModelPathPart previous = null; + + foreach (var part in _parts) + { + var notNull = Expression.NotEqual(expression, Expression.Constant(null)); + nullCondition = nullCondition != null ? Expression.AndAlso(nullCondition, notNull) : notNull; + expression = part.Initialize(previous, parameter, expression, nullCondition); + if (expression == null) + return; + + previous = part; + } } /// /// Gets the data model at which this path starts /// - public DataModel DataModel { get; private set; } + public DataModel DataModel { get; } /// /// Gets a read-only list of all parts of this path @@ -28,5 +63,23 @@ namespace Artemis.Core public IReadOnlyCollection Parts => _parts.ToList().AsReadOnly(); internal Guid DataModelGuid { get; set; } + + public string GetPathToPart(DataModelPathPart part) + { + var endIndex = _parts.IndexOf(part); + return endIndex < 0 ? null : string.Join('.', _parts.Take(endIndex + 1)); + } + + internal DataModelPathPart GetPartBefore(DataModelPathPart dataModelPathPart) + { + var index = _parts.IndexOf(dataModelPathPart); + return index > 0 ? _parts[index - 1] : null; + } + + internal DataModelPathPart GetPartAfter(DataModelPathPart dataModelPathPart) + { + var index = _parts.IndexOf(dataModelPathPart); + return index < _parts.Count - 1 ? _parts[index + 1] : null; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPart.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPart.cs index 8711f41dd..fc72190dc 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPart.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPart.cs @@ -1,18 +1,136 @@ -namespace Artemis.Core +using System; +using System.Linq.Expressions; +using System.Reflection; +using Artemis.Core.DataModelExpansions; + +namespace Artemis.Core { /// /// Represents a part of a data model path /// public class DataModelPathPart { + internal DataModelPathPart(DataModelPath dataModelPath, string identifier) + { + DataModelPath = dataModelPath; + Identifier = identifier; + } + + /// + /// Gets the data model path this is a part of + /// + public DataModelPath DataModelPath { get; } + /// /// Gets the identifier that is associated with this part /// - public string Identifier { get; private set; } + public string Identifier { get; } /// /// Gets the type of data model this part of the path points to /// public DataModelPathPartType Type { get; private set; } + + /// + /// Gets the type of dynamic data model this path points to + /// Not used if the is + /// + public Type DynamicDataModelType { get; private set; } + + /// + /// Gets the previous part in the path + /// + public DataModelPathPart Previous => DataModelPath.GetPartBefore(this); + + /// + /// Gets the next part in the path + /// + public DataModelPathPart Next => DataModelPath.GetPartAfter(this); + + internal Func Accessor { get; set; } + + /// + /// Returns the current value of the path up to this part + /// + /// + public object GetValue() + { + return Type == DataModelPathPartType.Invalid ? null : Accessor(DataModelPath.DataModel); + } + + private Expression CreateExpression(ParameterExpression parameter, Expression expression, Expression nullCondition) + { + if (Type == DataModelPathPartType.Invalid) + { + Accessor = null; + return null; + } + + Expression accessorExpression; + // A static part just needs to access the property or filed + if (Type == DataModelPathPartType.Static) + accessorExpression = Expression.PropertyOrField(expression, Identifier); + // A dynamic part calls the generic method DataModel.DynamicChild and provides the identifier as an argument + else + { + accessorExpression = Expression.Call( + expression, + nameof(DataModel.DynamicChild), + new[] {DynamicDataModelType}, + Expression.Constant(Identifier) + ); + } + + var test = Expression.Lambda>( + Expression.Condition(nullCondition, expression, Expression.Default(expression.Type)), // Wrap with a null check + parameter + ); + Accessor = Expression.Lambda>( + Expression.Condition(nullCondition, expression, Expression.Default(expression.Type)), // Wrap with a null check + parameter + ).Compile(); + + return accessorExpression; + } + + internal Expression Initialize(DataModelPathPart previous, ParameterExpression parameter, Expression expression, Expression nullCondition) + { + var previousValue = previous != null ? previous.GetValue() : DataModelPath.DataModel; + if (previousValue == null) + { + Type = DataModelPathPartType.Invalid; + return null; + } + + // Determine this part's type by looking for a dynamic data model with the identifier + if (previousValue is DataModel dataModel) + { + var hasDynamicDataModel = dataModel.DynamicDataModels.TryGetValue(Identifier, out var dynamicDataModel); + // If a dynamic data model is found the use that + if (hasDynamicDataModel) + DetermineDynamicType(dynamicDataModel); + // Otherwise look for a static type + else + DetermineStaticType(previousValue); + } + // Only data models can have dynamic types so if it is something else, its always going to be static + else + DetermineStaticType(previousValue); + + return CreateExpression(parameter, expression, nullCondition); + } + + private void DetermineDynamicType(DataModel dynamicDataModel) + { + Type = DataModelPathPartType.Dynamic; + DynamicDataModelType = dynamicDataModel.GetType(); + } + + private void DetermineStaticType(object previous) + { + var previousType = previous.GetType(); + var property = previousType.GetProperty(Identifier, BindingFlags.Public | BindingFlags.Instance); + Type = property == null ? DataModelPathPartType.Invalid : DataModelPathPartType.Static; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPartType.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPartType.cs index e922ba5b3..d3e494264 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPartType.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPartType.cs @@ -5,6 +5,11 @@ /// public enum DataModelPathPartType { + /// + /// Represents an invalid data model type that points to a missing data model + /// + Invalid, + /// /// Represents a static data model type that points to a data model defined in code /// diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs index edeee260a..f558f6e5d 100644 --- a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs +++ b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Artemis.Core.Modules; +using Humanizer; namespace Artemis.Core.DataModelExpansions { @@ -59,6 +60,11 @@ namespace Artemis.Core.DataModelExpansions /// An optional description public void AddDynamicChild(DataModel dynamicDataModel, string key, string name = null, string description = null) { + if (dynamicDataModel == null) + throw new ArgumentNullException(nameof(dynamicDataModel)); + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (_dynamicDataModels.ContainsKey(key)) { throw new ArtemisCoreException($"Cannot add a dynamic data model with key '{key}' " + @@ -72,6 +78,12 @@ namespace Artemis.Core.DataModelExpansions $"because the dynamic data model is already added with key '{existingKey}."); } + dynamicDataModel.PluginInfo = PluginInfo; + dynamicDataModel.DataModelDescription = new DataModelPropertyAttribute() + { + Name = name ?? key.Humanize(), + Description = description + }; _dynamicDataModels.Add(key, dynamicDataModel); } @@ -100,8 +112,8 @@ namespace Artemis.Core.DataModelExpansions /// /// The type of data model you expect /// The unique key of the dynamic data model - /// If found, the dynamic data model - public T GetDynamicChild(string key) where T : DataModel + /// If found, the dynamic data model otherwise null + public T DynamicChild(string key) where T : DataModel { _dynamicDataModels.TryGetValue(key, out var value); return value as T; diff --git a/src/Plugins/Artemis.Plugins.DataModelExpansions.TestData/DataModels/DynamicDataModel.cs b/src/Plugins/Artemis.Plugins.DataModelExpansions.TestData/DataModels/DynamicDataModel.cs new file mode 100644 index 000000000..53f309125 --- /dev/null +++ b/src/Plugins/Artemis.Plugins.DataModelExpansions.TestData/DataModels/DynamicDataModel.cs @@ -0,0 +1,15 @@ +using Artemis.Core.DataModelExpansions; + +namespace Artemis.Plugins.DataModelExpansions.TestData.DataModels +{ + public class DynamicDataModel : DataModel + { + public DynamicDataModel() + { + DynamicString = "Test 123"; + } + + [DataModelProperty(Description = "Descriptionnnnnn")] + public string DynamicString { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/Artemis.Plugins.DataModelExpansions.TestData/PluginDataModelExpansion.cs b/src/Plugins/Artemis.Plugins.DataModelExpansions.TestData/PluginDataModelExpansion.cs index f07a415c3..d8a2c34a5 100644 --- a/src/Plugins/Artemis.Plugins.DataModelExpansions.TestData/PluginDataModelExpansion.cs +++ b/src/Plugins/Artemis.Plugins.DataModelExpansions.TestData/PluginDataModelExpansion.cs @@ -1,5 +1,5 @@ using System; -using System.Linq; +using Artemis.Core; using Artemis.Core.DataModelExpansions; using Artemis.Plugins.DataModelExpansions.TestData.DataModels; using SkiaSharp; @@ -14,12 +14,13 @@ namespace Artemis.Plugins.DataModelExpansions.TestData { _rand = new Random(); AddTimedUpdate(TimeSpan.FromSeconds(1), TimedUpdate); - } - private void TimedUpdate(double deltaTime) - { - DataModel.TestColorA = SKColor.FromHsv(_rand.Next(0, 360), 100, 100); - DataModel.TestColorB = SKColor.FromHsv(_rand.Next(0, 360), 100, 100); + DataModel.AddDynamicChild(new DynamicDataModel(), "Dynamic1", "Dynamic data model 1"); + + // var testPath1 = new DataModelPath(DataModel, "TemplateDataModelString"); + var testPath2 = new DataModelPath(DataModel, "PluginSubDataModel.Number"); + // var testPath3 = new DataModelPath(DataModel, "Dynamic1.DynamicString"); + // var testPath4 = new DataModelPath(DataModel, "TemplateDataModelString"); } public override void DisablePlugin() @@ -30,6 +31,15 @@ namespace Artemis.Plugins.DataModelExpansions.TestData { // You can access your data model here and update it however you like DataModel.TemplateDataModelString = $"The last delta time was {deltaTime} seconds"; + + var dynamic = DataModel.DynamicChild("Dynamic1")?.DynamicString; + var dynamic2 = DataModel.DynamicChild("Dynamic2")?.DynamicString; + } + + private void TimedUpdate(double deltaTime) + { + DataModel.TestColorA = SKColor.FromHsv(_rand.Next(0, 360), 100, 100); + DataModel.TestColorB = SKColor.FromHsv(_rand.Next(0, 360), 100, 100); } } } \ No newline at end of file