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
}
}