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

@@ -0,0 +1,11 @@
using System;
namespace Artemis.Core.DataModelExpansions
{
/// <summary>
/// Represents an attribute that marks a data model property to be ignored by the UI
/// </summary>
public class DataModelIgnoreAttribute : Attribute
{
}
}
@@ -0,0 +1,46 @@
using System;
namespace Artemis.Core.DataModelExpansions
{
/// <summary>
/// Represents an attribute that describes a data model property
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class DataModelPropertyAttribute : Attribute
{
/// <summary>
/// Gets or sets the user-friendly name for this property, shown in the UI.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the user-friendly description for this property, shown in the UI.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the an optional prefix to show before displaying elements in the UI.
/// </summary>
public string? Prefix { get; set; }
/// <summary>
/// Gets or sets an optional affix to show behind displaying elements in the UI.
/// </summary>
public string? Affix { get; set; }
/// <summary>
/// Gets or sets an optional maximum value, this value is not enforced but used for percentage calculations.
/// </summary>
public object? MaxValue { get; set; }
/// <summary>
/// Gets or sets an optional minimum value, this value is not enforced but used for percentage calculations.
/// </summary>
public object? MinValue { get; set; }
/// <summary>
/// Gets or sets whether this property resets the max depth of the data model, defaults to true
/// </summary>
public bool ResetsDepth { get; set; } = true;
}
}
@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Artemis.Core.Modules;
using Humanizer;
using Newtonsoft.Json;
using Module = Artemis.Core.Modules.Module;
namespace Artemis.Core.DataModelExpansions
{
/// <summary>
/// Represents a data model that contains information on a game/application etc.
/// </summary>
public abstract class DataModel
{
private readonly Dictionary<string, DynamicChild> _dynamicChildren = new();
/// <summary>
/// Creates a new instance of the <see cref="DataModel" /> class
/// </summary>
protected DataModel()
{
// These are both set right after construction to keep the constructor of inherited classes clean
Module = null!;
DataModelDescription = null!;
}
/// <summary>
/// Gets the module this data model belongs to
/// </summary>
[JsonIgnore]
[DataModelIgnore]
public Module Module { get; internal set; }
/// <summary>
/// Gets the <see cref="DataModelPropertyAttribute" /> describing this data model
/// </summary>
[JsonIgnore]
[DataModelIgnore]
public DataModelPropertyAttribute DataModelDescription { get; internal set; }
/// <summary>
/// Gets the is expansion status indicating whether this data model expands the main data model
/// </summary>
[DataModelIgnore]
public bool IsExpansion { get; internal set; }
/// <summary>
/// Gets an read-only dictionary of all dynamic children
/// </summary>
[DataModelIgnore]
public ReadOnlyDictionary<string, DynamicChild> DynamicChildren => new(_dynamicChildren);
/// <summary>
/// Returns a read-only collection of all properties in this datamodel that are to be ignored
/// </summary>
/// <returns></returns>
public ReadOnlyCollection<PropertyInfo> GetHiddenProperties()
{
if (Module is Module module)
return module.HiddenProperties;
return new List<PropertyInfo>().AsReadOnly();
}
/// <summary>
/// Occurs when a dynamic child has been added to this data model
/// </summary>
public event EventHandler<DynamicDataModelChildEventArgs>? DynamicChildAdded;
/// <summary>
/// Occurs when a dynamic child has been removed from this data model
/// </summary>
public event EventHandler<DynamicDataModelChildEventArgs>? DynamicChildRemoved;
/// <summary>
/// Invokes the <see cref="DynamicChildAdded" /> event
/// </summary>
protected virtual void OnDynamicDataModelAdded(DynamicDataModelChildEventArgs e)
{
DynamicChildAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="DynamicChildRemoved" /> event
/// </summary>
protected virtual void OnDynamicDataModelRemoved(DynamicDataModelChildEventArgs e)
{
DynamicChildRemoved?.Invoke(this, e);
}
#region Dynamic children
/// <summary>
/// Adds a dynamic child to this data model
/// </summary>
/// <param name="key">The key of the child, must be unique to this data model</param>
/// <param name="initialValue">The initial value of the dynamic child</param>
/// <returns>The resulting dynamic child which can be used to further update the value</returns>
public DynamicChild<T> AddDynamicChild<T>(string key, T initialValue)
{
return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute());
}
/// <summary>
/// Adds a dynamic child to this data model
/// </summary>
/// <param name="key">The key of the child, must be unique to this data model</param>
/// <param name="initialValue">The initial value of the dynamic child</param>
/// <param name="name">A human readable name for your dynamic child, shown in the UI</param>
/// <param name="description">An optional description, shown in the UI</param>
/// <returns>The resulting dynamic child which can be used to further update the value</returns>
public DynamicChild<T> AddDynamicChild<T>(string key, T initialValue, string name, string? description = null)
{
return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute {Name = name, Description = description});
}
/// <summary>
/// Adds a dynamic child to this data model
/// </summary>
/// <param name="key">The key of the child, must be unique to this data model</param>
/// <param name="initialValue">The initial value of the dynamic child</param>
/// <param name="attribute">A data model property attribute describing the dynamic child</param>
/// <returns>The resulting dynamic child which can be used to further update the value</returns>
public DynamicChild<T> AddDynamicChild<T>(string key, T initialValue, DataModelPropertyAttribute attribute)
{
if (key == null) throw new ArgumentNullException(nameof(key));
if (initialValue == null) throw new ArgumentNullException(nameof(initialValue));
if (attribute == null) throw new ArgumentNullException(nameof(attribute));
if (key.Contains('.'))
throw new ArtemisCoreException("The provided key contains an illegal character (.)");
if (_dynamicChildren.ContainsKey(key))
{
throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " +
"because the key is already in use on by another dynamic property this data model.");
}
if (GetType().GetProperty(key) != null)
{
throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " +
"because the key is already in use by a static property on this data model.");
}
// Make sure a name is on the attribute or funny things might happen
attribute.Name ??= key.Humanize();
if (initialValue is DataModel dynamicDataModel)
{
dynamicDataModel.Module = Module;
dynamicDataModel.DataModelDescription = attribute;
}
DynamicChild<T> dynamicChild = new(initialValue, key, attribute);
_dynamicChildren.Add(key, dynamicChild);
OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key));
return dynamicChild;
}
/// <summary>
/// Gets a previously added dynamic child by its key
/// </summary>
/// <param name="key">The key of the dynamic child</param>
public DynamicChild GetDynamicChild(string key)
{
if (key == null) throw new ArgumentNullException(nameof(key));
return DynamicChildren[key];
}
/// <summary>
/// Gets a previously added dynamic child by its key
/// </summary>
/// <typeparam name="T">The typer of dynamic child you are expecting</typeparam>
/// <param name="key">The key of the dynamic child</param>
/// <returns></returns>
public DynamicChild<T> GetDynamicChild<T>(string key)
{
if (key == null) throw new ArgumentNullException(nameof(key));
return (DynamicChild<T>) DynamicChildren[key];
}
/// <summary>
/// Gets a previously added dynamic child by its key
/// </summary>
/// <param name="key">The key of the dynamic child</param>
/// <param name="dynamicChild">
/// When this method returns, the <see cref="DynamicChild" /> associated with the specified key,
/// if the key is found; otherwise, <see langword="null" />. This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true" /> if the data model contains the dynamic child; otherwise <see langword="false" />
/// </returns>
public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild)
{
if (key == null) throw new ArgumentNullException(nameof(key));
dynamicChild = null;
if (!DynamicChildren.TryGetValue(key, out DynamicChild? value))
return false;
dynamicChild = value;
return true;
}
/// <summary>
/// Gets a previously added dynamic child by its key
/// </summary>
/// <typeparam name="T">The typer of dynamic child you are expecting</typeparam>
/// <param name="key">The key of the dynamic child</param>
/// <param name="dynamicChild">
/// When this method returns, the <see cref="DynamicChild{T}" /> associated with the specified
/// key, if the key is found and the type matches; otherwise, <see langword="null" />. This parameter is passed
/// uninitialized.
/// </param>
/// <returns>
/// <see langword="true" /> if the data model contains the dynamic child; otherwise <see langword="false" />
/// </returns>
public bool TryGetDynamicChild<T>(string key, [MaybeNullWhen(false)] out DynamicChild<T> dynamicChild)
{
if (key == null) throw new ArgumentNullException(nameof(key));
dynamicChild = null;
if (!DynamicChildren.TryGetValue(key, out DynamicChild? value))
return false;
if (value is not DynamicChild<T> typedDynamicChild)
return false;
dynamicChild = typedDynamicChild;
return true;
}
/// <summary>
/// Removes a dynamic child from the data model by its key
/// </summary>
/// <param name="key">The key of the dynamic child to remove</param>
public void RemoveDynamicChildByKey(string key)
{
if (key == null) throw new ArgumentNullException(nameof(key));
if (!_dynamicChildren.TryGetValue(key, out DynamicChild? dynamicChild))
return;
_dynamicChildren.Remove(key);
OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key));
}
/// <summary>
/// Removes a dynamic child from this data model
/// </summary>
/// <param name="dynamicChild">The dynamic data child to remove</param>
public void RemoveDynamicChild(DynamicChild dynamicChild)
{
if (dynamicChild == null) throw new ArgumentNullException(nameof(dynamicChild));
List<string> keys = _dynamicChildren.Where(kvp => kvp.Value.BaseValue == dynamicChild).Select(kvp => kvp.Key).ToList();
foreach (string key in keys)
{
_dynamicChildren.Remove(key);
OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key));
}
}
/// <summary>
/// Removes all dynamic children from this data model
/// </summary>
public void ClearDynamicChildren()
{
while (_dynamicChildren.Any())
RemoveDynamicChildByKey(_dynamicChildren.First().Key);
}
// Used a runtime by data model paths only
internal T? GetDynamicChildValue<T>(string key)
{
if (TryGetDynamicChild(key, out DynamicChild? dynamicChild) && dynamicChild.BaseValue != null)
return (T) dynamicChild.BaseValue;
return default;
}
#endregion
}
}
@@ -0,0 +1,66 @@
using System;
namespace Artemis.Core.DataModelExpansions
{
/// <summary>
/// Represents a dynamic child value with its property attribute
/// </summary>
public class DynamicChild<T> : DynamicChild
{
internal DynamicChild(T value, string key, DataModelPropertyAttribute attribute) : base(key, attribute, typeof(T))
{
Value = value;
}
/// <summary>
/// Gets or sets the current value of the dynamic child
/// </summary>
public new T Value { get; set; }
/// <inheritdoc />
protected override object? GetValue()
{
return Value;
}
}
/// <summary>
/// Represents a dynamic child value with its property attribute
/// </summary>
public abstract class DynamicChild
{
internal DynamicChild(string key, DataModelPropertyAttribute attribute, Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
Key = key ?? throw new ArgumentNullException(nameof(key));
Attribute = attribute ?? throw new ArgumentNullException(nameof(attribute));
Type = type;
}
/// <summary>
/// Gets the key of the dynamic child
/// </summary>
public string Key { get; }
/// <summary>
/// Gets the attribute describing the dynamic child
/// </summary>
public DataModelPropertyAttribute Attribute { get; }
/// <summary>
/// Gets the type of <see cref="BaseValue" />
/// </summary>
public Type Type { get; }
/// <summary>
/// Gets the current value of the dynamic child
/// </summary>
public object? BaseValue => GetValue();
/// <summary>
/// Gets the current value of the dynamic child
/// </summary>
/// <returns>The current value of the dynamic child</returns>
protected abstract object? GetValue();
}
}
+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);
}
}
}