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.Text; namespace Artemis.Core.Modules { /// /// Allows you to add new data to the Artemis data model /// public abstract class Module : Module where T : DataModel { /// /// The data model driving this module /// Note: This default data model is automatically registered and instantiated 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; } /// /// 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)); } /// /// Determines whether the provided dot-separated path is actively being used by Artemis /// Note: is slightly faster but string-based. /// /// /// The path to check per example: IsPropertyInUse(dm => dm.TimeDataModel.CurrentTimeUTC) /// /// /// If any child of the given path will return true as well; if /// only an exact path match returns . /// public bool IsPropertyInUse(Expression> propertyLambda, bool includeChildren) { string path = GetMemberPath((MemberExpression) propertyLambda.Body); return IsPropertyInUse(path, includeChildren); } /// /// Determines whether the provided dot-separated path is actively being used by Artemis /// /// The path to check per example: MyDataModelChild.MyDataModelProperty /// /// If any child of the given path will return true as well; if /// only an exact path match returns . /// public bool IsPropertyInUse(string path, bool includeChildren) { return DataModel.IsPropertyInUse(path, includeChildren); } internal override void InternalEnable() { DataModel = Activator.CreateInstance(); DataModel.Module = this; DataModel.DataModelDescription = GetDataModelDescription(); base.InternalEnable(); } internal override void InternalDisable() { Deactivate(true); base.InternalDisable(); } private static string GetMemberPath(MemberExpression? me) { StringBuilder builder = new(); while (me != null) { builder.Insert(0, me.Member.Name); me = me.Expression as MemberExpression; if (me != null) builder.Insert(0, "."); } return builder.ToString(); } } /// /// For internal use only, please use . /// public abstract class Module : PluginFeature { private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new(); private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new(); protected Module() { DefaultProfilePaths = new ReadOnlyCollection<(DefaultCategoryName, string)>(_defaultProfilePaths); HiddenProperties = new(HiddenPropertiesList); } /// /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) /// protected internal readonly List HiddenPropertiesList = new(); /// /// Gets a read only collection of default profile paths /// public IReadOnlyCollection<(DefaultCategoryName, string)> DefaultProfilePaths { get; } /// /// A list of activation requirements /// /// If this list is not and not empty becomes /// and the data of this module is only available to profiles specifically targeting it. /// /// public abstract List? ActivationRequirements { get; } /// /// Gets whether this module is activated. A module can only be active while its /// are met /// public bool IsActivated { get; internal set; } /// /// Gets whether this module's activation was due to an override, can only be true if is /// /// public bool IsActivatedOverride { get; private set; } /// /// Gets whether this module should update while is . When /// set to and any timed updates will not get called during an /// activation override. /// Defaults to /// public bool UpdateDuringActivationOverride { get; protected set; } /// /// Gets or sets the activation requirement mode, defaults to /// public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any; /// /// Gets a boolean indicating whether this module is always available to profiles or only to profiles that specifically /// target this module. /// /// Note: if there are any ; otherwise /// /// /// public bool IsAlwaysAvailable => ActivationRequirements == null || ActivationRequirements.Count == 0; /// /// Gets whether updating this module is currently allowed /// public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride); /// /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) /// public ReadOnlyCollection HiddenProperties { get; } internal DataModel? InternalDataModel { get; set; } /// /// Called each frame when the module should update /// /// Time in seconds since the last update public abstract void Update(double deltaTime); /// /// Called when the are met or during an override /// /// /// If true, the activation was due to an override. This usually means the module was activated /// by the profile editor /// public virtual void ModuleActivated(bool isOverride) { } /// /// Called when the are no longer met or during an override /// /// /// If true, the deactivation was due to an override. This usually means the module was deactivated /// by the profile editor /// public virtual void ModuleDeactivated(bool isOverride) { } /// /// Evaluates the activation requirements following the and returns the result /// /// The evaluated result of the activation requirements public bool EvaluateActivationRequirements() { if (IsAlwaysAvailable) return true; if (ActivationRequirementMode == ActivationRequirementType.All) return ActivationRequirements!.All(r => r.Evaluate()); if (ActivationRequirementMode == ActivationRequirementType.Any) return ActivationRequirements!.Any(r => r.Evaluate()); return false; } /// /// 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}; } /// /// Adds a default profile by reading it from the file found at the provided path /// /// The category in which to place the default profile /// A path pointing towards a profile file. May be relative to the plugin directory. /// /// if the default profile was added; if it was not because it is /// already in the list. /// 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(); if (IsUpdateAllowed) Update(deltaTime); StopUpdateMeasure(); } internal virtual void Activate(bool isOverride) { if (IsActivated) return; IsActivatedOverride = isOverride; ModuleActivated(isOverride); IsActivated = true; } internal virtual void Deactivate(bool isOverride) { if (!IsActivated) return; IsActivatedOverride = false; IsActivated = false; ModuleDeactivated(isOverride); } #region Overrides of PluginFeature /// 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) return; Deactivate(isDeactivateOverride); Activate(isActivateOverride); } } /// /// Describes in what way the activation requirements of a module must be met /// public enum ActivationRequirementType { /// /// Any activation requirement must be met for the module to activate /// Any, /// /// All activation requirements must be met for the module to activate /// All } }