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,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"