Profiles - Reworked profile system

Sidebar - Redesigned sidebar with customizable categories
Profiles - Added the ability to configure custom profile icons
Profiles - Added the ability to activate multiple profiles for modules at once
Profiles - Added the ability to create profiles for no modules
Profiles - Added the ability to suspend a profile or an entire category
Profiles - Added profile activation conditions
Profiles - Added file-based importing/exporting
Profile editor - Condensed UI, removed tabs 
Profile editor - Disable condition operators until a left-side is picked
This commit is contained in:
Robert Beekman authored and GitHub committed 2021-06-03 22:34:43 +02:00
1 parent 4152c290d2
commit ceeaa4bf6d
164 files changed
+5019 -4220

No files matched your search

@@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=defaulttypes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofileconfiguration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaption/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaptionhints/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaption_005Chints/@EntryIndexedValue">True</s:Boolean>
@@ -0,0 +1,21 @@
using System;
using Artemis.Core.Modules;
namespace Artemis.Core
{
/// <summary>
/// Provides data about module events
/// </summary>
public class ModuleEventArgs : EventArgs
{
internal ModuleEventArgs(Module module)
{
Module = module;
}
/// <summary>
/// Gets the module this event is related to
/// </summary>
public Module Module { get; }
}
}
@@ -0,0 +1,20 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data for profile configuration events.
/// </summary>
public class ProfileConfigurationEventArgs : EventArgs
{
internal ProfileConfigurationEventArgs(ProfileConfiguration profileConfiguration)
{
ProfileConfiguration = profileConfiguration;
}
/// <summary>
/// Gets the profile configuration this event is related to
/// </summary>
public ProfileConfiguration ProfileConfiguration { get; }
}
}
@@ -0,0 +1,45 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace Artemis.Core.JsonConverters
{
/// <inheritdoc />
public class StreamConverter : JsonConverter<Stream>
{
#region Overrides of JsonConverter<Stream>
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
using MemoryStream memoryStream = new();
value.Position = 0;
value.CopyTo(memoryStream);
writer.WriteValue(memoryStream.ToArray());
}
/// <inheritdoc />
public override Stream? ReadJson(JsonReader reader, Type objectType, Stream? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value is not string base64)
return null;
if (existingValue == null || !hasExistingValue || !existingValue.CanRead)
return new MemoryStream(Convert.FromBase64String(base64));
using MemoryStream memoryStream = new(Convert.FromBase64String(base64));
existingValue.Position = 0;
memoryStream.CopyTo(existingValue);
existingValue.Position = 0;
return existingValue;
}
#endregion
}
}
@@ -105,37 +105,66 @@ namespace Artemis.Core
InitializeRightPath();
// Right side static
else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null)
{
try
{
if (LeftPath != null && LeftPath.IsValid)
// If the left path is not valid we cannot reliably set up the right side because the type is unknown
// Because of that wait for it to validate first
if (LeftPath != null && !LeftPath.IsValid)
{
// Use the left side type so JSON.NET has a better idea what to do
Type leftSideType = LeftPath.GetPropertyType()!;
object? rightSideValue;
try
{
rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType);
}
// If deserialization fails, use the type's default
catch (JsonSerializationException e)
{
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
rightSideValue = Activator.CreateInstance(leftSideType);
}
UpdateRightSideStatic(rightSideValue);
LeftPath.PathValidated += InitializeRightSideStatic;
return;
}
else
if (LeftPath == null)
return;
// Use the left side type so JSON.NET has a better idea what to do
Type leftSideType = LeftPath.GetPropertyType()!;
object? rightSideValue;
try
{
// Hope for the best...
UpdateRightSideStatic(CoreJson.DeserializeObject(Entity.RightStaticValue));
rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType);
}
// If deserialization fails, use the type's default
catch (JsonSerializationException e)
{
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
rightSideValue = Activator.CreateInstance(leftSideType);
}
UpdateRightSideStatic(rightSideValue);
}
catch (JsonReaderException e)
{
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
}
}
}
private void InitializeRightSideStatic(object? sender, EventArgs args)
{
if (LeftPath == null)
return;
LeftPath.PathValidated -= InitializeRightSideStatic;
// Use the left side type so JSON.NET has a better idea what to do
Type leftSideType = LeftPath.GetPropertyType()!;
object? rightSideValue;
try
{
rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType);
}
// If deserialization fails, use the type's default
catch (JsonSerializationException e)
{
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
rightSideValue = Activator.CreateInstance(leftSideType);
}
UpdateRightSideStatic(rightSideValue);
}
/// <summary>
@@ -16,7 +16,7 @@ namespace Artemis.Core
{
internal EventPredicateWrapperDataModel()
{
Feature = Constants.CorePluginFeature;
Module = Constants.CorePluginFeature;
}
/// <summary>
@@ -16,7 +16,7 @@ namespace Artemis.Core
{
internal ListPredicateWrapperDataModel()
{
Feature = Constants.CorePluginFeature;
Module = Constants.CorePluginFeature;
}
/// <summary>
@@ -93,7 +93,7 @@ namespace Artemis.Core
/// <summary>
/// Gets the data model ID of the <see cref="Target" /> if it is a <see cref="DataModel" />
/// </summary>
public string? DataModelId => Target?.Feature.Id;
public string? DataModelId => Target?.Module.Id;
/// <summary>
/// Gets the point-separated path associated with this <see cref="DataModelPath" />
@@ -327,7 +327,7 @@ namespace Artemis.Core
private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Feature.Id != Entity.DataModelId)
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Target = e.Registration.DataModel;
@@ -336,7 +336,7 @@ namespace Artemis.Core
private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Feature.Id != Entity.DataModelId)
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Target = null;
+1 -7
View File
@@ -205,13 +205,6 @@ namespace Artemis.Core
canvas.SaveLayer(layerPaint);
canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y);
// If required, apply the opacity override of the module to the root folder
if (IsRootFolder && Profile.Module.OpacityOverride < 1)
{
double multiplier = Easings.SineEaseInOut(Profile.Module.OpacityOverride);
layerPaint.Color = layerPaint.Color.WithAlpha((byte) (layerPaint.Color.Alpha * multiplier));
}
// No point rendering if the alpha was set to zero by one of the effects
if (layerPaint.Color.Alpha == 0)
return;
@@ -240,6 +233,7 @@ namespace Artemis.Core
{
Disposed = true;
Disable();
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
+2
View File
@@ -168,6 +168,8 @@ namespace Artemis.Core
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
Disable();
Disposed = true;
// Brush first in case it depends on any of the other disposables during it's own disposal
+10 -79
View File
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
@@ -13,31 +12,15 @@ namespace Artemis.Core
public sealed class Profile : ProfileElement
{
private readonly object _lock = new();
private bool _isActivated;
private bool _isFreshImport;
internal Profile(ProfileModule module, string name) : base(null!)
{
ProfileEntity = new ProfileEntity();
EntityId = Guid.NewGuid();
Profile = this;
Module = module;
Name = name;
UndoStack = new Stack<string>();
RedoStack = new Stack<string>();
Folder _ = new(this, "Root folder");
Save();
}
internal Profile(ProfileModule module, ProfileEntity profileEntity) : base(null!)
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
Configuration = configuration;
Profile = this;
ProfileEntity = profileEntity;
EntityId = profileEntity.Id;
Module = module;
UndoStack = new Stack<string>();
RedoStack = new Stack<string>();
@@ -45,18 +28,9 @@ namespace Artemis.Core
}
/// <summary>
/// Gets the module backing this profile
/// Gets the profile configuration of this profile
/// </summary>
public ProfileModule Module { get; }
/// <summary>
/// Gets a boolean indicating whether this profile is activated
/// </summary>
public bool IsActivated
{
get => _isActivated;
private set => SetAndNotify(ref _isActivated, value);
}
public ProfileConfiguration Configuration { get; }
/// <summary>
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
@@ -87,8 +61,6 @@ namespace Artemis.Core
{
if (Disposed)
throw new ObjectDisposedException("Profile");
if (!IsActivated)
throw new ArtemisCoreException($"Cannot update inactive profile: {this}");
foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime);
@@ -102,8 +74,6 @@ namespace Artemis.Core
{
if (Disposed)
throw new ObjectDisposedException("Profile");
if (!IsActivated)
throw new ArtemisCoreException($"Cannot render inactive profile: {this}");
foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition);
@@ -133,7 +103,7 @@ namespace Artemis.Core
/// <inheritdoc />
public override string ToString()
{
return $"[Profile] {nameof(Name)}: {Name}, {nameof(IsActivated)}: {IsActivated}, {nameof(Module)}: {Module}";
return $"[Profile] {nameof(Name)}: {Name}";
}
/// <summary>
@@ -149,29 +119,15 @@ namespace Artemis.Core
layer.PopulateLeds(devices);
}
/// <summary>
/// Occurs when the profile has been activated.
/// </summary>
public event EventHandler? Activated;
/// <summary>
/// Occurs when the profile is being deactivated.
/// </summary>
public event EventHandler? Deactivated;
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (!disposing)
return;
OnDeactivating();
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
IsActivated = false;
Disposed = true;
}
@@ -180,7 +136,7 @@ namespace Artemis.Core
if (Disposed)
throw new ObjectDisposedException("Profile");
Name = ProfileEntity.Name;
Name = Configuration.Name;
IsFreshImport = ProfileEntity.IsFreshImport;
lock (ChildrenList)
@@ -197,7 +153,9 @@ namespace Artemis.Core
Folder _ = new(this, "Root folder");
}
else
{
AddChild(new Folder(this, this, rootFolder));
}
}
}
@@ -207,9 +165,7 @@ namespace Artemis.Core
throw new ObjectDisposedException("Profile");
ProfileEntity.Id = EntityId;
ProfileEntity.ModuleId = Module.Id;
ProfileEntity.Name = Name;
ProfileEntity.IsActive = IsActivated;
ProfileEntity.Name = Configuration.Name;
ProfileEntity.IsFreshImport = IsFreshImport;
foreach (ProfileElement profileElement in Children)
@@ -221,30 +177,5 @@ namespace Artemis.Core
ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
}
internal void Activate(IEnumerable<ArtemisDevice> devices)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
if (IsActivated)
return;
PopulateLeds(devices);
OnActivated();
IsActivated = true;
}
}
private void OnActivated()
{
Activated?.Invoke(this, EventArgs.Empty);
}
private void OnDeactivating()
{
Deactivated?.Invoke(this, EventArgs.Empty);
}
}
}
@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
public class ProfileCategory : CorePropertyChanged, IStorageModel
{
private readonly List<ProfileConfiguration> _profileConfigurations = new();
private bool _isCollapsed;
private bool _isSuspended;
private string _name;
private int _order;
/// <summary>
/// Creates a new instance of the <see cref="ProfileCategory" /> class
/// </summary>
/// <param name="name">The name of the category</param>
internal ProfileCategory(string name)
{
_name = name;
Entity = new ProfileCategoryEntity();
}
internal ProfileCategory(ProfileCategoryEntity entity)
{
Entity = entity;
Load();
}
/// <summary>
/// Gets or sets the name of the profile category
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this category appears in the update loop and sidebar
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the category is collapsed or not
/// <para>Note: Has no implications other than inside the UI</para>
/// </summary>
public bool IsCollapsed
{
get => _isCollapsed;
set => SetAndNotify(ref _isCollapsed, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a read only collection of the profiles inside this category
/// </summary>
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations => _profileConfigurations.AsReadOnly();
/// <summary>
/// Gets the unique ID of this category
/// </summary>
public Guid EntityId => Entity.Id;
internal ProfileCategoryEntity Entity { get; }
/// <summary>
/// Adds a profile configuration to this category
/// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
{
// Removing the original will shift every item in the list forwards, keep that in mind with the target index
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
targetIndex -= 1;
configuration.Category.RemoveProfileConfiguration(configuration);
if (targetIndex != null)
_profileConfigurations.Insert(Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count), configuration);
else
_profileConfigurations.Add(configuration);
configuration.Category = this;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationAdded(new ProfileConfigurationEventArgs(configuration));
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileCategory] {Order} {nameof(Name)}: {Name}, {nameof(IsSuspended)}: {IsSuspended}";
}
internal void RemoveProfileConfiguration(ProfileConfiguration configuration)
{
if (!_profileConfigurations.Remove(configuration)) return;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationRemoved(new ProfileConfigurationEventArgs(configuration));
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Name = Entity.Name;
IsCollapsed = Entity.IsCollapsed;
IsSuspended = Entity.IsSuspended;
Order = Entity.Order;
_profileConfigurations.Clear();
foreach (ProfileConfigurationEntity entityProfileConfiguration in Entity.ProfileConfigurations)
_profileConfigurations.Add(new ProfileConfiguration(this, entityProfileConfiguration));
}
/// <inheritdoc />
public void Save()
{
Entity.Name = Name;
Entity.IsCollapsed = IsCollapsed;
Entity.IsSuspended = IsSuspended;
Entity.Order = Order;
Entity.ProfileConfigurations.Clear();
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
{
profileConfiguration.Save();
Entity.ProfileConfigurations.Add(profileConfiguration.Entity);
}
}
#endregion
#region Events
/// <summary>
/// Occurs when a profile configuration is added to this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationAdded;
/// <summary>
/// Occurs when a profile configuration is removed from this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationRemoved;
/// <summary>
/// Invokes the <see cref="ProfileConfigurationAdded" /> event
/// </summary>
protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e)
{
ProfileConfigurationAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="ProfileConfigurationRemoved" /> event
/// </summary>
protected virtual void OnProfileConfigurationRemoved(ProfileConfigurationEventArgs e)
{
ProfileConfigurationRemoved?.Invoke(this, e);
}
#endregion
}
/// <summary>
/// Represents a name of one of the default categories
/// </summary>
public enum DefaultCategoryName
{
/// <summary>
/// The category used by profiles tied to games
/// </summary>
Games,
/// <summary>
/// The category used by profiles tied to applications
/// </summary>
Applications,
/// <summary>
/// The category used by general profiles
/// </summary>
General
}
/// <summary>
/// Represents a type of behaviour when this profile is activated
/// </summary>
public enum ActivationBehaviour
{
/// <summary>
/// Do nothing to other profiles
/// </summary>
None,
/// <summary>
/// Disable all other profiles
/// </summary>
DisableOthers,
/// <summary>
/// Disable all other profiles below this one
/// </summary>
DisableOthersBelow,
/// <summary>
/// Disable all other profiles above this one
/// </summary>
DisableOthersAbove,
/// <summary>
/// Disable all other profiles in the same category
/// </summary>
DisableOthersInCategory,
/// <summary>
/// Disable all other profiles below this one in the same category
/// </summary>
DisableOthersBelowInCategory,
/// <summary>
/// Disable all other profiles above this one in the same category
/// </summary>
DisableOthersAboveInCategory
}
}
@@ -1,41 +0,0 @@
using System;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
/// <summary>
/// Represents a descriptor that describes a profile
/// </summary>
public class ProfileDescriptor : CorePropertyChanged
{
internal ProfileDescriptor(ProfileModule profileModule, ProfileEntity profileEntity)
{
ProfileModule = profileModule;
Id = profileEntity.Id;
Name = profileEntity.Name;
IsLastActiveProfile = profileEntity.IsActive;
}
/// <summary>
/// Gets the module backing the profile
/// </summary>
public ProfileModule ProfileModule { get; }
/// <summary>
/// Gets the unique ID of the profile by which it can be loaded from storage
/// </summary>
public Guid Id { get; }
/// <summary>
/// Gets the name of the profile
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a boolean indicating whether this was the last active profile
/// </summary>
public bool IsLastActiveProfile { get; }
}
}
@@ -11,22 +11,21 @@ namespace Artemis.Core
/// </summary>
public abstract class ProfileElement : CorePropertyChanged, IDisposable
{
private bool _suspended;
private Guid _entityId;
private string? _name;
private int _order;
private ProfileElement? _parent;
private Profile _profile;
private bool _suspended;
internal List<ProfileElement> ChildrenList;
internal bool Disposed;
internal ProfileElement(Profile profile)
{
_profile = profile;
ChildrenList = new List<ProfileElement>();
}
/// <summary>
/// Gets the unique ID of this profile element
/// </summary>
@@ -95,6 +94,11 @@ namespace Artemis.Core
set => SetAndNotify(ref _suspended, value);
}
/// <summary>
/// Gets a boolean indicating whether the profile element is disposed
/// </summary>
public bool Disposed { get; protected set; }
/// <summary>
/// Updates the element
/// </summary>
@@ -0,0 +1,210 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
public class ProfileConfiguration : CorePropertyChanged, IStorageModel
{
private ProfileCategory _category;
private bool _isMissingModule;
private bool _isSuspended;
private Module? _module;
private string _name;
private int _order;
private Profile? _profile;
internal ProfileConfiguration(ProfileCategory category, string name, string icon)
{
_name = name;
_category = category;
Entity = new ProfileConfigurationEntity();
Icon = new ProfileConfigurationIcon(Entity) {MaterialIcon = icon};
}
internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity)
{
// Will be loaded from the entity
_name = null!;
_category = category;
Entity = entity;
Icon = new ProfileConfigurationIcon(Entity);
Load();
}
/// <summary>
/// Gets or sets the name of this profile configuration
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this profile appears in the update loop and sidebar
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this profile is suspended, disabling it regardless of the
/// <see cref="ActivationCondition" />
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a boolean indicating whether this profile configuration is missing any modules
/// </summary>
public bool IsMissingModule
{
get => _isMissingModule;
private set => SetAndNotify(ref _isMissingModule, value);
}
/// <summary>
/// Gets or sets the category of this profile configuration
/// </summary>
public ProfileCategory Category
{
get => _category;
internal set => SetAndNotify(ref _category, value);
}
/// <summary>
/// Gets the icon configuration
/// </summary>
public ProfileConfigurationIcon Icon { get; }
/// <summary>
/// Gets the profile of this profile configuration
/// </summary>
public Profile? Profile
{
get => _profile;
internal set => SetAndNotify(ref _profile, value);
}
/// <summary>
/// Gets or sets the behaviour of when this profile is activated
/// </summary>
public ActivationBehaviour ActivationBehaviour { get; set; }
/// <summary>
/// Gets the data model condition that must evaluate to <see langword="true" /> for this profile to be activated
/// alongside any activation requirements of the <see cref="Module" />, if set
/// </summary>
public DataModelConditionGroup? ActivationCondition { get; set; }
/// <summary>
/// Gets or sets the module this profile uses
/// </summary>
public Module? Module
{
get => _module;
set
{
_module = value;
IsMissingModule = false;
}
}
/// <summary>
/// Gets a boolean indicating whether the activation conditions where met during the last <see cref="Update" /> call
/// </summary>
public bool ActivationConditionMet { get; private set; }
/// <summary>
/// Gets or sets a boolean indicating whether this profile configuration is being edited
/// </summary>
public bool IsBeingEdited { get; set; }
/// <summary>
/// Gets the entity used by this profile config
/// </summary>
public ProfileConfigurationEntity Entity { get; }
/// <summary>
/// Updates this configurations activation condition status
/// </summary>
public void Update()
{
ActivationConditionMet = ActivationCondition == null || ActivationCondition.Evaluate();
}
public bool ShouldBeActive(bool includeActivationCondition)
{
if (Category.IsSuspended || IsSuspended || IsMissingModule)
return false;
if (includeActivationCondition)
return ActivationConditionMet && (Module == null || Module.IsActivated);
return Module == null || Module.IsActivated;
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileConfiguration] {nameof(Name)}: {Name}";
}
internal void LoadModules(List<Module> enabledModules)
{
Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ModuleId);
IsMissingModule = Module == null && Entity.ModuleId != null;
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Name = Entity.Name;
IsSuspended = Entity.IsSuspended;
ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour;
Icon.Load();
ActivationCondition = Entity.ActivationCondition != null
? new DataModelConditionGroup(null, Entity.ActivationCondition)
: null;
}
/// <inheritdoc />
public void Save()
{
Entity.Name = Name;
Entity.IsSuspended = IsSuspended;
Entity.ActivationBehaviour = (int) ActivationBehaviour;
Entity.ProfileCategoryId = Category.Entity.Id;
Icon.Save();
if (ActivationCondition != null)
{
ActivationCondition.Save();
Entity.ActivationCondition = ActivationCondition.Entity;
}
else
{
Entity.ActivationCondition = null;
}
if (!IsMissingModule)
Entity.ModuleId = Module?.Id;
}
#endregion
}
}
@@ -0,0 +1,30 @@
using System.IO;
using Artemis.Core.JsonConverters;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
namespace Artemis.Core
{
/// <summary>
/// A model that can be used to serialize a profile configuration, it's profile and it's icon
/// </summary>
public class ProfileConfigurationExportModel
{
/// <summary>
/// Gets or sets the storage entity of the profile configuration
/// </summary>
public ProfileConfigurationEntity? ProfileConfigurationEntity { get; set; }
/// <summary>
/// Gets or sets the storage entity of the profile
/// </summary>
[JsonProperty(Required = Required.Always)]
public ProfileEntity ProfileEntity { get; set; } = null!;
/// <summary>
/// Gets or sets a stream containing the profile image
/// </summary>
[JsonConverter(typeof(StreamConverter))]
public Stream? ProfileImage { get; set; }
}
}
@@ -0,0 +1,89 @@
using System.ComponentModel;
using System.IO;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
/// <summary>
/// Represents the icon of a <see cref="ProfileConfiguration" />
/// </summary>
public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel
{
private readonly ProfileConfigurationEntity _entity;
private Stream? _fileIcon;
private ProfileConfigurationIconType _iconType;
private string? _materialIcon;
internal ProfileConfigurationIcon(ProfileConfigurationEntity entity)
{
_entity = entity;
}
/// <summary>
/// Gets or sets the type of icon this profile configuration uses
/// </summary>
public ProfileConfigurationIconType IconType
{
get => _iconType;
set => SetAndNotify(ref _iconType, value);
}
/// <summary>
/// Gets or sets the icon if it is a Material icon
/// </summary>
public string? MaterialIcon
{
get => _materialIcon;
set => SetAndNotify(ref _materialIcon, value);
}
/// <summary>
/// Gets or sets a stream containing the icon if it is bitmap or SVG
/// </summary>
/// <returns></returns>
public Stream? FileIcon
{
get => _fileIcon;
set => SetAndNotify(ref _fileIcon, value);
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
IconType = (ProfileConfigurationIconType) _entity.IconType;
MaterialIcon = _entity.MaterialIcon;
}
/// <inheritdoc />
public void Save()
{
_entity.IconType = (int) IconType;
_entity.MaterialIcon = MaterialIcon;
}
#endregion
}
/// <summary>
/// Represents a type of profile icon
/// </summary>
public enum ProfileConfigurationIconType
{
/// <summary>
/// An icon picked from the Material Design Icons collection
/// </summary>
[Description("Material Design Icon")] MaterialIcon,
/// <summary>
/// A bitmap image icon
/// </summary>
[Description("Bitmap Image")] BitmapImage,
/// <summary>
/// An SVG image icon
/// </summary>
[Description("SVG Image")] SvgImage
}
}
@@ -1,53 +0,0 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace Artemis.Core.DataModelExpansions
{
/// <summary>
/// Allows you to expand the application-wide datamodel
/// </summary>
public abstract class DataModelExpansion<T> : BaseDataModelExpansion where T : DataModel
{
/// <summary>
/// The main data model of this data model expansion
/// <para>Note: This default data model is automatically registered upon plugin enable</para>
/// </summary>
public T DataModel
{
get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model");
internal set => InternalDataModel = value;
}
/// <summary>
/// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <param name="propertyLambda">A lambda expression pointing to the property to ignore</param>
public void HideProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo)))
HiddenPropertiesList.Add(propertyInfo);
}
/// <summary>
/// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm =>
/// dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <param name="propertyLambda">A lambda expression pointing to the property to stop ignoring</param>
public void ShowProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo));
}
internal override void InternalEnable()
{
DataModel = Activator.CreateInstance<T>();
DataModel.Feature = this;
DataModel.DataModelDescription = GetDataModelDescription();
base.InternalEnable();
}
}
}
@@ -1,47 +0,0 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
namespace Artemis.Core.DataModelExpansions
{
/// <summary>
/// For internal use only, to implement your own layer property type, extend <see cref="DataModelExpansion{T}" />
/// instead.
/// </summary>
public abstract class BaseDataModelExpansion : DataModelPluginFeature
{
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
protected internal readonly List<PropertyInfo> HiddenPropertiesList = new();
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
public ReadOnlyCollection<PropertyInfo> HiddenProperties => HiddenPropertiesList.AsReadOnly();
internal DataModel? InternalDataModel { get; set; }
/// <summary>
/// Called each frame when the data model should update
/// </summary>
/// <param name="deltaTime">Time in seconds since the last update</param>
public abstract void Update(double deltaTime);
internal void InternalUpdate(double deltaTime)
{
if (InternalDataModel != null)
Update(deltaTime);
}
/// <summary>
/// Override to provide your own data model description. By default this returns a description matching your plugin
/// name and description
/// </summary>
/// <returns></returns>
public virtual DataModelPropertyAttribute GetDataModelDescription()
{
return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description};
}
}
}
@@ -1,13 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents an feature of a certain type provided by a plugin with support for data models
/// </summary>
public abstract class DataModelPluginFeature : PluginFeature
{
}
}
@@ -6,7 +6,7 @@ using SkiaSharp;
namespace Artemis.Core.LayerBrushes
{
/// <summary>
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="RgbNetLayerBrush{T}" /> or instead
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead
/// </summary>
public abstract class BaseLayerBrush : CorePropertyChanged, IDisposable
{
@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Artemis.Core.LayerBrushes
{
/// <summary>
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="RgbNetLayerBrush{T}" /> or instead
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead
/// </summary>
public abstract class PropertiesLayerBrush<T> : BaseLayerBrush where T : LayerPropertyGroup
{
@@ -1,104 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Services;
using Ninject;
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.Core.LayerBrushes
{
/// <summary>
/// An RGB.NET brush that uses RGB.NET's per-LED rendering engine.
/// <para>Note: This brush type always renders on top of regular brushes</para>
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class RgbNetLayerBrush<T> : PropertiesLayerBrush<T> where T : LayerPropertyGroup
{
/// <summary>
/// Creates a new instance of the <see cref="RgbNetLayerBrush{T}" /> class
/// </summary>
protected RgbNetLayerBrush()
{
BrushType = LayerBrushType.RgbNet;
SupportsTransformation = false;
}
/// <summary>
/// The LED group this layer effect is applied to
/// </summary>
public ListLedGroup? LedGroup { get; internal set; }
/// <summary>
/// For internal use only, is public for dependency injection but ignore pl0x
/// </summary>
[Inject]
public IRgbService? RgbService { get; set; }
/// <summary>
/// Called when Artemis needs an instance of the RGB.NET effect you are implementing
/// </summary>
/// <returns>Your RGB.NET effect</returns>
public abstract IBrush GetBrush();
#region IDisposable
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (RgbService == null)
throw new ArtemisCoreException("Cannot dispose RGB.NET layer brush because RgbService is not set");
Layer.RenderPropertiesUpdated -= LayerOnRenderPropertiesUpdated;
LedGroup?.Detach();
LedGroup = null;
}
base.Dispose(disposing);
}
#endregion
internal void UpdateLedGroup()
{
if (LedGroup == null)
return;
if (Layer.Parent != null)
LedGroup.ZIndex = Layer.Parent.Children.Count - Layer.Parent.Children.IndexOf(Layer);
else
LedGroup.ZIndex = 1;
List<Led> missingLeds = Layer.Leds.Where(l => !LedGroup.ContainsLed(l.RgbLed)).Select(l => l.RgbLed).ToList();
List<Led> extraLeds = LedGroup.Where(l => Layer.Leds.All(layerLed => layerLed.RgbLed != l)).ToList();
LedGroup.AddLeds(missingLeds);
LedGroup.RemoveLeds(extraLeds);
LedGroup.Brush = GetBrush();
}
internal override void Initialize()
{
if (RgbService == null)
throw new ArtemisCoreException("Cannot initialize RGB.NET layer brush because RgbService is not set");
LedGroup = new ListLedGroup(RgbService.Surface);
Layer.RenderPropertiesUpdated += LayerOnRenderPropertiesUpdated;
InitializeProperties();
UpdateLedGroup();
}
// Not used in this effect type
internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
throw new NotImplementedException("RGB.NET layer effects do not implement InternalRender");
}
private void LayerOnRenderPropertiesUpdated(object? sender, EventArgs e)
{
UpdateLedGroup();
}
}
}
@@ -7,6 +7,7 @@ using System.Reflection;
using Artemis.Core.Modules;
using Humanizer;
using Newtonsoft.Json;
using Module = Artemis.Core.Modules.Module;
namespace Artemis.Core.DataModelExpansions
{
@@ -23,16 +24,16 @@ namespace Artemis.Core.DataModelExpansions
protected DataModel()
{
// These are both set right after construction to keep the constructor of inherited classes clean
Feature = null!;
Module = null!;
DataModelDescription = null!;
}
/// <summary>
/// Gets the plugin feature this data model belongs to
/// Gets the module this data model belongs to
/// </summary>
[JsonIgnore]
[DataModelIgnore]
public DataModelPluginFeature Feature { get; internal set; }
public Module Module { get; internal set; }
/// <summary>
/// Gets the <see cref="DataModelPropertyAttribute" /> describing this data model
@@ -59,11 +60,9 @@ namespace Artemis.Core.DataModelExpansions
/// <returns></returns>
public ReadOnlyCollection<PropertyInfo> GetHiddenProperties()
{
if (Feature is ProfileModule profileModule)
return profileModule.HiddenProperties;
if (Feature is BaseDataModelExpansion dataModelExpansion)
return dataModelExpansion.HiddenProperties;
if (Module is Module module)
return module.HiddenProperties;
return new List<PropertyInfo>().AsReadOnly();
}
@@ -149,7 +148,7 @@ namespace Artemis.Core.DataModelExpansions
attribute.Name ??= key.Humanize();
if (initialValue is DataModel dynamicDataModel)
{
dynamicDataModel.Feature = Feature;
dynamicDataModel.Module = Module;
dynamicDataModel.DataModelDescription = attribute;
}
+113 -90
View File
@@ -1,21 +1,22 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Artemis.Core.DataModelExpansions;
using Artemis.Storage.Entities.Module;
using SkiaSharp;
namespace Artemis.Core.Modules
{
/// <summary>
/// Allows you to add support for new games/applications while utilizing your own data model
/// Allows you to add new data to the Artemis data model
/// </summary>
public abstract class Module<T> : Module where T : DataModel
{
/// <summary>
/// The data model driving this module
/// <para>Note: This default data model is automatically registered upon plugin enable</para>
/// <para>Note: This default data model is automatically registered and instantiated upon plugin enable</para>
/// </summary>
public T DataModel
{
@@ -24,43 +25,61 @@ namespace Artemis.Core.Modules
}
/// <summary>
/// Gets or sets whether this module must also expand the main data model
/// <para>
/// Note: If expanding the main data model is all you want your plugin to do, create a
/// <see cref="DataModelExpansion{T}" /> plugin instead.
/// </para>
/// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
public bool ExpandsDataModel
/// <param name="propertyLambda">A lambda expression pointing to the property to ignore</param>
public void HideProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
get => InternalExpandsMainDataModel;
set => InternalExpandsMainDataModel = value;
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo)))
HiddenPropertiesList.Add(propertyInfo);
}
/// <summary>
/// Override to provide your own data model description. By default this returns a description matching your plugin
/// name and description
/// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm =>
/// dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <returns></returns>
public virtual DataModelPropertyAttribute GetDataModelDescription()
/// <param name="propertyLambda">A lambda expression pointing to the property to stop ignoring</param>
public void ShowProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description};
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo));
}
internal override void InternalEnable()
{
DataModel = Activator.CreateInstance<T>();
DataModel.Feature = this;
DataModel.Module = this;
DataModel.DataModelDescription = GetDataModelDescription();
base.InternalEnable();
}
internal override void InternalDisable()
{
Deactivate(true);
base.InternalDisable();
}
}
/// <summary>
/// Allows you to add support for new games/applications
/// For internal use only, please use <see cref="Module{T}" />.
/// </summary>
public abstract class Module : DataModelPluginFeature
public abstract class Module : PluginFeature
{
private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new();
private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new();
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
protected internal readonly List<PropertyInfo> HiddenPropertiesList = new();
/// <summary>
/// Gets a read only collection of default profile paths
/// </summary>
public IReadOnlyCollection<(DefaultCategoryName, string)> DefaultProfilePaths => _defaultProfilePaths.AsReadOnly();
/// <summary>
/// The modules display name that's shown in the menu
/// </summary>
@@ -107,35 +126,23 @@ namespace Artemis.Core.Modules
public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any;
/// <summary>
/// Gets or sets the default priority category for this module, defaults to
/// <see cref="ModulePriorityCategory.Normal" />
/// Gets or sets a boolean indicating whether this module is always available to profiles or only when profiles
/// specifically target this module.
/// <para>Note: If set to <see langword="true" />, <see cref="ActivationRequirements" /> are not evaluated.</para>
/// </summary>
public ModulePriorityCategory DefaultPriorityCategory { get; set; } = ModulePriorityCategory.Normal;
/// <summary>
/// Gets the current priority category of this module
/// </summary>
public ModulePriorityCategory PriorityCategory { get; internal set; }
/// <summary>
/// Gets the current priority of this module within its priority category
/// </summary>
public int Priority { get; internal set; }
/// <summary>
/// A list of custom module tabs that show in the UI
/// </summary>
public IEnumerable<ModuleTab>? ModuleTabs { get; protected set; }
public bool IsAlwaysAvailable { get; set; }
/// <summary>
/// Gets whether updating this module is currently allowed
/// </summary>
public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride);
internal DataModel? InternalDataModel { get; set; }
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
public ReadOnlyCollection<PropertyInfo> HiddenProperties => HiddenPropertiesList.AsReadOnly();
internal bool InternalExpandsMainDataModel { get; set; }
internal ModuleSettingsEntity? SettingsEntity { get; set; }
internal DataModel? InternalDataModel { get; set; }
/// <summary>
/// Called each frame when the module should update
@@ -143,14 +150,6 @@ namespace Artemis.Core.Modules
/// <param name="deltaTime">Time in seconds since the last update</param>
public abstract void Update(double deltaTime);
/// <summary>
/// Called each frame when the module should render
/// </summary>
/// <param name="deltaTime">Time since the last render</param>
/// <param name="canvas"></param>
/// <param name="canvasInfo"></param>
public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo);
/// <summary>
/// Called when the <see cref="ActivationRequirements" /> are met or during an override
/// </summary>
@@ -158,7 +157,9 @@ namespace Artemis.Core.Modules
/// If true, the activation was due to an override. This usually means the module was activated
/// by the profile editor
/// </param>
public abstract void ModuleActivated(bool isOverride);
public virtual void ModuleActivated(bool isOverride)
{
}
/// <summary>
/// Called when the <see cref="ActivationRequirements" /> are no longer met or during an override
@@ -167,7 +168,9 @@ namespace Artemis.Core.Modules
/// If true, the deactivation was due to an override. This usually means the module was deactivated
/// by the profile editor
/// </param>
public abstract void ModuleDeactivated(bool isOverride);
public virtual void ModuleDeactivated(bool isOverride)
{
}
/// <summary>
/// Evaluates the activation requirements following the <see cref="ActivationRequirementMode" /> and returns the result
@@ -185,6 +188,50 @@ namespace Artemis.Core.Modules
return false;
}
/// <summary>
/// Override to provide your own data model description. By default this returns a description matching your plugin
/// name and description
/// </summary>
/// <returns></returns>
public virtual DataModelPropertyAttribute GetDataModelDescription()
{
return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description};
}
/// <summary>
/// Adds a default profile by reading it from the file found at the provided path
/// </summary>
/// <param name="category">The category in which to place the default profile</param>
/// <param name="file">A path pointing towards a profile file. May be relative to the plugin directory.</param>
/// <returns>
/// <see langword="true" /> if the default profile was added; <see langword="false" /> if it was not because it is
/// already in the list.
/// </returns>
protected bool AddDefaultProfile(DefaultCategoryName category, string file)
{
// It can be null if the plugin has not loaded yet in which case Plugin.ResolveRelativePath fails
if (Plugin == null!)
{
if (_pendingDefaultProfilePaths.Contains((category, file)))
return false;
_pendingDefaultProfilePaths.Add((category, file));
return true;
}
if (!Path.IsPathRooted(file))
file = Plugin.ResolveRelativePath(file);
// Ensure the file exists
if (!File.Exists(file))
throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}.");
if (_defaultProfilePaths.Contains((category, file)))
return false;
_defaultProfilePaths.Add((category, file));
return true;
}
internal virtual void InternalUpdate(double deltaTime)
{
StartUpdateMeasure();
@@ -193,13 +240,6 @@ namespace Artemis.Core.Modules
StopUpdateMeasure();
}
internal virtual void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
StartRenderMeasure();
Render(deltaTime, canvas, canvasInfo);
StopRenderMeasure();
}
internal virtual void Activate(bool isOverride)
{
if (IsActivated)
@@ -220,6 +260,20 @@ namespace Artemis.Core.Modules
ModuleDeactivated(isOverride);
}
#region Overrides of PluginFeature
/// <inheritdoc />
internal override void InternalEnable()
{
foreach ((DefaultCategoryName categoryName, var path) in _pendingDefaultProfilePaths)
AddDefaultProfile(categoryName, path);
_pendingDefaultProfilePaths.Clear();
base.InternalEnable();
}
#endregion
internal virtual void Reactivate(bool isDeactivateOverride, bool isActivateOverride)
{
if (!IsActivated)
@@ -228,16 +282,6 @@ namespace Artemis.Core.Modules
Deactivate(isDeactivateOverride);
Activate(isActivateOverride);
}
internal void ApplyToEntity()
{
if (SettingsEntity == null)
SettingsEntity = new ModuleSettingsEntity();
SettingsEntity.ModuleId = Id;
SettingsEntity.PriorityCategory = (int) PriorityCategory;
SettingsEntity.Priority = Priority;
}
}
/// <summary>
@@ -255,25 +299,4 @@ namespace Artemis.Core.Modules
/// </summary>
All
}
/// <summary>
/// Describes the priority category of a module
/// </summary>
public enum ModulePriorityCategory
{
/// <summary>
/// Indicates a normal render priority
/// </summary>
Normal,
/// <summary>
/// Indicates that the module renders for a specific application/game, rendering on top of normal modules
/// </summary>
Application,
/// <summary>
/// Indicates that the module renders an overlay, always rendering on top
/// </summary>
Overlay
}
}
@@ -1,44 +0,0 @@
using System;
namespace Artemis.Core.Modules
{
/// <inheritdoc />
public class ModuleTab<T> : ModuleTab where T : IModuleViewModel
{
/// <summary>
/// Creates a new instance of the <see cref="ModuleTab{T}" /> class
/// </summary>
/// <param name="title">The title of the tab</param>
public ModuleTab(string title) : base(title)
{
}
/// <inheritdoc />
public override Type Type => typeof(T);
}
/// <summary>
/// Describes a UI tab for a specific module
/// </summary>
public abstract class ModuleTab
{
/// <summary>
/// Creates a new instance of the <see cref="ModuleTab" /> class
/// </summary>
/// <param name="title">The title of the tab</param>
protected ModuleTab(string title)
{
Title = title;
}
/// <summary>
/// The title of the tab
/// </summary>
public string Title { get; protected set; }
/// <summary>
/// The type of view model the tab contains
/// </summary>
public abstract Type Type { get; }
}
}
@@ -1,329 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Artemis.Core.DataModelExpansions;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
using SkiaSharp;
namespace Artemis.Core.Modules
{
/// <summary>
/// Allows you to add support for new games/applications while utilizing Artemis' profile engine and your own data
/// model
/// </summary>
public abstract class ProfileModule<T> : ProfileModule where T : DataModel
{
/// <summary>
/// The data model driving this module
/// <para>Note: This default data model is automatically registered upon plugin enable</para>
/// </summary>
public T DataModel
{
get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model");
internal set => InternalDataModel = value;
}
/// <summary>
/// Gets or sets whether this module must also expand the main data model
/// <para>
/// Note: If expanding the main data model is all you want your plugin to do, create a
/// <see cref="BaseDataModelExpansion" /> plugin instead.
/// </para>
/// </summary>
public bool ExpandsDataModel
{
get => InternalExpandsMainDataModel;
set => InternalExpandsMainDataModel = value;
}
/// <summary>
/// Override to provide your own data model description. By default this returns a description matching your plugin
/// name and description
/// </summary>
/// <returns></returns>
public virtual DataModelPropertyAttribute GetDataModelDescription()
{
return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description};
}
/// <summary>
/// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <param name="propertyLambda">A lambda expression pointing to the property to ignore</param>
public void HideProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo)))
HiddenPropertiesList.Add(propertyInfo);
}
/// <summary>
/// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm =>
/// dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <param name="propertyLambda">A lambda expression pointing to the property to stop ignoring</param>
public void ShowProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo));
}
internal override void InternalEnable()
{
DataModel = Activator.CreateInstance<T>();
DataModel.Feature = this;
DataModel.DataModelDescription = GetDataModelDescription();
base.InternalEnable();
}
internal override void InternalDisable()
{
Deactivate(true);
base.InternalDisable();
}
}
/// <summary>
/// Allows you to add support for new games/applications while utilizing Artemis' profile engine
/// </summary>
public abstract class ProfileModule : Module
{
private readonly List<string> _defaultProfilePaths = new();
private readonly List<string> _pendingDefaultProfilePaths = new();
private readonly List<ProfileEntity> _defaultProfiles = new();
private readonly object _lock = new();
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
protected internal readonly List<PropertyInfo> HiddenPropertiesList = new();
/// <summary>
/// Creates a new instance of the <see cref="ProfileModule" /> class
/// </summary>
protected ProfileModule()
{
OpacityOverride = 1;
}
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
public ReadOnlyCollection<PropertyInfo> HiddenProperties => HiddenPropertiesList.AsReadOnly();
/// <summary>
/// Gets the currently active profile
/// </summary>
public Profile? ActiveProfile { get; private set; }
/// <summary>
/// Disables updating the profile, rendering does continue
/// </summary>
public bool IsProfileUpdatingDisabled { get; set; }
/// <summary>
/// Overrides the opacity of the root folder
/// </summary>
public double OpacityOverride { get; set; }
/// <summary>
/// Indicates whether or not a profile change is being animated
/// </summary>
public bool AnimatingProfileChange { get; private set; }
/// <summary>
/// Gets a list of default profiles, to add a new default profile use <see cref="AddDefaultProfile" />
/// </summary>
internal ReadOnlyCollection<ProfileEntity> DefaultProfiles => _defaultProfiles.AsReadOnly();
/// <summary>
/// Called after the profile has updated
/// </summary>
/// <param name="deltaTime">Time in seconds since the last update</param>
public virtual void ProfileUpdated(double deltaTime)
{
}
/// <summary>
/// Called after the profile has rendered
/// </summary>
/// <param name="deltaTime">Time since the last render</param>
/// <param name="canvas"></param>
/// <param name="canvasInfo"></param>
public virtual void ProfileRendered(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
}
/// <summary>
/// Occurs when the <see cref="ActiveProfile" /> has changed
/// </summary>
public event EventHandler? ActiveProfileChanged;
/// <summary>
/// Adds a default profile by reading it from the file found at the provided path
/// </summary>
/// <param name="file">A path pointing towards a profile file. May be relative to the plugin directory.</param>
/// <returns>
/// <see langword="true" /> if the default profile was added; <see langword="false" /> if it was not because it is
/// already in the list.
/// </returns>
protected bool AddDefaultProfile(string file)
{
// It can be null if the plugin has not loaded yet...
if (Plugin == null!)
{
if (_pendingDefaultProfilePaths.Contains(file))
return false;
_pendingDefaultProfilePaths.Add(file);
return true;
}
if (!Path.IsPathRooted(file))
file = Plugin.ResolveRelativePath(file);
if (_defaultProfilePaths.Contains(file))
return false;
_defaultProfilePaths.Add(file);
// Ensure the file exists
if (!File.Exists(file))
throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}.");
// Deserialize and make sure that succeeded
ProfileEntity? profileEntity = JsonConvert.DeserializeObject<ProfileEntity>(File.ReadAllText(file), ProfileService.ExportSettings);
if (profileEntity == null)
throw new ArtemisPluginFeatureException(this, $"Failed to deserialize default profile at {file}.");
// Ensure the profile ID is unique
if (_defaultProfiles.Any(d => d.Id == profileEntity.Id))
throw new ArtemisPluginFeatureException(this, $"Cannot add default profile from {file}, profile ID {profileEntity.Id} already in use.");
profileEntity.IsFreshImport = true;
profileEntity.IsActive = false;
_defaultProfiles.Add(profileEntity);
return true;
}
/// <summary>
/// Invokes the <see cref="ActiveProfileChanged" /> event
/// </summary>
protected virtual void OnActiveProfileChanged()
{
ActiveProfileChanged?.Invoke(this, EventArgs.Empty);
}
internal override void InternalEnable()
{
foreach (string pendingDefaultProfile in _pendingDefaultProfilePaths)
AddDefaultProfile(pendingDefaultProfile);
_pendingDefaultProfilePaths.Clear();
base.InternalEnable();
}
internal override void InternalUpdate(double deltaTime)
{
StartUpdateMeasure();
if (IsUpdateAllowed)
Update(deltaTime);
lock (_lock)
{
OpacityOverride = AnimatingProfileChange
? Math.Max(0, OpacityOverride - 0.1)
: Math.Min(1, OpacityOverride + 0.1);
// Update the profile
if (!IsProfileUpdatingDisabled)
ActiveProfile?.Update(deltaTime);
}
ProfileUpdated(deltaTime);
StopUpdateMeasure();
}
internal override void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
StartRenderMeasure();
Render(deltaTime, canvas, canvasInfo);
lock (_lock)
{
// Render the profile
ActiveProfile?.Render(canvas, SKPointI.Empty);
}
ProfileRendered(deltaTime, canvas, canvasInfo);
StopRenderMeasure();
}
internal async Task ChangeActiveProfileAnimated(Profile? profile, IEnumerable<ArtemisDevice> devices)
{
if (profile != null && profile.Module != this)
throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {this}.");
if (!IsActivated)
throw new ArtemisCoreException("Cannot activate a profile on a deactivated module");
if (profile == ActiveProfile || AnimatingProfileChange)
return;
AnimatingProfileChange = true;
while (OpacityOverride > 0)
await Task.Delay(50);
ChangeActiveProfile(profile, devices);
AnimatingProfileChange = false;
while (OpacityOverride < 1)
await Task.Delay(50);
}
internal void ChangeActiveProfile(Profile? profile, IEnumerable<ArtemisDevice> devices)
{
if (profile != null && profile.Module != this)
throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {this}.");
if (!IsActivated)
throw new ArtemisCoreException("Cannot activate a profile on a deactivated module");
lock (_lock)
{
if (profile == ActiveProfile)
return;
ActiveProfile?.Dispose();
ActiveProfile = profile;
ActiveProfile?.Activate(devices);
}
OnActiveProfileChanged();
}
internal override void Deactivate(bool isOverride)
{
base.Deactivate(isOverride);
Profile? profile = ActiveProfile;
ActiveProfile = null;
profile?.Dispose();
}
internal override void Reactivate(bool isDeactivateOverride, bool isActivateOverride)
{
if (!IsActivated)
return;
// Avoid disposing the profile
base.Deactivate(isDeactivateOverride);
Activate(isActivateOverride);
}
}
}
@@ -35,14 +35,10 @@ namespace Artemis.Core
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
if (Icon != null) return;
if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType))
Icon = "TableAdd";
else if (typeof(DeviceProvider).IsAssignableFrom(featureType))
if (typeof(DeviceProvider).IsAssignableFrom(featureType))
Icon = "Devices";
else if (typeof(ProfileModule).IsAssignableFrom(featureType))
Icon = "VectorRectangle";
else if (typeof(Module).IsAssignableFrom(featureType))
Icon = "GearBox";
Icon = "VectorRectangle";
else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType))
Icon = "Brush";
else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType))
@@ -66,10 +62,8 @@ namespace Artemis.Core
if (Icon != null) return;
Icon = Instance switch
{
BaseDataModelExpansion => "TableAdd",
DeviceProvider => "Devices",
ProfileModule => "VectorRectangle",
Module => "GearBox",
Module => "VectorRectangle",
LayerBrushProvider => "Brush",
LayerEffectProvider => "AutoAwesome",
_ => "Plugin"
+10 -90
View File
@@ -29,11 +29,10 @@ namespace Artemis.Core.Services
private readonly PluginSetting<LogEventLevel> _loggingLevel;
private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService;
private readonly IModuleService _moduleService;
private readonly IRgbService _rgbService;
private readonly List<Exception> _updateExceptions = new();
private List<BaseDataModelExpansion> _dataModelExpansions = new();
private DateTime _lastExceptionLog;
private List<Module> _modules = new();
// ReSharper disable UnusedParameter.Local
public CoreService(IKernel kernel,
@@ -43,8 +42,7 @@ namespace Artemis.Core.Services
IPluginManagementService pluginManagementService,
IRgbService rgbService,
IProfileService profileService,
IModuleService moduleService // injected to ensure module priorities get applied
)
IModuleService moduleService)
{
Kernel = kernel;
Constants.CorePlugin.Kernel = kernel;
@@ -53,18 +51,14 @@ namespace Artemis.Core.Services
_pluginManagementService = pluginManagementService;
_rgbService = rgbService;
_profileService = profileService;
_moduleService = moduleService;
_loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug);
_frameStopWatch = new Stopwatch();
StartupArguments = new List<string>();
UpdatePluginCache();
_rgbService.IsRenderPaused = true;
_rgbService.Surface.Updating += SurfaceOnUpdating;
_loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel();
_pluginManagementService.PluginFeatureEnabled += (sender, args) => UpdatePluginCache();
_pluginManagementService.PluginFeatureDisabled += (sender, args) => UpdatePluginCache();
}
// ReSharper restore UnusedParameter.Local
@@ -79,12 +73,6 @@ namespace Artemis.Core.Services
FrameRendered?.Invoke(this, e);
}
private void UpdatePluginCache()
{
_modules = _pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled).ToList();
_dataModelExpansions = _pluginManagementService.GetFeaturesOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled).ToList();
}
private void ApplyLoggingLevel()
{
string? argument = StartupArguments.FirstOrDefault(a => a.StartsWith("--logging"));
@@ -119,68 +107,19 @@ namespace Artemis.Core.Services
try
{
_frameStopWatch.Restart();
// Render all active modules
_moduleService.UpdateActiveModules(args.DeltaTime);
SKTexture texture = _rgbService.OpenRender();
lock (_dataModelExpansions)
{
// Update all active modules, check Enabled status because it may go false before before the _dataModelExpansions list is updated
foreach (BaseDataModelExpansion dataModelExpansion in _dataModelExpansions.Where(e => e.IsEnabled))
{
try
{
dataModelExpansion.InternalUpdate(args.DeltaTime);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
}
List<Module> modules;
lock (_modules)
{
modules = _modules.Where(m => m.IsActivated || m.InternalExpandsMainDataModel)
.OrderBy(m => m.PriorityCategory)
.ThenByDescending(m => m.Priority)
.ToList();
}
// Update all active modules
foreach (Module module in modules)
{
try
{
module.InternalUpdate(args.DeltaTime);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
SKCanvas canvas = texture.Surface.Canvas;
canvas.Save();
if (Math.Abs(texture.RenderScale - 1) > 0.001)
canvas.Scale(texture.RenderScale);
canvas.Clear(new SKColor(0, 0, 0));
// While non-activated modules may be updated above if they expand the main data model, they may never render
if (!ModuleRenderingDisabled)
if (!ProfileRenderingDisabled)
{
foreach (Module module in modules.Where(m => m.IsActivated))
{
try
{
module.InternalRender(args.DeltaTime, canvas, texture.ImageInfo);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
_profileService.UpdateProfiles(args.DeltaTime);
_profileService.RenderProfiles(canvas);
}
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
@@ -228,7 +167,7 @@ namespace Artemis.Core.Services
}
public TimeSpan FrameTime { get; private set; }
public bool ModuleRenderingDisabled { get; set; }
public bool ProfileRenderingDisabled { get; set; }
public List<string> StartupArguments { get; set; }
public bool IsElevated { get; set; }
@@ -272,25 +211,6 @@ namespace Artemis.Core.Services
OnInitialized();
}
public void PlayIntroAnimation()
{
IntroAnimation intro = new(_logger, _profileService, _rgbService.EnabledDevices);
// Draw a white overlay over the device
void DrawOverlay(object? sender, FrameRenderingEventArgs args)
{
if (intro.AnimationProfile.GetAllLayers().All(l => l.Timeline.IsFinished))
{
FrameRendering -= DrawOverlay;
intro.AnimationProfile.Dispose();
}
intro.Render(args.DeltaTime, args.Canvas);
}
FrameRendering += DrawOverlay;
}
public event EventHandler? Initialized;
public event EventHandler<FrameRenderingEventArgs>? FrameRendering;
public event EventHandler<FrameRenderedEventArgs>? FrameRendered;
@@ -19,9 +19,9 @@ namespace Artemis.Core.Services
TimeSpan FrameTime { get; }
/// <summary>
/// Gets or sets whether modules are rendered each frame by calling their Render method
/// Gets or sets whether profiles are rendered each frame by calling their Render method
/// </summary>
bool ModuleRenderingDisabled { get; set; }
bool ProfileRenderingDisabled { get; set; }
/// <summary>
/// Gets or sets a list of startup arguments
@@ -38,11 +38,6 @@ namespace Artemis.Core.Services
/// </summary>
void Initialize();
/// <summary>
/// Plays the into animation profile defined in <c>Resources/intro-profile.json</c>
/// </summary>
void PlayIntroAnimation();
/// <summary>
/// Occurs the core has finished initializing
/// </summary>
@@ -1,5 +1,5 @@
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using Artemis.Core.Modules;
namespace Artemis.Core.Services
@@ -10,33 +10,29 @@ namespace Artemis.Core.Services
public interface IModuleService : IArtemisService
{
/// <summary>
/// Gets the current active module override. If set, all other modules are deactivated and only the
/// <see cref="ActiveModuleOverride" /> is active.
/// Updates all currently active modules
/// </summary>
Module? ActiveModuleOverride { get; }
/// <summary>
/// Changes the current <see cref="ActiveModuleOverride" /> and deactivates all other modules
/// </summary>
/// <param name="overrideModule"></param>
Task SetActiveModuleOverride(Module? overrideModule);
/// <param name="deltaTime"></param>
void UpdateActiveModules(double deltaTime);
/// <summary>
/// Evaluates every enabled module's activation requirements and activates/deactivates modules accordingly
/// </summary>
Task UpdateModuleActivation();
void UpdateModuleActivation();
/// <summary>
/// Updates the priority and priority category of the given module
/// Overrides activation on the provided module and restores regular activation to any remaining modules
/// </summary>
/// <param name="module">The module to update</param>
/// <param name="category">The new priority category of the module</param>
/// <param name="priority">The new priority of the module</param>
void UpdateModulePriority(Module module, ModulePriorityCategory category, int priority);
void SetActivationOverride(Module? module);
/// <summary>
/// Occurs when the priority of a module is updated.
/// Occurs whenever a module is activated
/// </summary>
event EventHandler? ModulePriorityUpdated;
event EventHandler<ModuleEventArgs> ModuleActivated;
/// <summary>
/// Occurs whenever a module is deactivated
/// </summary>
event EventHandler<ModuleEventArgs> ModuleDeactivated;
}
}
+104 -196
View File
@@ -1,103 +1,48 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Repositories.Interfaces;
using Newtonsoft.Json;
using Serilog;
using Timer = System.Timers.Timer;
namespace Artemis.Core.Services
{
internal class ModuleService : IModuleService
{
private static readonly SemaphoreSlim ActiveModuleSemaphore = new(1, 1);
private readonly Timer _activationUpdateTimer;
private readonly ILogger _logger;
private readonly IModuleRepository _moduleRepository;
private readonly IProfileRepository _profileRepository;
private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService;
private readonly List<Module> _modules;
private readonly object _updateLock = new();
public ModuleService(ILogger logger, IModuleRepository moduleRepository, IProfileRepository profileRepository, IPluginManagementService pluginManagementService, IProfileService profileService)
private Module? _activationOverride;
public ModuleService(ILogger logger, IPluginManagementService pluginManagementService, IProfileService profileService)
{
_logger = logger;
_moduleRepository = moduleRepository;
_profileRepository = profileRepository;
_pluginManagementService = pluginManagementService;
_profileService = profileService;
_pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled;
Timer activationUpdateTimer = new(2000);
activationUpdateTimer.Start();
activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed;
_activationUpdateTimer = new Timer(2000);
_activationUpdateTimer.Start();
_activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed;
foreach (Module module in _pluginManagementService.GetFeaturesOfType<Module>())
InitialiseOrApplyPriority(module);
pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureEnabled;
pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled;
_modules = pluginManagementService.GetFeaturesOfType<Module>().ToList();
foreach (Module module in _modules)
ImportDefaultProfiles(module);
}
private async void ActivationUpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
protected virtual void OnModuleActivated(ModuleEventArgs e)
{
await UpdateModuleActivation();
ModuleActivated?.Invoke(this, e);
}
private async Task ActivateModule(Module module)
protected virtual void OnModuleDeactivated(ModuleEventArgs e)
{
try
{
ProfileModule? profileModule = module as ProfileModule;
if (profileModule != null && profileModule.DefaultProfiles.Any())
{
List<ProfileDescriptor> descriptors = _profileService.GetProfileDescriptors(profileModule);
foreach (ProfileEntity defaultProfile in profileModule.DefaultProfiles)
{
if (descriptors.All(d => d.Id != defaultProfile.Id))
_profileRepository.Add(defaultProfile);
}
}
module.Activate(false);
try
{
// If this is a profile module, activate the last active profile after module activation
if (profileModule != null)
await _profileService.ActivateLastProfileAnimated(profileModule);
}
catch (Exception e)
{
_logger.Warning(e, $"Failed to activate last profile on module {module}");
}
}
catch (Exception e)
{
_logger.Error(new ArtemisPluginFeatureException(module, "Failed to activate module.", e), "Failed to activate module");
throw;
}
}
private async Task DeactivateModule(Module module)
{
try
{
// If this is a profile module, animate profile disable
// module.Deactivate would do the same but without animation
if (module.IsActivated && module is ProfileModule profileModule)
await profileModule.ChangeActiveProfileAnimated(null, Enumerable.Empty<ArtemisDevice>());
module.Deactivate(false);
}
catch (Exception e)
{
_logger.Error(new ArtemisPluginFeatureException(
module, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile"
);
throw;
}
ModuleDeactivated?.Invoke(this, e);
}
private void OverrideActivate(Module module)
@@ -110,21 +55,15 @@ namespace Artemis.Core.Services
// If activating while it should be deactivated, its an override
bool shouldBeActivated = module.EvaluateActivationRequirements();
module.Activate(!shouldBeActivated);
// If this is a profile module, activate the last active profile after module activation
if (module is ProfileModule profileModule)
_profileService.ActivateLastProfile(profileModule);
}
catch (Exception e)
{
_logger.Error(new ArtemisPluginFeatureException(
module, "Failed to activate module and last profile.", e), "Failed to activate module and last profile"
);
_logger.Error(new ArtemisPluginFeatureException(module, "Failed to activate module.", e), "Failed to activate module");
throw;
}
}
private void OverrideDeactivate(Module module, bool clearingOverride)
private void OverrideDeactivate(Module module)
{
try
{
@@ -134,163 +73,132 @@ namespace Artemis.Core.Services
// If deactivating while it should be activated, its an override
bool shouldBeActivated = module.EvaluateActivationRequirements();
// No need to deactivate if it is not in an overridden state
if (shouldBeActivated && !module.IsActivatedOverride && !clearingOverride)
if (shouldBeActivated && !module.IsActivatedOverride)
return;
module.Deactivate(true);
}
catch (Exception e)
{
_logger.Error(new ArtemisPluginFeatureException(
module, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile"
);
_logger.Error(new ArtemisPluginFeatureException(module, "Failed to deactivate module.", e), "Failed to deactivate module");
throw;
}
}
private void OnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e)
private void ActivationUpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
{
if (e.PluginFeature is Module module)
InitialiseOrApplyPriority(module);
UpdateModuleActivation();
}
private void InitialiseOrApplyPriority(Module module)
private void PluginManagementServiceOnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e)
{
ModulePriorityCategory category = module.DefaultPriorityCategory;
int priority = 1;
module.SettingsEntity = _moduleRepository.GetByModuleId(module.Id);
if (module.SettingsEntity != null)
lock (_updateLock)
{
category = (ModulePriorityCategory) module.SettingsEntity.PriorityCategory;
priority = module.SettingsEntity.Priority;
if (e.PluginFeature is Module module && !_modules.Contains(module))
{
ImportDefaultProfiles(module);
_modules.Add(module);
}
}
UpdateModulePriority(module, category, priority);
}
public Module? ActiveModuleOverride { get; private set; }
private void PluginManagementServiceOnPluginFeatureDisabled(object? sender, PluginFeatureEventArgs e)
{
lock (_updateLock)
{
if (e.PluginFeature is Module module)
_modules.Remove(module);
}
}
public async Task SetActiveModuleOverride(Module? overrideModule)
private void ImportDefaultProfiles(Module module)
{
try
{
await ActiveModuleSemaphore.WaitAsync();
if (ActiveModuleOverride == overrideModule)
return;
if (overrideModule != null)
List<ProfileConfiguration> profileConfigurations = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).ToList();
foreach ((DefaultCategoryName categoryName, string profilePath) in module.DefaultProfilePaths)
{
OverrideActivate(overrideModule);
_logger.Information($"Setting active module override to {overrideModule.DisplayName}");
}
else
{
_logger.Information("Clearing active module override");
}
ProfileConfigurationExportModel? profileConfigurationExportModel = JsonConvert.DeserializeObject<ProfileConfigurationExportModel>(File.ReadAllText(profilePath), IProfileService.ExportSettings);
if (profileConfigurationExportModel?.ProfileEntity == null)
throw new ArtemisCoreException($"Default profile at path {profilePath} contains no valid profile data");
if (profileConfigurations.Any(p => p.Entity.ProfileId == profileConfigurationExportModel.ProfileEntity.Id))
continue;
// Always deactivate all other modules whenever override is called
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList();
foreach (Module module in modules.Where(m => m != overrideModule))
OverrideDeactivate(module, overrideModule != null);
ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == categoryName.ToString()) ??
_profileService.CreateProfileCategory(categoryName.ToString());
ActiveModuleOverride = overrideModule;
_profileService.ImportProfile(category, profileConfigurationExportModel, false, true, null);
}
}
finally
catch (Exception e)
{
ActiveModuleSemaphore.Release();
_logger.Warning(e, "Failed to import default profiles for module {module}", module);
}
}
public async Task UpdateModuleActivation()
public void UpdateModuleActivation()
{
if (ActiveModuleSemaphore.CurrentCount == 0)
return;
try
lock (_updateLock)
{
await ActiveModuleSemaphore.WaitAsync();
if (ActiveModuleOverride != null)
try
{
// The conditions of the active module override may be matched, in that case reactivate as a non-override
// the principle is different for this service but not for the module
bool shouldBeActivated = ActiveModuleOverride.EvaluateActivationRequirements();
if (shouldBeActivated && ActiveModuleOverride.IsActivatedOverride)
ActiveModuleOverride.Reactivate(true, false);
else if (!shouldBeActivated && !ActiveModuleOverride.IsActivatedOverride) ActiveModuleOverride.Reactivate(false, true);
return;
}
Stopwatch stopwatch = new();
stopwatch.Start();
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList();
List<Task> tasks = new();
foreach (Module module in modules)
{
lock (module)
_activationUpdateTimer.Elapsed -= ActivationUpdateTimerOnElapsed;
foreach (Module module in _modules)
{
bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled;
if (module.IsActivatedOverride)
continue;
if (module.IsAlwaysAvailable)
{
module.Activate(false);
continue;
}
module.Profiler.StartMeasurement("EvaluateActivationRequirements");
bool shouldBeActivated = module.IsEnabled && module.EvaluateActivationRequirements();
module.Profiler.StopMeasurement("EvaluateActivationRequirements");
if (shouldBeActivated && !module.IsActivated)
tasks.Add(ActivateModule(module));
{
module.Activate(false);
OnModuleActivated(new ModuleEventArgs(module));
}
else if (!shouldBeActivated && module.IsActivated)
tasks.Add(DeactivateModule(module));
{
module.Deactivate(false);
OnModuleDeactivated(new ModuleEventArgs(module));
}
}
}
await Task.WhenAll(tasks);
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > 100 && !tasks.Any())
_logger.Warning("Activation requirements evaluation took too long: {moduleCount} module(s) in {elapsed}", modules.Count, stopwatch.Elapsed);
}
finally
{
ActiveModuleSemaphore.Release();
}
}
public void UpdateModulePriority(Module module, ModulePriorityCategory category, int priority)
{
if (module.PriorityCategory == category && module.Priority == priority)
return;
List<Module> modules = _pluginManagementService
.GetFeaturesOfType<Module>()
.Where(m => m.PriorityCategory == category)
.OrderBy(m => m.Priority)
.ToList();
if (modules.Contains(module))
modules.Remove(module);
priority = Math.Min(modules.Count, Math.Max(0, priority));
modules.Insert(priority, module);
module.PriorityCategory = category;
for (int index = 0; index < modules.Count; index++)
{
Module categoryModule = modules[index];
categoryModule.Priority = index;
// Don't save modules whose priority hasn't been initialized yet
if (categoryModule == module || categoryModule.SettingsEntity != null)
finally
{
categoryModule.ApplyToEntity();
_moduleRepository.Save(categoryModule.SettingsEntity);
_activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed;
}
}
ModulePriorityUpdated?.Invoke(this, EventArgs.Empty);
}
#region Events
public void SetActivationOverride(Module? module)
{
lock (_updateLock)
{
if (_activationOverride != null)
OverrideDeactivate(_activationOverride);
_activationOverride = module;
if (_activationOverride != null)
OverrideActivate(_activationOverride);
}
}
public event EventHandler? ModulePriorityUpdated;
public void UpdateActiveModules(double deltaTime)
{
lock (_updateLock)
{
foreach (Module module in _modules)
module.InternalUpdate(deltaTime);
}
}
#endregion
public event EventHandler<ModuleEventArgs>? ModuleActivated;
public event EventHandler<ModuleEventArgs>? ModuleDeactivated;
}
}
@@ -13,8 +13,6 @@ namespace Artemis.Core.Services
// Add data models of already loaded plugins
foreach (Module module in pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled))
AddModuleDataModel(module);
foreach (BaseDataModelExpansion dataModelExpansion in pluginManagementService.GetFeaturesOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled))
AddDataModelExpansionDataModel(dataModelExpansion);
// Add data models of new plugins when they get enabled
pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled;
@@ -54,8 +52,6 @@ namespace Artemis.Core.Services
{
if (e.PluginFeature is Module module)
AddModuleDataModel(module);
else if (e.PluginFeature is BaseDataModelExpansion dataModelExpansion)
AddDataModelExpansionDataModel(dataModelExpansion);
}
private void AddModuleDataModel(Module module)
@@ -66,19 +62,8 @@ namespace Artemis.Core.Services
if (module.InternalDataModel.DataModelDescription == null)
throw new ArtemisPluginFeatureException(module, "Module overrides GetDataModelDescription but returned null");
module.InternalDataModel.IsExpansion = module.InternalExpandsMainDataModel;
module.InternalDataModel.IsExpansion = module.IsAlwaysAvailable;
RegisterDataModel(module.InternalDataModel);
}
private void AddDataModelExpansionDataModel(BaseDataModelExpansion dataModelExpansion)
{
if (dataModelExpansion.InternalDataModel == null)
throw new ArtemisCoreException("Cannot add data model expansion that is not enabled");
if (dataModelExpansion.InternalDataModel.DataModelDescription == null)
throw new ArtemisPluginFeatureException(dataModelExpansion, "Data model expansion overrides GetDataModelDescription but returned null");
dataModelExpansion.InternalDataModel.IsExpansion = true;
RegisterDataModel(dataModelExpansion.InternalDataModel);
}
}
}
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Artemis.Core.Modules;
using System.Collections.ObjectModel;
using Newtonsoft.Json;
using SkiaSharp;
namespace Artemis.Core.Services
{
@@ -10,122 +10,144 @@ namespace Artemis.Core.Services
public interface IProfileService : IArtemisService
{
/// <summary>
/// Creates a new profile for the given module and returns a descriptor pointing to it
/// Gets the JSON serializer settings used to create profile mementos
/// </summary>
/// <param name="module">The profile module to create the profile for</param>
/// <param name="name">The name of the new profile</param>
/// <returns></returns>
ProfileDescriptor CreateProfileDescriptor(ProfileModule module, string name);
public static JsonSerializerSettings MementoSettings { get; } = new() {TypeNameHandling = TypeNameHandling.All};
/// <summary>
/// Gets a descriptor for each profile stored for the given <see cref="ProfileModule" />
/// Gets the JSON serializer settings used to import/export profiles
/// </summary>
/// <param name="module">The module to return profile descriptors for</param>
/// <returns></returns>
List<ProfileDescriptor> GetProfileDescriptors(ProfileModule module);
public static JsonSerializerSettings ExportSettings { get; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented};
/// <summary>
/// Gets a read only collection containing all the profile categories
/// </summary>
ReadOnlyCollection<ProfileCategory> ProfileCategories { get; }
/// <summary>
/// Gets a read only collection containing all the profile configurations
/// </summary>
ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
/// <summary>
/// Gets or sets a boolean indicating whether rendering should only be done for profiles being edited
/// </summary>
bool RenderForEditor { get; set; }
/// <summary>
/// Activates the profile of the given <see cref="ProfileConfiguration" /> with the currently active surface
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to activate</param>
Profile ActivateProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Deactivates the profile of the given <see cref="ProfileConfiguration" /> with the currently active surface
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to activate</param>
void DeactivateProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Permanently deletes the profile of the given <see cref="ProfileConfiguration" />
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to delete</param>
void DeleteProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Saves the provided <see cref="ProfileCategory" /> and it's <see cref="ProfileConfiguration" />s but not the
/// <see cref="Profile" />s themselves
/// </summary>
/// <param name="profileCategory">The profile category to update</param>
void SaveProfileCategory(ProfileCategory profileCategory);
/// <summary>
/// Creates a new profile category and saves it to persistent storage
/// </summary>
/// <param name="name">The name of the new profile category, must be unique</param>
/// <returns>The newly created profile category</returns>
ProfileCategory CreateProfileCategory(string name);
/// <summary>
/// Permanently deletes the provided profile category
/// </summary>
void DeleteProfileCategory(ProfileCategory profileCategory);
/// <summary>
/// Creates a new profile configuration and adds it to the provided <see cref="ProfileCategory" />
/// </summary>
/// <param name="category">The profile category to add the profile to</param>
/// <param name="name">The name of the new profile configuration</param>
/// <param name="icon">The icon of the new profile configuration</param>
/// <returns>The newly created profile configuration</returns>
ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon);
/// <summary>
/// Removes the provided profile configuration from the <see cref="ProfileCategory" />
/// </summary>
/// <param name="profileConfiguration"></param>
void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration);
/// <summary>
/// Loads the icon of this profile configuration if needed and puts it into <c>ProfileConfiguration.Icon.FileIcon</c>
/// </summary>
void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration);
/// <summary>
/// Saves the current icon of this profile
/// </summary>
void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration);
/// <summary>
/// Writes the profile to persistent storage
/// </summary>
/// <param name="profile"></param>
/// <param name="includeChildren"></param>
void UpdateProfile(Profile profile, bool includeChildren);
void SaveProfile(Profile profile, bool includeChildren);
/// <summary>
/// Disposes and permanently deletes the provided profile
/// </summary>
/// <param name="profile">The profile to delete</param>
void DeleteProfile(Profile profile);
/// <summary>
/// Permanently deletes the profile described by the provided profile descriptor
/// </summary>
/// <param name="profileDescriptor">The descriptor pointing to the profile to delete</param>
void DeleteProfile(ProfileDescriptor profileDescriptor);
/// <summary>
/// Activates the last profile of the given profile module
/// </summary>
/// <param name="profileModule"></param>
void ActivateLastProfile(ProfileModule profileModule);
/// <summary>
/// Reloads the currently active profile on the provided profile module
/// </summary>
void ReloadProfile(ProfileModule module);
/// <summary>
/// Asynchronously activates the last profile of the given profile module using a fade animation
/// </summary>
/// <param name="profileModule"></param>
/// <returns></returns>
Task ActivateLastProfileAnimated(ProfileModule profileModule);
/// <summary>
/// Activates the profile described in the given <see cref="ProfileDescriptor" /> with the currently active surface
/// </summary>
/// <param name="profileDescriptor">The descriptor describing the profile to activate</param>
Profile ActivateProfile(ProfileDescriptor profileDescriptor);
/// <summary>
/// Asynchronously activates the profile described in the given <see cref="ProfileDescriptor" /> with the currently
/// active surface using a fade animation
/// </summary>
/// <param name="profileDescriptor">The descriptor describing the profile to activate</param>
Task<Profile> ActivateProfileAnimated(ProfileDescriptor profileDescriptor);
/// <summary>
/// Clears the active profile on the given <see cref="ProfileModule" />
/// </summary>
/// <param name="module">The profile module to deactivate the active profile on</param>
void ClearActiveProfile(ProfileModule module);
/// <summary>
/// Asynchronously clears the active profile on the given <see cref="ProfileModule" /> using a fade animation
/// </summary>
/// <param name="module">The profile module to deactivate the active profile on</param>
Task ClearActiveProfileAnimated(ProfileModule module);
/// <summary>
/// Attempts to restore the profile to the state it had before the last <see cref="UpdateProfile" /> call.
/// Attempts to restore the profile to the state it had before the last <see cref="SaveProfile" /> call.
/// </summary>
/// <param name="profile"></param>
bool UndoUpdateProfile(Profile profile);
bool UndoSaveProfile(Profile profile);
/// <summary>
/// Attempts to restore the profile to the state it had before the last <see cref="UndoUpdateProfile" /> call.
/// Attempts to restore the profile to the state it had before the last <see cref="UndoSaveProfile" /> call.
/// </summary>
/// <param name="profile"></param>
bool RedoUpdateProfile(Profile profile);
bool RedoSaveProfile(Profile profile);
/// <summary>
/// Prepares the profile for rendering. You should not need to call this, it is exposed for some niche usage in the
/// core
/// Exports the profile described in the given <see cref="ProfileConfiguration" /> into an export model
/// </summary>
/// <param name="profile"></param>
void InstantiateProfile(Profile profile);
/// <param name="profileConfiguration">The profile configuration of the profile to export</param>
/// <returns>The resulting export model</returns>
ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// [Placeholder] Exports the profile described in the given <see cref="ProfileDescriptor" /> in a JSON format
/// Imports the provided base64 encoded GZIPed JSON as a profile configuration
/// </summary>
/// <param name="profileDescriptor">The descriptor of the profile to export</param>
/// <returns>The resulting JSON</returns>
string ExportProfile(ProfileDescriptor profileDescriptor);
/// <summary>
/// [Placeholder] Imports the provided base64 encoded GZIPed JSON as a profile for the given
/// <see cref="ProfileModule" />
/// </summary>
/// <param name="json">The content of the profile as JSON</param>
/// <param name="profileModule">The module to import the profile in to</param>
/// <param name="category">The <see cref="ProfileCategory" /> in which to import the profile</param>
/// <param name="exportModel">The model containing the profile to import</param>
/// <param name="makeUnique">Whether or not to give the profile a new GUID, making it unique</param>
/// <param name="markAsFreshImport">Whether or not to mark the profile as a fresh import, causing it to be adapted until any changes are made to it</param>
/// <param name="nameAffix">Text to add after the name of the profile (separated by a dash)</param>
/// <returns></returns>
ProfileDescriptor ImportProfile(string json, ProfileModule profileModule, string nameAffix = "imported");
/// <returns>The resulting profile configuration</returns>
ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, bool makeUnique = true, bool markAsFreshImport = true, string? nameAffix = "imported");
/// <summary>
/// Adapts a given profile to the currently active devices
/// </summary>
/// <param name="profile">The profile to adapt</param>
void AdaptProfile(Profile profile);
/// <summary>
/// Updates all currently active profiles
/// </summary>
void UpdateProfiles(double deltaTime);
/// <summary>
/// Renders all currently active profiles
/// </summary>
/// <param name="canvas"></param>
void RenderProfiles(SKCanvas canvas);
}
}
@@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Repositories.Interfaces;
using Newtonsoft.Json;
using Serilog;
using SkiaSharp;
namespace Artemis.Core.Services
{
@@ -14,48 +15,38 @@ namespace Artemis.Core.Services
{
private readonly ILogger _logger;
private readonly IPluginManagementService _pluginManagementService;
private readonly IRgbService _rgbService;
private readonly List<ProfileCategory> _profileCategories;
private readonly IProfileCategoryRepository _profileCategoryRepository;
private readonly IProfileRepository _profileRepository;
private readonly IRgbService _rgbService;
private readonly List<Exception> _updateExceptions = new();
private DateTime _lastUpdateExceptionLog;
private readonly List<Exception> _renderExceptions = new();
private DateTime _lastRenderExceptionLog;
public ProfileService(ILogger logger,
IPluginManagementService pluginManagementService,
IRgbService rgbService,
// TODO: Move these two
IConditionOperatorService conditionOperatorService,
IDataBindingService dataBindingService,
IProfileCategoryRepository profileCategoryRepository,
IPluginManagementService pluginManagementService,
IProfileRepository profileRepository)
{
_logger = logger;
_pluginManagementService = pluginManagementService;
_rgbService = rgbService;
_profileCategoryRepository = profileCategoryRepository;
_pluginManagementService = pluginManagementService;
_profileRepository = profileRepository;
_profileCategories = new List<ProfileCategory>(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order));
_rgbService.LedsChanged += RgbServiceOnLedsChanged;
}
_pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled;
_pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled;
public static JsonSerializerSettings MementoSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All};
public static JsonSerializerSettings ExportSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented};
public ProfileDescriptor? GetLastActiveProfile(ProfileModule module)
{
List<ProfileEntity> moduleProfiles = _profileRepository.GetByModuleId(module.Id);
if (!moduleProfiles.Any())
return CreateProfileDescriptor(module, "Default");
ProfileEntity? profileEntity = moduleProfiles.FirstOrDefault(p => p.IsActive) ?? moduleProfiles.FirstOrDefault();
return profileEntity == null ? null : new ProfileDescriptor(module, profileEntity);
}
private void SaveActiveProfile(ProfileModule module)
{
if (module.ActiveProfile == null)
return;
List<ProfileEntity> profileEntities = _profileRepository.GetByModuleId(module.Id);
foreach (ProfileEntity profileEntity in profileEntities)
{
profileEntity.IsActive = module.ActiveProfile.EntityId == profileEntity.Id;
_profileRepository.Save(profileEntity);
}
if (!_profileCategories.Any())
CreateDefaultProfileCategories();
}
/// <summary>
@@ -63,168 +54,324 @@ namespace Artemis.Core.Services
/// </summary>
private void ActiveProfilesPopulateLeds()
{
List<ProfileModule> profileModules = _pluginManagementService.GetFeaturesOfType<ProfileModule>();
foreach (ProfileModule profileModule in profileModules)
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
{
// Avoid race condition, make the check here
if (profileModule.ActiveProfile == null)
continue;
profileModule.ActiveProfile.PopulateLeds(_rgbService.EnabledDevices);
if (profileModule.ActiveProfile.IsFreshImport)
if (profileConfiguration.Profile == null) continue;
profileConfiguration.Profile.PopulateLeds(_rgbService.EnabledDevices);
if (!profileConfiguration.Profile.IsFreshImport) continue;
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile);
AdaptProfile(profileConfiguration.Profile);
}
}
private void UpdateModules()
{
lock (_profileRepository)
{
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
foreach (ProfileCategory profileCategory in _profileCategories)
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
profileConfiguration.LoadModules(modules);
}
}
private void RgbServiceOnLedsChanged(object? sender, EventArgs e)
{
ActiveProfilesPopulateLeds();
}
private void PluginManagementServiceOnPluginFeatureToggled(object? sender, PluginFeatureEventArgs e)
{
if (e.PluginFeature is Module)
UpdateModules();
}
public bool RenderForEditor { get; set; }
public void UpdateProfiles(double deltaTime)
{
lock (_profileCategories)
{
// Iterate the children in reverse because the first category must be rendered last to end up on top
for (int i = _profileCategories.Count - 1; i > -1; i--)
{
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileModule.ActiveProfile);
AdaptProfile(profileModule.ActiveProfile);
ProfileCategory profileCategory = _profileCategories[i];
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
{
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
// Profiles being edited are updated at their own leisure
if (profileConfiguration.IsBeingEdited)
continue;
bool shouldBeActive = profileConfiguration.ShouldBeActive(false);
if (shouldBeActive)
{
profileConfiguration.Update();
shouldBeActive = profileConfiguration.ActivationConditionMet;
}
try
{
// Make sure the profile is active or inactive according to the parameters above
if (shouldBeActive && profileConfiguration.Profile == null)
ActivateProfile(profileConfiguration);
else if (!shouldBeActive && profileConfiguration.Profile != null)
DeactivateProfile(profileConfiguration);
profileConfiguration.Profile?.Update(deltaTime);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
}
LogProfileUpdateExceptions();
}
}
public void RenderProfiles(SKCanvas canvas)
{
lock (_profileCategories)
{
// Iterate the children in reverse because the first category must be rendered last to end up on top
for (int i = _profileCategories.Count - 1; i > -1; i--)
{
ProfileCategory profileCategory = _profileCategories[i];
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
{
try
{
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
if (RenderForEditor)
{
if (profileConfiguration.IsBeingEdited)
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty);
}
else
{
// Ensure all criteria are met before rendering
if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && profileConfiguration.ActivationConditionMet)
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty);
}
}
catch (Exception e)
{
_renderExceptions.Add(e);
}
}
}
LogProfileRenderExceptions();
}
}
private void CreateDefaultProfileCategories()
{
foreach (DefaultCategoryName defaultCategoryName in Enum.GetValues<DefaultCategoryName>())
CreateProfileCategory(defaultCategoryName.ToString());
}
private void LogProfileUpdateExceptions()
{
// Only log update exceptions every 10 seconds to avoid spamming the logs
if (DateTime.Now - _lastUpdateExceptionLog < TimeSpan.FromSeconds(10))
return;
_lastUpdateExceptionLog = DateTime.Now;
if (!_updateExceptions.Any())
return;
// Group by stack trace, that should gather up duplicate exceptions
foreach (IGrouping<string?, Exception> exceptions in _updateExceptions.GroupBy(e => e.StackTrace))
_logger.Warning(exceptions.First(), "Exception was thrown {count} times during profile update in the last 10 seconds", exceptions.Count());
// When logging is finished start with a fresh slate
_updateExceptions.Clear();
}
private void LogProfileRenderExceptions()
{
// Only log update exceptions every 10 seconds to avoid spamming the logs
if (DateTime.Now - _lastRenderExceptionLog < TimeSpan.FromSeconds(10))
return;
_lastRenderExceptionLog = DateTime.Now;
if (!_renderExceptions.Any())
return;
// Group by stack trace, that should gather up duplicate exceptions
foreach (IGrouping<string?, Exception> exceptions in _renderExceptions.GroupBy(e => e.StackTrace))
_logger.Warning(exceptions.First(), "Exception was thrown {count} times during profile render in the last 10 seconds", exceptions.Count());
// When logging is finished start with a fresh slate
_renderExceptions.Clear();
}
public ReadOnlyCollection<ProfileCategory> ProfileCategories
{
get
{
lock (_profileRepository)
{
return _profileCategories.AsReadOnly();
}
}
}
public List<ProfileDescriptor> GetProfileDescriptors(ProfileModule module)
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations
{
List<ProfileEntity> profileEntities = _profileRepository.GetByModuleId(module.Id);
return profileEntities.Select(e => new ProfileDescriptor(module, e)).ToList();
get
{
lock (_profileRepository)
{
return _profileCategories.SelectMany(c => c.ProfileConfigurations).ToList().AsReadOnly();
}
}
}
public ProfileDescriptor CreateProfileDescriptor(ProfileModule module, string name)
public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
{
ProfileEntity profileEntity = new() {Id = Guid.NewGuid(), Name = name, ModuleId = module.Id};
_profileRepository.Add(profileEntity);
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
return;
if (profileConfiguration.Icon.FileIcon != null)
return;
return new ProfileDescriptor(module, profileEntity);
profileConfiguration.Icon.FileIcon = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId);
}
public void ActivateLastProfile(ProfileModule profileModule)
public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
{
ProfileDescriptor? activeProfile = GetLastActiveProfile(profileModule);
if (activeProfile != null)
ActivateProfile(activeProfile);
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
return;
if (profileConfiguration.Icon.FileIcon != null)
{
profileConfiguration.Icon.FileIcon.Position = 0;
_profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, profileConfiguration.Icon.FileIcon);
}
}
public async Task ActivateLastProfileAnimated(ProfileModule profileModule)
public Profile ActivateProfile(ProfileConfiguration profileConfiguration)
{
ProfileDescriptor? activeProfile = GetLastActiveProfile(profileModule);
if (activeProfile != null)
await ActivateProfileAnimated(activeProfile);
}
if (profileConfiguration.Profile != null)
return profileConfiguration.Profile;
public Profile ActivateProfile(ProfileDescriptor profileDescriptor)
{
if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id)
return profileDescriptor.ProfileModule.ActiveProfile;
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id);
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity == null)
throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}");
throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}");
Profile profile = new(profileDescriptor.ProfileModule, profileEntity);
InstantiateProfile(profile);
Profile profile = new(profileConfiguration, profileEntity);
profile.PopulateLeds(_rgbService.EnabledDevices);
profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _rgbService.EnabledDevices);
if (profile.IsFreshImport)
{
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
AdaptProfile(profile);
}
SaveActiveProfile(profileDescriptor.ProfileModule);
profileConfiguration.Profile = profile;
return profile;
}
public void ReloadProfile(ProfileModule module)
public void DeactivateProfile(ProfileConfiguration profileConfiguration)
{
if (module.ActiveProfile == null)
if (profileConfiguration.IsBeingEdited)
throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude");
if (profileConfiguration.Profile == null)
return;
ProfileEntity entity = _profileRepository.Get(module.ActiveProfile.EntityId);
Profile profile = new(module, entity);
InstantiateProfile(profile);
module.ChangeActiveProfile(null, _rgbService.EnabledDevices);
module.ChangeActiveProfile(profile, _rgbService.EnabledDevices);
Profile profile = profileConfiguration.Profile;
profileConfiguration.Profile = null;
profile.Dispose();
}
public async Task<Profile> ActivateProfileAnimated(ProfileDescriptor profileDescriptor)
public void DeleteProfile(ProfileConfiguration profileConfiguration)
{
if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id)
return profileDescriptor.ProfileModule.ActiveProfile;
DeactivateProfile(profileConfiguration);
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id);
if (profileEntity == null)
throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}");
Profile profile = new(profileDescriptor.ProfileModule, profileEntity);
InstantiateProfile(profile);
void ActivatingRgbServiceOnLedsChanged(object? sender, EventArgs e)
{
profile.PopulateLeds(_rgbService.EnabledDevices);
}
void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e)
{
if (!profile.Disposed)
InstantiateProfile(profile);
}
// This could happen during activation so subscribe to it
_pluginManagementService.PluginEnabled += ActivatingProfilePluginToggle;
_pluginManagementService.PluginDisabled += ActivatingProfilePluginToggle;
_rgbService.LedsChanged += ActivatingRgbServiceOnLedsChanged;
await profileDescriptor.ProfileModule.ChangeActiveProfileAnimated(profile, _rgbService.EnabledDevices);
if (profile.IsFreshImport)
{
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
AdaptProfile(profile);
}
SaveActiveProfile(profileDescriptor.ProfileModule);
_pluginManagementService.PluginEnabled -= ActivatingProfilePluginToggle;
_pluginManagementService.PluginDisabled -= ActivatingProfilePluginToggle;
_rgbService.LedsChanged -= ActivatingRgbServiceOnLedsChanged;
return profile;
}
public void ClearActiveProfile(ProfileModule module)
{
module.ChangeActiveProfile(null, _rgbService.EnabledDevices);
SaveActiveProfile(module);
}
public async Task ClearActiveProfileAnimated(ProfileModule module)
{
await module.ChangeActiveProfileAnimated(null, _rgbService.EnabledDevices);
}
public void DeleteProfile(Profile profile)
{
_logger.Debug("Removing profile " + profile);
// If the given profile is currently active, disable it first (this also disposes it)
if (profile.Module.ActiveProfile == profile)
ClearActiveProfile(profile.Module);
else
profile.Dispose();
_profileRepository.Remove(profile.ProfileEntity);
}
public void DeleteProfile(ProfileDescriptor profileDescriptor)
{
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id);
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity == null)
return;
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
_profileRepository.Remove(profileEntity);
SaveProfileCategory(profileConfiguration.Category);
}
public void UpdateProfile(Profile profile, bool includeChildren)
public ProfileCategory CreateProfileCategory(string name)
{
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings);
lock (_profileRepository)
{
ProfileCategory profileCategory = new(name);
_profileCategories.Add(profileCategory);
SaveProfileCategory(profileCategory);
return profileCategory;
}
}
public void DeleteProfileCategory(ProfileCategory profileCategory)
{
List<ProfileConfiguration> profileConfigurations = profileCategory.ProfileConfigurations.ToList();
foreach (ProfileConfiguration profileConfiguration in profileConfigurations)
RemoveProfileConfiguration(profileConfiguration);
lock (_profileRepository)
{
_profileCategories.Remove(profileCategory);
_profileCategoryRepository.Remove(profileCategory.Entity);
}
}
/// <summary>
/// Creates a new profile configuration and adds it to the provided <see cref="ProfileCategory" />
/// </summary>
/// <param name="category">The profile category to add the profile to</param>
/// <param name="name">The name of the new profile configuration</param>
/// <param name="icon">The icon of the new profile configuration</param>
/// <returns>The newly created profile configuration</returns>
public ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon)
{
ProfileConfiguration configuration = new(category, name, icon);
ProfileEntity entity = new();
_profileRepository.Add(entity);
configuration.Entity.ProfileId = entity.Id;
category.AddProfileConfiguration(configuration, 0);
return configuration;
}
/// <summary>
/// Removes the provided profile configuration from the <see cref="ProfileCategory" />
/// </summary>
/// <param name="profileConfiguration"></param>
public void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration)
{
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
DeactivateProfile(profileConfiguration);
SaveProfileCategory(profileConfiguration.Category);
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity != null)
_profileRepository.Remove(profileEntity);
}
public void SaveProfileCategory(ProfileCategory profileCategory)
{
profileCategory.Save();
_profileCategoryRepository.Save(profileCategory.Entity);
lock (_profileCategories)
{
_profileCategories.Sort((a, b) => a.Order - b.Order);
}
}
public void SaveProfile(Profile profile, bool includeChildren)
{
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
profile.Save();
if (includeChildren)
{
@@ -235,7 +382,7 @@ namespace Artemis.Core.Services
}
// If there are no changes, don't bother saving
string updatedMemento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings);
string updatedMemento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
if (memento.Equals(updatedMemento))
{
_logger.Debug("Updating profile - Skipping save, no changes");
@@ -253,7 +400,7 @@ namespace Artemis.Core.Services
_profileRepository.Save(profile.ProfileEntity);
}
public bool UndoUpdateProfile(Profile profile)
public bool UndoSaveProfile(Profile profile)
{
// Keep the profile from being rendered by locking it
lock (profile)
@@ -265,20 +412,20 @@ namespace Artemis.Core.Services
}
string top = profile.UndoStack.Pop();
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings);
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
profile.RedoStack.Push(memento);
profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, MementoSettings)
profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, IProfileService.MementoSettings)
?? throw new InvalidOperationException("Failed to deserialize memento");
profile.Load();
InstantiateProfile(profile);
profile.PopulateLeds(_rgbService.EnabledDevices);
}
_logger.Debug("Undo profile update - Success");
return true;
}
public bool RedoUpdateProfile(Profile profile)
public bool RedoSaveProfile(Profile profile)
{
// Keep the profile from being rendered by locking it
lock (profile)
@@ -290,53 +437,81 @@ namespace Artemis.Core.Services
}
string top = profile.RedoStack.Pop();
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings);
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
profile.UndoStack.Push(memento);
profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, MementoSettings)
profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, IProfileService.MementoSettings)
?? throw new InvalidOperationException("Failed to deserialize memento");
profile.Load();
InstantiateProfile(profile);
profile.PopulateLeds(_rgbService.EnabledDevices);
_logger.Debug("Redo profile update - Success");
return true;
}
}
public void InstantiateProfile(Profile profile)
public ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration)
{
profile.PopulateLeds(_rgbService.EnabledDevices);
// The profile may not be active and in that case lets activate it real quick
Profile profile = profileConfiguration.Profile ?? ActivateProfile(profileConfiguration);
return new ProfileConfigurationExportModel
{
ProfileConfigurationEntity = profileConfiguration.Entity,
ProfileEntity = profile.ProfileEntity,
ProfileImage = profileConfiguration.Icon.FileIcon
};
}
public string ExportProfile(ProfileDescriptor profileDescriptor)
public ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, bool makeUnique, bool markAsFreshImport, string? nameAffix)
{
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id);
if (profileEntity == null)
throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}");
if (exportModel.ProfileEntity == null)
throw new ArtemisCoreException("Cannot import a profile without any data");
return JsonConvert.SerializeObject(profileEntity, ExportSettings);
}
public ProfileDescriptor ImportProfile(string json, ProfileModule profileModule, string nameAffix)
{
ProfileEntity? profileEntity = JsonConvert.DeserializeObject<ProfileEntity>(json, ExportSettings);
if (profileEntity == null)
throw new ArtemisCoreException("Failed to import profile but JSON.NET threw no error :(");
// Create a copy of the entity because we'll be using it from now on
ProfileEntity profileEntity = JsonConvert.DeserializeObject<ProfileEntity>(
JsonConvert.SerializeObject(exportModel.ProfileEntity, IProfileService.ExportSettings), IProfileService.ExportSettings
)!;
// Assign a new GUID to make sure it is unique in case of a previous import of the same content
profileEntity.UpdateGuid(Guid.NewGuid());
profileEntity.Name = $"{profileEntity.Name} - {nameAffix}";
profileEntity.IsFreshImport = true;
profileEntity.IsActive = false;
if (makeUnique)
profileEntity.UpdateGuid(Guid.NewGuid());
if (nameAffix != null)
profileEntity.Name = $"{profileEntity.Name} - {nameAffix}";
if (markAsFreshImport)
profileEntity.IsFreshImport = true;
_profileRepository.Add(profileEntity);
return new ProfileDescriptor(profileModule, profileEntity);
ProfileConfiguration profileConfiguration;
if (exportModel.ProfileConfigurationEntity != null)
{
// A new GUID will be given on save
exportModel.ProfileConfigurationEntity.FileIconId = Guid.Empty;
profileConfiguration = new ProfileConfiguration(category, exportModel.ProfileConfigurationEntity);
if (nameAffix != null)
profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}";
}
else
{
profileConfiguration = new ProfileConfiguration(category, exportModel.ProfileEntity!.Name, "Import");
}
if (exportModel.ProfileImage != null)
profileConfiguration.Icon.FileIcon = exportModel.ProfileImage;
profileConfiguration.Entity.ProfileId = profileEntity.Id;
category.AddProfileConfiguration(profileConfiguration, 0);
SaveProfileCategory(category);
return profileConfiguration;
}
/// <inheritdoc />
public void AdaptProfile(Profile profile)
{
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings);
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
List<ArtemisDevice> devices = _rgbService.EnabledDevices.ToList();
foreach (Layer layer in profile.GetAllLayers())
@@ -355,14 +530,5 @@ namespace Artemis.Core.Services
_profileRepository.Save(profile.ProfileEntity);
}
#region Event handlers
private void RgbServiceOnLedsChanged(object? sender, EventArgs e)
{
ActiveProfilesPopulateLeds();
}
#endregion
}
}
@@ -15,17 +15,7 @@ namespace Artemis.Core.Services
/// </summary>
public class DataModelJsonPluginEndPoint<T> : PluginEndPoint where T : DataModel
{
private readonly ProfileModule<T>? _profileModule;
private readonly Module<T>? _module;
private readonly DataModelExpansion<T>? _dataModelExpansion;
internal DataModelJsonPluginEndPoint(ProfileModule<T> profileModule, string name, PluginsModule pluginsModule) : base(profileModule, name, pluginsModule)
{
_profileModule = profileModule ?? throw new ArgumentNullException(nameof(profileModule));
ThrowOnFail = true;
Accepts = MimeType.Json;
}
private readonly Module<T> _module;
internal DataModelJsonPluginEndPoint(Module<T> module, string name, PluginsModule pluginsModule) : base(module, name, pluginsModule)
{
@@ -35,14 +25,6 @@ namespace Artemis.Core.Services
Accepts = MimeType.Json;
}
internal DataModelJsonPluginEndPoint(DataModelExpansion<T> dataModelExpansion, string name, PluginsModule pluginsModule) : base(dataModelExpansion, name, pluginsModule)
{
_dataModelExpansion = dataModelExpansion ?? throw new ArgumentNullException(nameof(dataModelExpansion));
ThrowOnFail = true;
Accepts = MimeType.Json;
}
/// <summary>
/// Whether or not the end point should throw an exception if deserializing the received JSON fails.
/// If set to <see langword="false" /> malformed JSON is silently ignored; if set to <see langword="true" /> malformed
@@ -63,12 +45,7 @@ namespace Artemis.Core.Services
using TextReader reader = context.OpenRequestText();
try
{
if (_profileModule != null)
JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _profileModule.DataModel);
else if (_module != null)
JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _module.DataModel);
else
JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _dataModelExpansion!.DataModel);
JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _module.DataModel);
}
catch (JsonException)
{
@@ -54,26 +54,6 @@ namespace Artemis.Core.Services
/// <returns>The resulting end point</returns>
DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(Module<T> module, string endPointName) where T : DataModel;
/// <summary>
/// Adds a new endpoint that directly maps received JSON to the data model of the provided
/// <paramref name="profileModule" />.
/// </summary>
/// <typeparam name="T">The data model type of the module</typeparam>
/// <param name="profileModule">The module whose datamodel to apply the received JSON to</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <returns>The resulting end point</returns>
DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(ProfileModule<T> profileModule, string endPointName) where T : DataModel;
/// <summary>
/// Adds a new endpoint that directly maps received JSON to the data model of the provided
/// <paramref name="dataModelExpansion" />.
/// </summary>
/// <typeparam name="T">The data model type of the module</typeparam>
/// <param name="dataModelExpansion">The data model expansion whose datamodel to apply the received JSON to</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <returns>The resulting end point</returns>
DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(DataModelExpansion<T> dataModelExpansion, string endPointName) where T : DataModel;
/// <summary>
/// Adds a new endpoint for the given plugin feature receiving an a <see cref="string" />.
/// </summary>
@@ -142,24 +142,6 @@ namespace Artemis.Core.Services
return endPoint;
}
public DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(ProfileModule<T> profileModule, string endPointName) where T : DataModel
{
if (profileModule == null) throw new ArgumentNullException(nameof(profileModule));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
DataModelJsonPluginEndPoint<T> endPoint = new(profileModule, endPointName, PluginsModule);
PluginsModule.AddPluginEndPoint(endPoint);
return endPoint;
}
public DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(DataModelExpansion<T> dataModelExpansion, string endPointName) where T : DataModel
{
if (dataModelExpansion == null) throw new ArgumentNullException(nameof(dataModelExpansion));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
DataModelJsonPluginEndPoint<T> endPoint = new(dataModelExpansion, endPointName, PluginsModule);
PluginsModule.AddPluginEndPoint(endPoint);
return endPoint;
}
private void HandleDataModelRequest<T>(Module<T> module, T value) where T : DataModel
{
}
+1 -1
View File
@@ -17,7 +17,7 @@ namespace Artemis.Core
if (Registrations.Any(r => r.DataModel == dataModel))
throw new ArtemisCoreException($"Data model store already contains data model '{dataModel.DataModelDescription}'");
registration = new DataModelRegistration(dataModel, dataModel.Feature) {IsInStore = true};
registration = new DataModelRegistration(dataModel, dataModel.Module) {IsInStore = true};
Registrations.Add(registration);
}
@@ -1,11 +1,12 @@
using Artemis.Core.LayerEffects;
using Artemis.Core.Modules;
namespace Artemis.Core
{
/// <summary>
/// An empty data model plugin feature used by <see cref="Constants.CorePlugin" />
/// </summary>
internal class CorePluginFeature : DataModelPluginFeature
internal class CorePluginFeature : Module
{
public CorePluginFeature()
{
@@ -20,6 +21,10 @@ namespace Artemis.Core
public override void Disable()
{
}
public override void Update(double deltaTime)
{
}
}
internal class EffectPlaceholderPlugin : LayerEffectProvider
@@ -1,99 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Serilog;
using SkiaSharp;
namespace Artemis.Core
{
internal class IntroAnimation
{
private readonly ILogger _logger;
private readonly IProfileService _profileService;
private readonly IEnumerable<ArtemisDevice> _devices;
public IntroAnimation(ILogger logger, IProfileService profileService, IEnumerable<ArtemisDevice> devices)
{
_logger = logger;
_profileService = profileService;
_devices = devices;
AnimationProfile = CreateIntroProfile();
}
public Profile AnimationProfile { get; set; }
public void Render(double deltaTime, SKCanvas canvas)
{
AnimationProfile.Update(deltaTime);
AnimationProfile.Render(canvas, SKPointI.Empty);
}
private Profile CreateIntroProfile()
{
try
{
// Load the intro profile from JSON into a ProfileEntity
string json = File.ReadAllText(Path.Combine(Constants.ApplicationFolder, "Resources", "intro-profile.json"));
ProfileEntity profileEntity = CoreJson.DeserializeObject<ProfileEntity>(json)!;
// Inject every LED on the surface into each layer
foreach (LayerEntity profileEntityLayer in profileEntity.Layers)
profileEntityLayer.Leds.AddRange(_devices.SelectMany(d => d.Leds).Select(l => new LedEntity
{
DeviceIdentifier = l.Device.Identifier,
LedName = l.RgbLed.Id.ToString()
}));
Profile profile = new(new DummyModule(), profileEntity);
profile.Activate(_devices);
_profileService.InstantiateProfile(profile);
return profile;
}
catch (Exception e)
{
_logger.Warning(e, "Failed to load intro profile");
}
return new Profile(new DummyModule(), "Intro");
}
}
internal class DummyModule : ProfileModule
{
public override void Enable()
{
throw new NotImplementedException();
}
public override void Disable()
{
throw new NotImplementedException();
}
public override void Update(double deltaTime)
{
throw new NotImplementedException();
}
public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
throw new NotImplementedException();
}
public override void ModuleActivated(bool isOverride)
{
throw new NotImplementedException();
}
public override void ModuleDeactivated(bool isOverride)
{
throw new NotImplementedException();
}
}
}