using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using Artemis.Core.DataModelExpansions; using SkiaSharp; namespace Artemis.Core.Modules { /// /// Allows you to add support for new games/applications while utilizing Artemis' profile engine and your own data /// model /// public abstract class ProfileModule : ProfileModule where T : DataModel { /// /// The data model driving this module /// Note: This default data model is automatically registered upon plugin enable /// 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; } /// /// Gets or sets whether this module must also expand the main data model /// /// Note: If expanding the main data model is all you want your plugin to do, create a /// plugin instead. /// /// public bool ExpandsDataModel { get => InternalExpandsMainDataModel; set => InternalExpandsMainDataModel = value; } /// /// Override to provide your own data model description. By default this returns a description matching your plugin /// name and description /// /// public virtual DataModelPropertyAttribute GetDataModelDescription() { return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description}; } /// /// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC) /// /// A lambda expression pointing to the property to ignore public void HideProperty(Expression> propertyLambda) { PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo))) HiddenPropertiesList.Add(propertyInfo); } /// /// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm => /// dm.TimeDataModel.CurrentTimeUTC) /// /// A lambda expression pointing to the property to stop ignoring public void ShowProperty(Expression> propertyLambda) { PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo)); } internal override void InternalEnable() { DataModel = Activator.CreateInstance(); DataModel.Feature = this; DataModel.DataModelDescription = GetDataModelDescription(); base.InternalEnable(); } internal override void InternalDisable() { Deactivate(true); base.InternalDisable(); } } /// /// Allows you to add support for new games/applications while utilizing Artemis' profile engine /// public abstract class ProfileModule : Module { /// /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) /// protected internal readonly List HiddenPropertiesList = new(); private readonly object _lock = new(); /// /// Creates a new instance of the class /// protected ProfileModule() { OpacityOverride = 1; } /// /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) /// public ReadOnlyCollection HiddenProperties => HiddenPropertiesList.AsReadOnly(); /// /// Gets the currently active profile /// public Profile? ActiveProfile { get; private set; } /// /// Disables updating the profile, rendering does continue /// public bool IsProfileUpdatingDisabled { get; set; } /// /// Overrides the opacity of the root folder /// public double OpacityOverride { get; set; } /// /// Indicates whether or not a profile change is being animated /// public bool AnimatingProfileChange { get; private set; } /// /// Called after the profile has updated /// /// Time in seconds since the last update public virtual void ProfileUpdated(double deltaTime) { } /// /// Called after the profile has rendered /// /// Time since the last render /// /// public virtual void ProfileRendered(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { } internal override void InternalUpdate(double deltaTime) { 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); } internal override void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { Render(deltaTime, canvas, canvasInfo); lock (_lock) { // Render the profile ActiveProfile?.Render(canvas); } ProfileRendered(deltaTime, canvas, canvasInfo); } internal async Task ChangeActiveProfileAnimated(Profile? profile, IEnumerable 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 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); } #region Events /// /// Occurs when the has changed /// public event EventHandler? ActiveProfileChanged; /// /// Invokes the event /// protected virtual void OnActiveProfileChanged() { ActiveProfileChanged?.Invoke(this, EventArgs.Empty); } #endregion } }