Code style - Use file scoped namespaces

Code style - Ran code cleanup
This commit is contained in:
Robert committed 2022-08-21 11:36:15 +02:00
1 parent e68e16df4d
commit f6090dc296
691 files changed
+37301 -38119

No files matched your search

@@ -5,124 +5,126 @@ using Ninject;
using RGB.NET.Core;
using Serilog;
namespace Artemis.Core.DeviceProviders
namespace Artemis.Core.DeviceProviders;
/// <inheritdoc />
/// <summary>
/// Allows you to implement and register your own device provider
/// </summary>
public abstract class DeviceProvider : PluginFeature
{
/// <inheritdoc />
/// <summary>
/// Allows you to implement and register your own device provider
/// Creates a new instance of the <see cref="DeviceProvider" /> class
/// </summary>
public abstract class DeviceProvider : PluginFeature
/// <param name="rgbDeviceProvider"></param>
protected DeviceProvider(IRGBDeviceProvider rgbDeviceProvider)
{
/// <summary>
/// Creates a new instance of the <see cref="DeviceProvider" /> class
/// </summary>
/// <param name="rgbDeviceProvider"></param>
protected DeviceProvider(IRGBDeviceProvider rgbDeviceProvider)
{
RgbDeviceProvider = rgbDeviceProvider ?? throw new ArgumentNullException(nameof(rgbDeviceProvider));
}
RgbDeviceProvider = rgbDeviceProvider ?? throw new ArgumentNullException(nameof(rgbDeviceProvider));
}
/// <summary>
/// The RGB.NET device provider backing this Artemis device provider
/// </summary>
public IRGBDeviceProvider RgbDeviceProvider { get; }
/// <summary>
/// The RGB.NET device provider backing this Artemis device provider
/// </summary>
public IRGBDeviceProvider RgbDeviceProvider { get; }
/// <summary>
/// TODO: Make internal while still injecting.
/// A logger used by the device provider internally, ignore this
/// </summary>
[Inject]
public ILogger? Logger { get; set; }
/// <summary>
/// TODO: Make internal while still injecting.
/// A logger used by the device provider internally, ignore this
/// </summary>
[Inject]
public ILogger? Logger { get; set; }
/// <summary>
/// A boolean indicating whether this device provider detects the physical layout of connected keyboards.
/// <para>
/// Note: <see cref="GetLogicalLayout" /> is only called when this or <see cref="CanDetectLogicalLayout" />
/// is <see langword="true" />.
/// </para>
/// </summary>
public bool CanDetectPhysicalLayout { get; protected set; }
/// <summary>
/// A boolean indicating whether this device provider detects the physical layout of connected keyboards.
/// <para>
/// Note: <see cref="GetLogicalLayout" /> is only called when this or <see cref="CanDetectLogicalLayout" />
/// is <see langword="true" />.
/// </para>
/// </summary>
public bool CanDetectPhysicalLayout { get; protected set; }
/// <summary>
/// A boolean indicating whether this device provider detects the logical layout of connected keyboards
/// <para>
/// Note: <see cref="GetLogicalLayout" /> is only called when this or <see cref="CanDetectPhysicalLayout" />
/// is <see langword="true" />.
/// </para>
/// </summary>
public bool CanDetectLogicalLayout { get; protected set; }
/// <summary>
/// A boolean indicating whether this device provider detects the logical layout of connected keyboards
/// <para>
/// Note: <see cref="GetLogicalLayout" /> is only called when this or <see cref="CanDetectPhysicalLayout" />
/// is <see langword="true" />.
/// </para>
/// </summary>
public bool CanDetectLogicalLayout { get; protected set; }
/// <summary>
/// Gets or sets a boolean indicating whether adding missing LEDs defined in a layout but missing on the device is supported
/// <para>Note: Defaults to <see langword="true" />.</para>
/// </summary>
public bool CreateMissingLedsSupported { get; protected set; } = true;
/// <summary>
/// Gets or sets a boolean indicating whether adding missing LEDs defined in a layout but missing on the device is
/// supported
/// <para>Note: Defaults to <see langword="true" />.</para>
/// </summary>
public bool CreateMissingLedsSupported { get; protected set; } = true;
/// <summary>
/// Gets or sets a boolean indicating whether removing excess LEDs present in the device but missing in the layout is supported
/// <para>Note: Defaults to <see langword="true" />.</para>
/// </summary>
public bool RemoveExcessiveLedsSupported { get; protected set; } = true;
/// <summary>
/// Gets or sets a boolean indicating whether removing excess LEDs present in the device but missing in the layout is
/// supported
/// <para>Note: Defaults to <see langword="true" />.</para>
/// </summary>
public bool RemoveExcessiveLedsSupported { get; protected set; } = true;
/// <summary>
/// Loads a layout for the specified device and wraps it in an <see cref="ArtemisLayout" />
/// </summary>
/// <param name="device">The device to load the layout for</param>
/// <returns>The resulting Artemis layout</returns>
public virtual ArtemisLayout LoadLayout(ArtemisDevice device)
{
string layoutDir = Path.Combine(Plugin.Directory.FullName, "Layouts");
string filePath = Path.Combine(
layoutDir,
device.RgbDevice.DeviceInfo.Manufacturer,
device.DeviceType.ToString(),
GetDeviceLayoutName(device)
);
return new ArtemisLayout(filePath, LayoutSource.Plugin);
}
/// <summary>
/// Loads a layout for the specified device and wraps it in an <see cref="ArtemisLayout" />
/// </summary>
/// <param name="device">The device to load the layout for</param>
/// <returns>The resulting Artemis layout</returns>
public virtual ArtemisLayout LoadLayout(ArtemisDevice device)
{
string layoutDir = Path.Combine(Plugin.Directory.FullName, "Layouts");
string filePath = Path.Combine(
layoutDir,
device.RgbDevice.DeviceInfo.Manufacturer,
device.DeviceType.ToString(),
GetDeviceLayoutName(device)
);
return new ArtemisLayout(filePath, LayoutSource.Plugin);
}
/// <summary>
/// Loads a layout from the user layout folder for the specified device and wraps it in an <see cref="ArtemisLayout" />
/// </summary>
/// <param name="device">The device to load the layout for</param>
/// <returns>The resulting Artemis layout</returns>
public virtual ArtemisLayout LoadUserLayout(ArtemisDevice device)
{
string layoutDir = Constants.LayoutsFolder;
string filePath = Path.Combine(
layoutDir,
device.RgbDevice.DeviceInfo.Manufacturer,
device.DeviceType.ToString(),
GetDeviceLayoutName(device)
);
return new ArtemisLayout(filePath, LayoutSource.User);
}
/// <summary>
/// Loads a layout from the user layout folder for the specified device and wraps it in an <see cref="ArtemisLayout" />
/// </summary>
/// <param name="device">The device to load the layout for</param>
/// <returns>The resulting Artemis layout</returns>
public virtual ArtemisLayout LoadUserLayout(ArtemisDevice device)
{
string layoutDir = Constants.LayoutsFolder;
string filePath = Path.Combine(
layoutDir,
device.RgbDevice.DeviceInfo.Manufacturer,
device.DeviceType.ToString(),
GetDeviceLayoutName(device)
);
return new ArtemisLayout(filePath, LayoutSource.User);
}
/// <summary>
/// Called when a specific RGB device's logical and physical layout must be detected
/// <para>
/// Note: Only called when <see cref="CanDetectLogicalLayout" /> is <see langword="true" />.
/// </para>
/// </summary>
/// <param name="keyboard">The device to detect the layout for, always a keyboard</param>
public virtual string GetLogicalLayout(IKeyboard keyboard)
{
throw new NotImplementedException("Device provider does not support detecting logical layouts (don't call base.GetLogicalLayout())");
}
/// <summary>
/// Called when a specific RGB device's logical and physical layout must be detected
/// <para>
/// Note: Only called when <see cref="CanDetectLogicalLayout" /> is <see langword="true" />.
/// </para>
/// </summary>
/// <param name="keyboard">The device to detect the layout for, always a keyboard</param>
public virtual string GetLogicalLayout(IKeyboard keyboard)
{
throw new NotImplementedException("Device provider does not support detecting logical layouts (don't call base.GetLogicalLayout())");
}
/// <summary>
/// Called when determining which file name to use when loading the layout of the specified <paramref name="device"></paramref>.
/// </summary>
/// <param name="device">The device to determine the layout file name for.</param>
/// <returns>A file name, including an extension</returns>
public virtual string GetDeviceLayoutName(ArtemisDevice device)
{
// Take out invalid file name chars, may not be perfect but neither are you
string fileName = Path.GetInvalidFileNameChars().Aggregate(device.RgbDevice.DeviceInfo.Model, (current, c) => current.Replace(c, '-'));
if (device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard)
fileName = $"{fileName}-{device.PhysicalLayout.ToString().ToUpper()}";
/// <summary>
/// Called when determining which file name to use when loading the layout of the specified
/// <paramref name="device"></paramref>.
/// </summary>
/// <param name="device">The device to determine the layout file name for.</param>
/// <returns>A file name, including an extension</returns>
public virtual string GetDeviceLayoutName(ArtemisDevice device)
{
// Take out invalid file name chars, may not be perfect but neither are you
string fileName = Path.GetInvalidFileNameChars().Aggregate(device.RgbDevice.DeviceInfo.Model, (current, c) => current.Replace(c, '-'));
if (device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard)
fileName = $"{fileName}-{device.PhysicalLayout.ToString().ToUpper()}";
return fileName + ".xml";
}
return fileName + ".xml";
}
}
@@ -1,15 +1,14 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a configuration dialog for a <see cref="Plugin" />
/// </summary>
public interface IPluginConfigurationDialog
{
/// <summary>
/// Represents a configuration dialog for a <see cref="Plugin" />
/// The type of view model the tab contains
/// </summary>
public interface IPluginConfigurationDialog
{
/// <summary>
/// The type of view model the tab contains
/// </summary>
Type Type { get; }
}
Type Type { get; }
}
@@ -1,10 +1,8 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a view model for a plugin configuration window
/// </summary>
public interface IPluginConfigurationViewModel
{
/// <summary>
/// Represents a view model for a plugin configuration window
/// </summary>
public interface IPluginConfigurationViewModel
{
}
}
@@ -1,9 +1,8 @@
namespace Artemis.Core.LayerBrushes
namespace Artemis.Core.LayerBrushes;
/// <summary>
/// Represents the configuration dialog of a layer brush
/// </summary>
public interface ILayerBrushConfigurationDialog
{
/// <summary>
/// Represents the configuration dialog of a layer brush
/// </summary>
public interface ILayerBrushConfigurationDialog
{
}
}
@@ -1,28 +1,27 @@
namespace Artemis.Core.LayerBrushes
namespace Artemis.Core.LayerBrushes;
/// <summary>
/// Represents a brush preset for a brush.
/// </summary>
public interface ILayerBrushPreset
{
/// <summary>
/// Represents a brush preset for a brush.
/// Gets the name of the preset
/// </summary>
public interface ILayerBrushPreset
{
/// <summary>
/// Gets the name of the preset
/// </summary>
string Name { get; }
string Name { get; }
/// <summary>
/// Gets the description of the preset
/// </summary>
string Description { get; }
/// <summary>
/// Gets the description of the preset
/// </summary>
string Description { get; }
/// <summary>
/// Gets the icon of the preset
/// </summary>
string Icon { get; }
/// <summary>
/// Gets the icon of the preset
/// </summary>
string Icon { get; }
/// <summary>
/// Applies the preset to the layer brush
/// </summary>
void Apply();
}
/// <summary>
/// Applies the preset to the layer brush
/// </summary>
void Apply();
}
@@ -1,220 +1,218 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
namespace Artemis.Core.LayerBrushes
namespace Artemis.Core.LayerBrushes;
/// <summary>
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead
/// </summary>
public abstract class BaseLayerBrush : BreakableModel, IDisposable
{
private LayerBrushType _brushType;
private ILayerBrushConfigurationDialog? _configurationDialog;
private LayerBrushDescriptor _descriptor;
private Layer _layer;
private bool _supportsTransformation = true;
/// <summary>
/// Creates a new instance of the <see cref="BaseLayerBrush" /> class
/// </summary>
protected BaseLayerBrush()
{
// Both are set right after construction to keep the constructor of inherited classes clean
_layer = null!;
_descriptor = null!;
LayerBrushEntity = null!;
}
/// <summary>
/// Gets the layer this brush is applied to
/// </summary>
public Layer Layer
{
get => _layer;
internal set => SetAndNotify(ref _layer, value);
}
/// <summary>
/// Gets the brush entity this brush uses for persistent storage
/// </summary>
public LayerBrushEntity LayerBrushEntity { get; internal set; }
/// <summary>
/// Gets the descriptor of this brush
/// </summary>
public LayerBrushDescriptor Descriptor
{
get => _descriptor;
internal set => SetAndNotify(ref _descriptor, value);
}
/// <summary>
/// Gets or sets a configuration dialog complementing the regular properties
/// </summary>
public ILayerBrushConfigurationDialog? ConfigurationDialog
{
get => _configurationDialog;
protected set => SetAndNotify(ref _configurationDialog, value);
}
/// <summary>
/// Gets the type of layer brush
/// </summary>
public LayerBrushType BrushType
{
get => _brushType;
internal set => SetAndNotify(ref _brushType, value);
}
/// <summary>
/// Gets the ID of the <see cref="LayerBrushProvider" /> that provided this effect
/// </summary>
public string? ProviderId => Descriptor?.Provider.Id;
/// <summary>
/// Gets a reference to the layer property group without knowing it's type
/// </summary>
public virtual LayerPropertyGroup? BaseProperties => null;
/// <summary>
/// Gets a list of presets available to this layer brush
/// </summary>
public virtual List<ILayerBrushPreset>? Presets => null;
/// <summary>
/// Gets the default preset used for new instances of this layer brush
/// </summary>
public virtual ILayerBrushPreset? DefaultPreset => Presets?.FirstOrDefault();
/// <summary>
/// Gets a boolean indicating whether the layer brush is enabled or not
/// </summary>
public bool Enabled { get; private set; }
/// <summary>
/// Gets or sets whether the brush supports transformations
/// <para>Note: RGB.NET brushes can never be transformed and setting this to true will throw an exception</para>
/// </summary>
public bool SupportsTransformation
{
get => _supportsTransformation;
protected set
{
if (value && BrushType == LayerBrushType.RgbNet)
throw new ArtemisPluginFeatureException(Descriptor?.Provider!, "An RGB.NET brush cannot support transformation");
_supportsTransformation = value;
}
}
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => Descriptor.DisplayName;
#endregion
/// <summary>
/// Called when the layer brush is activated
/// </summary>
public abstract void EnableLayerBrush();
/// <summary>
/// Called when the layer brush is deactivated
/// </summary>
public abstract void DisableLayerBrush();
/// <summary>
/// Called before rendering every frame, write your update logic here
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public abstract void Update(double deltaTime);
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
DisableLayerBrush();
BaseProperties?.Dispose();
}
}
internal void InternalUpdate(Timeline timeline)
{
BaseProperties?.Update(timeline);
TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update");
}
/// <summary>
/// Enables the layer brush if it isn't already enabled
/// </summary>
internal void InternalEnable()
{
if (Enabled)
return;
if (!TryOrBreak(EnableLayerBrush, "Failed to enable"))
return;
Enabled = true;
}
/// <summary>
/// Disables the layer brush if it isn't already disabled
/// </summary>
internal void InternalDisable()
{
if (!Enabled)
return;
DisableLayerBrush();
Enabled = false;
}
// Not only is this needed to initialize properties on the layer brushes, it also prevents implementing anything
// but LayerBrush<T> and RgbNetLayerBrush<T> outside the core
internal abstract void Initialize();
internal abstract void InternalRender(SKCanvas canvas, SKRect path, SKPaint paint);
internal void Save()
{
// No need to update the type or provider ID, they're set once by the LayerBrushDescriptors CreateInstance and can't change
BaseProperties?.ApplyToEntity();
LayerBrushEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Describes the type of a layer brush
/// </summary>
public enum LayerBrushType
{
/// <summary>
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead
/// A regular brush that users Artemis' SkiaSharp-based rendering engine
/// </summary>
public abstract class BaseLayerBrush : BreakableModel, IDisposable
{
private LayerBrushType _brushType;
private ILayerBrushConfigurationDialog? _configurationDialog;
private LayerBrushDescriptor _descriptor;
private Layer _layer;
private bool _supportsTransformation = true;
/// <summary>
/// Creates a new instance of the <see cref="BaseLayerBrush" /> class
/// </summary>
protected BaseLayerBrush()
{
// Both are set right after construction to keep the constructor of inherited classes clean
_layer = null!;
_descriptor = null!;
LayerBrushEntity = null!;
}
/// <summary>
/// Gets the layer this brush is applied to
/// </summary>
public Layer Layer
{
get => _layer;
internal set => SetAndNotify(ref _layer, value);
}
/// <summary>
/// Gets the brush entity this brush uses for persistent storage
/// </summary>
public LayerBrushEntity LayerBrushEntity { get; internal set; }
/// <summary>
/// Gets the descriptor of this brush
/// </summary>
public LayerBrushDescriptor Descriptor
{
get => _descriptor;
internal set => SetAndNotify(ref _descriptor, value);
}
/// <summary>
/// Gets or sets a configuration dialog complementing the regular properties
/// </summary>
public ILayerBrushConfigurationDialog? ConfigurationDialog
{
get => _configurationDialog;
protected set => SetAndNotify(ref _configurationDialog, value);
}
/// <summary>
/// Gets the type of layer brush
/// </summary>
public LayerBrushType BrushType
{
get => _brushType;
internal set => SetAndNotify(ref _brushType, value);
}
/// <summary>
/// Gets the ID of the <see cref="LayerBrushProvider" /> that provided this effect
/// </summary>
public string? ProviderId => Descriptor?.Provider.Id;
/// <summary>
/// Gets a reference to the layer property group without knowing it's type
/// </summary>
public virtual LayerPropertyGroup? BaseProperties => null;
/// <summary>
/// Gets a list of presets available to this layer brush
/// </summary>
public virtual List<ILayerBrushPreset>? Presets => null;
/// <summary>
/// Gets the default preset used for new instances of this layer brush
/// </summary>
public virtual ILayerBrushPreset? DefaultPreset => Presets?.FirstOrDefault();
/// <summary>
/// Gets a boolean indicating whether the layer brush is enabled or not
/// </summary>
public bool Enabled { get; private set; }
/// <summary>
/// Gets or sets whether the brush supports transformations
/// <para>Note: RGB.NET brushes can never be transformed and setting this to true will throw an exception</para>
/// </summary>
public bool SupportsTransformation
{
get => _supportsTransformation;
protected set
{
if (value && BrushType == LayerBrushType.RgbNet)
throw new ArtemisPluginFeatureException(Descriptor?.Provider!, "An RGB.NET brush cannot support transformation");
_supportsTransformation = value;
}
}
/// <summary>
/// Called when the layer brush is activated
/// </summary>
public abstract void EnableLayerBrush();
/// <summary>
/// Called when the layer brush is deactivated
/// </summary>
public abstract void DisableLayerBrush();
/// <summary>
/// Called before rendering every frame, write your update logic here
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public abstract void Update(double deltaTime);
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
DisableLayerBrush();
BaseProperties?.Dispose();
}
}
internal void InternalUpdate(Timeline timeline)
{
BaseProperties?.Update(timeline);
TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update");
}
/// <summary>
/// Enables the layer brush if it isn't already enabled
/// </summary>
internal void InternalEnable()
{
if (Enabled)
return;
if (!TryOrBreak(EnableLayerBrush, "Failed to enable"))
return;
Enabled = true;
}
/// <summary>
/// Disables the layer brush if it isn't already disabled
/// </summary>
internal void InternalDisable()
{
if (!Enabled)
return;
DisableLayerBrush();
Enabled = false;
}
// Not only is this needed to initialize properties on the layer brushes, it also prevents implementing anything
// but LayerBrush<T> and RgbNetLayerBrush<T> outside the core
internal abstract void Initialize();
internal abstract void InternalRender(SKCanvas canvas, SKRect path, SKPaint paint);
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => Descriptor.DisplayName;
#endregion
internal void Save()
{
// No need to update the type or provider ID, they're set once by the LayerBrushDescriptors CreateInstance and can't change
BaseProperties?.ApplyToEntity();
LayerBrushEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity;
}
}
Regular,
/// <summary>
/// Describes the type of a layer brush
/// An RGB.NET brush that uses RGB.NET's per-LED rendering engine.
/// </summary>
public enum LayerBrushType
{
/// <summary>
/// A regular brush that users Artemis' SkiaSharp-based rendering engine
/// </summary>
Regular,
/// <summary>
/// An RGB.NET brush that uses RGB.NET's per-LED rendering engine.
/// </summary>
RgbNet
}
RgbNet
}
@@ -1,44 +1,42 @@
using System;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core.LayerBrushes
namespace Artemis.Core.LayerBrushes;
/// <summary>
/// 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, new()
{
private T _properties = null!;
/// <summary>
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead
/// Gets whether all properties on this brush are initialized
/// </summary>
public abstract class PropertiesLayerBrush<T> : BaseLayerBrush where T : LayerPropertyGroup, new()
public bool PropertiesInitialized { get; internal set; }
/// <inheritdoc />
public override LayerPropertyGroup BaseProperties => Properties;
/// <summary>
/// Gets the properties of this brush.
/// </summary>
public T Properties
{
private T _properties = null!;
/// <summary>
/// Gets whether all properties on this brush are initialized
/// </summary>
public bool PropertiesInitialized { get; internal set; }
/// <inheritdoc />
public override LayerPropertyGroup BaseProperties => Properties;
/// <summary>
/// Gets the properties of this brush.
/// </summary>
public T Properties
get
{
get
{
// I imagine a null reference here can be confusing, so lets throw an exception explaining what to do
if (_properties == null)
throw new InvalidOperationException("Cannot access brush properties until OnPropertiesInitialized has been called");
return _properties;
}
internal set => _properties = value;
// I imagine a null reference here can be confusing, so lets throw an exception explaining what to do
if (_properties == null)
throw new InvalidOperationException("Cannot access brush properties until OnPropertiesInitialized has been called");
return _properties;
}
internal set => _properties = value;
}
internal void InitializeProperties()
{
Properties = new T();
PropertyGroupDescriptionAttribute groupDescription = new() {Identifier = "Brush", Name = Descriptor.DisplayName, Description = Descriptor.Description};
Properties.Initialize(Layer, null, groupDescription, LayerBrushEntity.PropertyGroup);
PropertiesInitialized = true;
}
internal void InitializeProperties()
{
Properties = new T();
PropertyGroupDescriptionAttribute groupDescription = new() {Identifier = "Brush", Name = Descriptor.DisplayName, Description = Descriptor.Description};
Properties.Initialize(Layer, null, groupDescription, LayerBrushEntity.PropertyGroup);
PropertiesInitialized = true;
}
}
@@ -1,39 +1,38 @@
using SkiaSharp;
namespace Artemis.Core.LayerBrushes
namespace Artemis.Core.LayerBrushes;
/// <summary>
/// Represents a brush that renders on a layer
/// </summary>
/// <typeparam name="T">The type of brush properties</typeparam>
public abstract class LayerBrush<T> : PropertiesLayerBrush<T> where T : LayerPropertyGroup, new()
{
/// <summary>
/// Represents a brush that renders on a layer
/// Creates a new instance of the <see cref="LayerBrush{T}" /> class
/// </summary>
/// <typeparam name="T">The type of brush properties</typeparam>
public abstract class LayerBrush<T> : PropertiesLayerBrush<T> where T : LayerPropertyGroup, new()
protected LayerBrush()
{
/// <summary>
/// Creates a new instance of the <see cref="LayerBrush{T}" /> class
/// </summary>
protected LayerBrush()
{
BrushType = LayerBrushType.Regular;
}
BrushType = LayerBrushType.Regular;
}
/// <summary>
/// The main method of rendering anything to the layer. The provided <see cref="SKCanvas" /> is specific to the layer
/// and matches it's width and height.
/// <para>Called during rendering or layer preview, in the order configured on the layer</para>
/// </summary>
/// <param name="canvas">The layer canvas</param>
/// <param name="bounds">The area to be filled, covers the shape</param>
/// <param name="paint">The paint to be used to fill the shape</param>
public abstract void Render(SKCanvas canvas, SKRect bounds, SKPaint paint);
/// <summary>
/// The main method of rendering anything to the layer. The provided <see cref="SKCanvas" /> is specific to the layer
/// and matches it's width and height.
/// <para>Called during rendering or layer preview, in the order configured on the layer</para>
/// </summary>
/// <param name="canvas">The layer canvas</param>
/// <param name="bounds">The area to be filled, covers the shape</param>
/// <param name="paint">The paint to be used to fill the shape</param>
public abstract void Render(SKCanvas canvas, SKRect bounds, SKPaint paint);
internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
TryOrBreak(() => Render(canvas, bounds, paint), "Failed to render");
}
internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
TryOrBreak(() => Render(canvas, bounds, paint), "Failed to render");
}
internal override void Initialize()
{
TryOrBreak(InitializeProperties, "Failed to initialize");
}
internal override void Initialize()
{
TryOrBreak(InitializeProperties, "Failed to initialize");
}
}
@@ -2,74 +2,73 @@
using Artemis.Storage.Entities.Profile;
using Ninject;
namespace Artemis.Core.LayerBrushes
namespace Artemis.Core.LayerBrushes;
/// <summary>
/// A class that describes a layer brush
/// </summary>
public class LayerBrushDescriptor
{
/// <summary>
/// A class that describes a layer brush
/// </summary>
public class LayerBrushDescriptor
internal LayerBrushDescriptor(string displayName, string description, string icon, Type layerBrushType, LayerBrushProvider provider)
{
internal LayerBrushDescriptor(string displayName, string description, string icon, Type layerBrushType, LayerBrushProvider provider)
{
DisplayName = displayName;
Description = description;
Icon = icon;
LayerBrushType = layerBrushType;
Provider = provider;
}
DisplayName = displayName;
Description = description;
Icon = icon;
LayerBrushType = layerBrushType;
Provider = provider;
}
/// <summary>
/// The name that is displayed in the UI
/// </summary>
public string DisplayName { get; }
/// <summary>
/// The name that is displayed in the UI
/// </summary>
public string DisplayName { get; }
/// <summary>
/// The description that is displayed in the UI
/// </summary>
public string Description { get; }
/// <summary>
/// The description that is displayed in the UI
/// </summary>
public string Description { get; }
/// <summary>
/// The Material icon to display in the UI, a full reference can be found
/// <see href="https://materialdesignicons.com">here</see>
/// </summary>
public string Icon { get; }
/// <summary>
/// The Material icon to display in the UI, a full reference can be found
/// <see href="https://materialdesignicons.com">here</see>
/// </summary>
public string Icon { get; }
/// <summary>
/// The type of the layer brush
/// </summary>
public Type LayerBrushType { get; }
/// <summary>
/// The type of the layer brush
/// </summary>
public Type LayerBrushType { get; }
/// <summary>
/// The plugin that provided this <see cref="LayerBrushDescriptor" />
/// </summary>
public LayerBrushProvider Provider { get; }
/// <summary>
/// The plugin that provided this <see cref="LayerBrushDescriptor" />
/// </summary>
public LayerBrushProvider Provider { get; }
/// <summary>
/// Determines whether the provided <paramref name="reference" /> references to a brush provided by this descriptor
/// </summary>
public bool MatchesLayerBrushReference(LayerBrushReference? reference)
{
if (reference == null)
return false;
/// <summary>
/// Determines whether the provided <paramref name="reference" /> references to a brush provided by this descriptor
/// </summary>
public bool MatchesLayerBrushReference(LayerBrushReference? reference)
{
if (reference == null)
return false;
return Provider.Id == reference.LayerBrushProviderId && LayerBrushType.Name == reference.BrushType;
}
return Provider.Id == reference.LayerBrushProviderId && LayerBrushType.Name == reference.BrushType;
}
/// <summary>
/// Creates an instance of the described brush and applies it to the layer
/// </summary>
public BaseLayerBrush CreateInstance(Layer layer, LayerBrushEntity? entity)
{
if (layer == null)
throw new ArgumentNullException(nameof(layer));
/// <summary>
/// Creates an instance of the described brush and applies it to the layer
/// </summary>
public BaseLayerBrush CreateInstance(Layer layer, LayerBrushEntity? entity)
{
if (layer == null)
throw new ArgumentNullException(nameof(layer));
BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType);
brush.Layer = layer;
brush.Descriptor = this;
brush.LayerBrushEntity = entity ?? new LayerBrushEntity { ProviderId = Provider.Id, BrushType = LayerBrushType.FullName };
brush.Initialize();
return brush;
}
BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType);
brush.Layer = layer;
brush.Descriptor = this;
brush.LayerBrushEntity = entity ?? new LayerBrushEntity {ProviderId = Provider.Id, BrushType = LayerBrushType.FullName};
brush.Initialize();
return brush;
}
}
@@ -1,59 +1,57 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
namespace Artemis.Core.LayerBrushes
namespace Artemis.Core.LayerBrushes;
/// <summary>
/// Allows you to create one or more <see cref="LayerBrush{T}" />s usable by profile layers.
/// </summary>
public abstract class LayerBrushProvider : PluginFeature
{
private readonly List<LayerBrushDescriptor> _layerBrushDescriptors;
/// <summary>
/// Allows you to create one or more <see cref="LayerBrush{T}" />s usable by profile layers.
/// Allows you to register one or more <see cref="LayerBrush{T}" />s usable by profile layers.
/// </summary>
public abstract class LayerBrushProvider : PluginFeature
protected LayerBrushProvider()
{
private readonly List<LayerBrushDescriptor> _layerBrushDescriptors;
_layerBrushDescriptors = new List<LayerBrushDescriptor>();
LayerBrushDescriptors = new ReadOnlyCollection<LayerBrushDescriptor>(_layerBrushDescriptors);
Disabled += OnDisabled;
}
/// <summary>
/// Allows you to register one or more <see cref="LayerBrush{T}" />s usable by profile layers.
/// </summary>
protected LayerBrushProvider()
{
_layerBrushDescriptors = new List<LayerBrushDescriptor>();
LayerBrushDescriptors = new ReadOnlyCollection<LayerBrushDescriptor>(_layerBrushDescriptors);
Disabled += OnDisabled;
}
/// <summary>
/// A read-only collection of all layer brushes added with <see cref="RegisterLayerBrushDescriptor{T}" />
/// </summary>
public ReadOnlyCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; }
/// <summary>
/// A read-only collection of all layer brushes added with <see cref="RegisterLayerBrushDescriptor{T}" />
/// </summary>
public ReadOnlyCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; }
/// <summary>
/// Registers a layer brush descriptor for a given layer brush, so that it appears in the UI.
/// <para>Note: You do not need to manually remove these on disable</para>
/// </summary>
/// <typeparam name="T">The type of the layer brush you wish to register</typeparam>
/// <param name="displayName">The name to display in the UI</param>
/// <param name="description">The description to display in the UI</param>
/// <param name="icon">
/// The Material icon to display in the UI, a full reference can be found
/// <see href="https://materialdesignicons.com">here</see>
/// </param>
protected void RegisterLayerBrushDescriptor<T>(string displayName, string description, string icon) where T : BaseLayerBrush
{
if (!IsEnabled)
throw new ArtemisPluginException(Plugin, "Can only add a layer brush descriptor when the plugin is enabled");
/// <summary>
/// Registers a layer brush descriptor for a given layer brush, so that it appears in the UI.
/// <para>Note: You do not need to manually remove these on disable</para>
/// </summary>
/// <typeparam name="T">The type of the layer brush you wish to register</typeparam>
/// <param name="displayName">The name to display in the UI</param>
/// <param name="description">The description to display in the UI</param>
/// <param name="icon">
/// The Material icon to display in the UI, a full reference can be found
/// <see href="https://materialdesignicons.com">here</see>
/// </param>
protected void RegisterLayerBrushDescriptor<T>(string displayName, string description, string icon) where T : BaseLayerBrush
{
if (!IsEnabled)
throw new ArtemisPluginException(Plugin, "Can only add a layer brush descriptor when the plugin is enabled");
if (icon.Contains('.'))
icon = Plugin.ResolveRelativePath(icon);
LayerBrushDescriptor descriptor = new(displayName, description, icon, typeof(T), this);
_layerBrushDescriptors.Add(descriptor);
LayerBrushStore.Add(descriptor);
}
if (icon.Contains('.'))
icon = Plugin.ResolveRelativePath(icon);
LayerBrushDescriptor descriptor = new(displayName, description, icon, typeof(T), this);
_layerBrushDescriptors.Add(descriptor);
LayerBrushStore.Add(descriptor);
}
private void OnDisabled(object? sender, EventArgs e)
{
// The store will clean up the registrations by itself, the plugin just needs to clear its own list
_layerBrushDescriptors.Clear();
}
private void OnDisabled(object? sender, EventArgs e)
{
// The store will clean up the registrations by itself, the plugin just needs to clear its own list
_layerBrushDescriptors.Clear();
}
}
@@ -1,84 +1,80 @@
using System;
using SkiaSharp;
using SkiaSharp;
namespace Artemis.Core.LayerBrushes
namespace Artemis.Core.LayerBrushes;
/// <summary>
/// Represents a brush that renders on a per-layer basis
/// </summary>
/// <typeparam name="T">The type of brush properties</typeparam>
public abstract class PerLedLayerBrush<T> : PropertiesLayerBrush<T> where T : LayerPropertyGroup, new()
{
/// <summary>
/// Represents a brush that renders on a per-layer basis
/// Creates a new instance of the <see cref="PerLedLayerBrush{T}" /> class
/// </summary>
/// <typeparam name="T">The type of brush properties</typeparam>
public abstract class PerLedLayerBrush<T> : PropertiesLayerBrush<T> where T : LayerPropertyGroup, new()
protected PerLedLayerBrush()
{
/// <summary>
/// Creates a new instance of the <see cref="PerLedLayerBrush{T}" /> class
/// </summary>
protected PerLedLayerBrush()
BrushType = LayerBrushType.Regular;
}
/// <summary>
/// The main method of rendering for this type of brush. Called once per frame for each LED in the layer
/// <para>
/// Note: Due to transformations, the render point may not match the position of the LED, always use the render
/// point to determine where the color will go.
/// </para>
/// </summary>
/// <param name="led">The LED that will receive the color</param>
/// <param name="renderPoint">The point at which the color is located</param>
/// <returns>The color the LED will receive</returns>
public abstract SKColor GetColor(ArtemisLed led, SKPoint renderPoint);
internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
// We don't want rotation on this canvas because that'll displace the LEDs, translations are applied to the points of each LED instead
if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation)
canvas.SetMatrix(canvas.TotalMatrix.PreConcat(Layer.GetTransformMatrix(true, false, false, true).Invert()));
using SKPath pointsPath = new();
foreach (ArtemisLed artemisLed in Layer.Leds)
{
BrushType = LayerBrushType.Regular;
pointsPath.AddPoly(new[]
{
new SKPoint(0, 0),
new SKPoint(artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left, artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top)
});
}
/// <summary>
/// The main method of rendering for this type of brush. Called once per frame for each LED in the layer
/// <para>
/// Note: Due to transformations, the render point may not match the position of the LED, always use the render
/// point to determine where the color will go.
/// </para>
/// </summary>
/// <param name="led">The LED that will receive the color</param>
/// <param name="renderPoint">The point at which the color is located</param>
/// <returns>The color the LED will receive</returns>
public abstract SKColor GetColor(ArtemisLed led, SKPoint renderPoint);
// Apply the translation to the points of each LED instead
if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation)
pointsPath.Transform(Layer.GetTransformMatrix(true, true, true, true).Invert());
internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint)
SKPoint[] points = pointsPath.Points;
TryOrBreak(() =>
{
// We don't want rotation on this canvas because that'll displace the LEDs, translations are applied to the points of each LED instead
if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation)
canvas.SetMatrix(canvas.TotalMatrix.PreConcat(Layer.GetTransformMatrix(true, false, false, true).Invert()));
using SKPath pointsPath = new();
foreach (ArtemisLed artemisLed in Layer.Leds)
for (int index = 0; index < Layer.Leds.Count; index++)
{
pointsPath.AddPoly(new[]
{
new SKPoint(0, 0),
new SKPoint(artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left, artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top)
});
ArtemisLed artemisLed = Layer.Leds[index];
SKPoint renderPoint = points[index * 2 + 1];
if (!float.IsFinite(renderPoint.X) || !float.IsFinite(renderPoint.Y))
continue;
// Let the brush determine the color
paint.Color = GetColor(artemisLed, renderPoint);
SKRect ledRectangle = SKRect.Create(
artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left,
artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top,
artemisLed.AbsoluteRectangle.Width,
artemisLed.AbsoluteRectangle.Height
);
canvas.DrawRect(ledRectangle, paint);
}
}, "Failed to render");
}
// Apply the translation to the points of each LED instead
if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation)
pointsPath.Transform(Layer.GetTransformMatrix(true, true, true, true).Invert());
SKPoint[] points = pointsPath.Points;
TryOrBreak(() =>
{
for (int index = 0; index < Layer.Leds.Count; index++)
{
ArtemisLed artemisLed = Layer.Leds[index];
SKPoint renderPoint = points[index * 2 + 1];
if (!float.IsFinite(renderPoint.X) || !float.IsFinite(renderPoint.Y))
continue;
// Let the brush determine the color
paint.Color = GetColor(artemisLed, renderPoint);
SKRect ledRectangle = SKRect.Create(
artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left,
artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top,
artemisLed.AbsoluteRectangle.Width,
artemisLed.AbsoluteRectangle.Height
);
canvas.DrawRect(ledRectangle, paint);
}
}, "Failed to render");
}
internal override void Initialize()
{
TryOrBreak(InitializeProperties, "Failed to initialize");
}
internal override void Initialize()
{
TryOrBreak(InitializeProperties, "Failed to initialize");
}
}
@@ -1,9 +1,8 @@
namespace Artemis.Core.LayerEffects
namespace Artemis.Core.LayerEffects;
/// <summary>
/// Represents a configuration dialog for a <see cref="LayerEffect{T}" />
/// </summary>
public interface ILayerEffectConfigurationDialog
{
/// <summary>
/// Represents a configuration dialog for a <see cref="LayerEffect{T}" />
/// </summary>
public interface ILayerEffectConfigurationDialog
{
}
}
@@ -2,241 +2,239 @@
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
namespace Artemis.Core.LayerEffects
namespace Artemis.Core.LayerEffects;
/// <summary>
/// For internal use only, please use <see cref="LayerEffect{T}" /> instead
/// </summary>
public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel
{
/// <summary>
/// For internal use only, please use <see cref="LayerEffect{T}" /> instead
/// </summary>
public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel
private ILayerEffectConfigurationDialog? _configurationDialog;
private LayerEffectDescriptor _descriptor;
private bool _hasBeenRenamed;
private string _name;
private int _order;
private RenderProfileElement _profileElement;
/// <inheritdoc />
protected BaseLayerEffect()
{
private ILayerEffectConfigurationDialog? _configurationDialog;
private LayerEffectDescriptor _descriptor;
private bool _hasBeenRenamed;
private string _name;
private int _order;
private RenderProfileElement _profileElement;
// These are set right after construction to keep the constructor of inherited classes clean
_profileElement = null!;
_descriptor = null!;
_name = null!;
LayerEffectEntity = null!;
}
/// <inheritdoc />
protected BaseLayerEffect()
/// <summary>
/// Gets the
/// </summary>
public LayerEffectEntity LayerEffectEntity { get; internal set; }
/// <summary>
/// Gets the profile element (such as layer or folder) this effect is applied to
/// </summary>
public RenderProfileElement ProfileElement
{
get => _profileElement;
internal set => SetAndNotify(ref _profileElement, value);
}
/// <summary>
/// The name which appears in the editor
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets whether the effect has been renamed by the user, if true consider refraining from changing the name
/// programatically
/// </summary>
public bool HasBeenRenamed
{
get => _hasBeenRenamed;
set => SetAndNotify(ref _hasBeenRenamed, value);
}
/// <summary>
/// Gets the order in which this effect appears in the update loop and editor
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets the <see cref="LayerEffectDescriptor" /> that registered this effect
/// </summary>
public LayerEffectDescriptor Descriptor
{
get => _descriptor;
internal set => SetAndNotify(ref _descriptor, value);
}
/// <summary>
/// Gets or sets a configuration dialog complementing the regular properties
/// </summary>
public ILayerEffectConfigurationDialog? ConfigurationDialog
{
get => _configurationDialog;
protected set => SetAndNotify(ref _configurationDialog, value);
}
/// <summary>
/// Gets the ID of the <see cref="LayerEffectProvider" /> that provided this effect
/// </summary>
public string ProviderId => Descriptor.Provider.Id;
/// <summary>
/// Gets a reference to the layer property group without knowing it's type
/// </summary>
public virtual LayerEffectPropertyGroup? BaseProperties => null;
/// <summary>
/// Gets a boolean indicating whether the layer effect is enabled or not
/// </summary>
public bool Enabled { get; private set; }
/// <summary>
/// Gets a boolean indicating whether the layer effect is suspended or not
/// </summary>
public bool Suspended => BaseProperties is not {PropertiesInitialized: true} || !BaseProperties.IsEnabled;
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => Name;
#endregion
/// <summary>
/// Called when the layer effect is activated
/// </summary>
public abstract void EnableLayerEffect();
/// <summary>
/// Called when the layer effect is deactivated
/// </summary>
public abstract void DisableLayerEffect();
/// <summary>
/// Called before rendering every frame, write your update logic here
/// </summary>
/// <param name="deltaTime"></param>
public abstract void Update(double deltaTime);
/// <summary>
/// Called before the layer or folder will be rendered
/// </summary>
/// <param name="canvas">The canvas used to render the frame</param>
/// <param name="renderBounds">The bounds this layer/folder will render in</param>
/// <param name="paint">The paint this layer/folder will use to render</param>
public abstract void PreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint);
/// <summary>
/// Called after the layer of folder has been rendered
/// </summary>
/// <param name="canvas">The canvas used to render the frame</param>
/// <param name="renderBounds">The bounds this layer/folder rendered in</param>
/// <param name="paint">The paint this layer/folder used to render</param>
public abstract void PostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint);
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// These are set right after construction to keep the constructor of inherited classes clean
_profileElement = null!;
_descriptor = null!;
_name = null!;
LayerEffectEntity = null!;
}
/// <summary>
/// Gets the
/// </summary>
public LayerEffectEntity LayerEffectEntity { get; internal set; }
/// <summary>
/// Gets the profile element (such as layer or folder) this effect is applied to
/// </summary>
public RenderProfileElement ProfileElement
{
get => _profileElement;
internal set => SetAndNotify(ref _profileElement, value);
}
/// <summary>
/// The name which appears in the editor
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets whether the effect has been renamed by the user, if true consider refraining from changing the name
/// programatically
/// </summary>
public bool HasBeenRenamed
{
get => _hasBeenRenamed;
set => SetAndNotify(ref _hasBeenRenamed, value);
}
/// <summary>
/// Gets the order in which this effect appears in the update loop and editor
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets the <see cref="LayerEffectDescriptor" /> that registered this effect
/// </summary>
public LayerEffectDescriptor Descriptor
{
get => _descriptor;
internal set => SetAndNotify(ref _descriptor, value);
}
/// <summary>
/// Gets or sets a configuration dialog complementing the regular properties
/// </summary>
public ILayerEffectConfigurationDialog? ConfigurationDialog
{
get => _configurationDialog;
protected set => SetAndNotify(ref _configurationDialog, value);
}
/// <summary>
/// Gets the ID of the <see cref="LayerEffectProvider"/> that provided this effect
/// </summary>
public string ProviderId => Descriptor.Provider.Id;
/// <summary>
/// Gets a reference to the layer property group without knowing it's type
/// </summary>
public virtual LayerEffectPropertyGroup? BaseProperties => null;
/// <summary>
/// Gets a boolean indicating whether the layer effect is enabled or not
/// </summary>
public bool Enabled { get; private set; }
/// <summary>
/// Gets a boolean indicating whether the layer effect is suspended or not
/// </summary>
public bool Suspended => BaseProperties is not {PropertiesInitialized: true} || !BaseProperties.IsEnabled;
#region IDisposable
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
DisableLayerEffect();
BaseProperties?.Dispose();
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
/// <summary>
/// Called when the layer effect is activated
/// </summary>
public abstract void EnableLayerEffect();
/// <summary>
/// Called when the layer effect is deactivated
/// </summary>
public abstract void DisableLayerEffect();
/// <summary>
/// Called before rendering every frame, write your update logic here
/// </summary>
/// <param name="deltaTime"></param>
public abstract void Update(double deltaTime);
/// <summary>
/// Called before the layer or folder will be rendered
/// </summary>
/// <param name="canvas">The canvas used to render the frame</param>
/// <param name="renderBounds">The bounds this layer/folder will render in</param>
/// <param name="paint">The paint this layer/folder will use to render</param>
public abstract void PreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint);
/// <summary>
/// Called after the layer of folder has been rendered
/// </summary>
/// <param name="canvas">The canvas used to render the frame</param>
/// <param name="renderBounds">The bounds this layer/folder rendered in</param>
/// <param name="paint">The paint this layer/folder used to render</param>
public abstract void PostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint);
// Not only is this needed to initialize properties on the layer effects, it also prevents implementing anything
// but LayerEffect<T> outside the core
internal abstract void Initialize();
internal virtual string GetEffectTypeName() => GetType().Name;
internal void InternalUpdate(Timeline timeline)
{
BaseProperties?.Update(timeline);
TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update");
}
internal void InternalPreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint)
{
TryOrBreak(() => PreProcess(canvas, renderBounds, paint), "Failed to pre-process");
}
internal void InternalPostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint)
{
TryOrBreak(() => PostProcess(canvas, renderBounds, paint), "Failed to post-process");
}
/// <summary>
/// Enables the layer effect if it isn't already enabled
/// </summary>
internal void InternalEnable()
{
if (Enabled)
return;
EnableLayerEffect();
Enabled = true;
}
/// <summary>
/// Disables the layer effect if it isn't already disabled
/// </summary>
internal void InternalDisable()
{
if (!Enabled)
return;
DisableLayerEffect();
Enabled = false;
}
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => Name;
#endregion
/// <inheritdoc />
public void Load()
{
HasBeenRenamed = LayerEffectEntity.HasBeenRenamed;
Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName;
Order = LayerEffectEntity.Order;
}
/// <inheritdoc />
public void Save()
{
LayerEffectEntity.ProviderId = Descriptor.Provider.Id;
LayerEffectEntity.EffectType = GetType().FullName;
LayerEffectEntity.Name = Name;
LayerEffectEntity.HasBeenRenamed = HasBeenRenamed;
LayerEffectEntity.Order = Order;
BaseProperties?.ApplyToEntity();
LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity;
BaseProperties?.Dispose();
}
}
// Not only is this needed to initialize properties on the layer effects, it also prevents implementing anything
// but LayerEffect<T> outside the core
internal abstract void Initialize();
internal virtual string GetEffectTypeName()
{
return GetType().Name;
}
internal void InternalUpdate(Timeline timeline)
{
BaseProperties?.Update(timeline);
TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update");
}
internal void InternalPreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint)
{
TryOrBreak(() => PreProcess(canvas, renderBounds, paint), "Failed to pre-process");
}
internal void InternalPostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint)
{
TryOrBreak(() => PostProcess(canvas, renderBounds, paint), "Failed to post-process");
}
/// <summary>
/// Enables the layer effect if it isn't already enabled
/// </summary>
internal void InternalEnable()
{
if (Enabled)
return;
EnableLayerEffect();
Enabled = true;
}
/// <summary>
/// Disables the layer effect if it isn't already disabled
/// </summary>
internal void InternalDisable()
{
if (!Enabled)
return;
DisableLayerEffect();
Enabled = false;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Load()
{
HasBeenRenamed = LayerEffectEntity.HasBeenRenamed;
Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName;
Order = LayerEffectEntity.Order;
}
/// <inheritdoc />
public void Save()
{
LayerEffectEntity.ProviderId = Descriptor.Provider.Id;
LayerEffectEntity.EffectType = GetType().FullName;
LayerEffectEntity.Name = Name;
LayerEffectEntity.HasBeenRenamed = HasBeenRenamed;
LayerEffectEntity.Order = Order;
BaseProperties?.ApplyToEntity();
LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity;
}
}
@@ -1,53 +1,52 @@
using System;
namespace Artemis.Core.LayerEffects
namespace Artemis.Core.LayerEffects;
/// <summary>
/// Represents an effect that applies preprocessing and/or postprocessing to a layer
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class LayerEffect<T> : BaseLayerEffect where T : LayerEffectPropertyGroup, new()
{
private T _properties = null!;
/// <summary>
/// Represents an effect that applies preprocessing and/or postprocessing to a layer
/// Gets whether all properties on this effect are initialized
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class LayerEffect<T> : BaseLayerEffect where T : LayerEffectPropertyGroup, new()
public bool PropertiesInitialized { get; internal set; }
/// <inheritdoc />
public override LayerEffectPropertyGroup BaseProperties => Properties;
/// <summary>
/// Gets the properties of this effect.
/// </summary>
public T Properties
{
private T _properties = null!;
/// <summary>
/// Gets whether all properties on this effect are initialized
/// </summary>
public bool PropertiesInitialized { get; internal set; }
/// <inheritdoc />
public override LayerEffectPropertyGroup BaseProperties => Properties;
/// <summary>
/// Gets the properties of this effect.
/// </summary>
public T Properties
get
{
get
{
// I imagine a null reference here can be confusing, so lets throw an exception explaining what to do
if (_properties == null)
throw new InvalidOperationException("Cannot access effect properties until OnPropertiesInitialized has been called");
return _properties;
}
internal set => _properties = value;
// I imagine a null reference here can be confusing, so lets throw an exception explaining what to do
if (_properties == null)
throw new InvalidOperationException("Cannot access effect properties until OnPropertiesInitialized has been called");
return _properties;
}
internal override void Initialize()
{
InitializeProperties();
}
private void InitializeProperties()
{
Properties = new T();
Properties.Initialize(ProfileElement, null, new PropertyGroupDescriptionAttribute {Identifier = "LayerEffect"}, LayerEffectEntity.PropertyGroup);
// Initialize will call PopulateDefaults but that is for plugin developers so can't rely on that to default IsEnabled to true
Properties.InitializeIsEnabled();
PropertiesInitialized = true;
internal set => _properties = value;
}
EnableLayerEffect();
}
internal override void Initialize()
{
InitializeProperties();
}
private void InitializeProperties()
{
Properties = new T();
Properties.Initialize(ProfileElement, null, new PropertyGroupDescriptionAttribute {Identifier = "LayerEffect"}, LayerEffectEntity.PropertyGroup);
// Initialize will call PopulateDefaults but that is for plugin developers so can't rely on that to default IsEnabled to true
Properties.InitializeIsEnabled();
PropertiesInitialized = true;
EnableLayerEffect();
}
}
@@ -1,59 +1,58 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
namespace Artemis.Core.LayerEffects
namespace Artemis.Core.LayerEffects;
/// <summary>
/// Allows you to register one or more <see cref="LayerEffect{T}" />s usable by profile layers.
/// </summary>
public abstract class LayerEffectProvider : PluginFeature
{
private readonly List<LayerEffectDescriptor> _layerEffectDescriptors;
/// <summary>
/// Allows you to register one or more <see cref="LayerEffect{T}" />s usable by profile layers.
/// </summary>
public abstract class LayerEffectProvider : PluginFeature
protected LayerEffectProvider()
{
private readonly List<LayerEffectDescriptor> _layerEffectDescriptors;
_layerEffectDescriptors = new List<LayerEffectDescriptor>();
LayerEffectDescriptors = new ReadOnlyCollection<LayerEffectDescriptor>(_layerEffectDescriptors);
Disabled += OnDisabled;
}
/// <summary>
/// Allows you to register one or more <see cref="LayerEffect{T}" />s usable by profile layers.
/// </summary>
protected LayerEffectProvider()
{
_layerEffectDescriptors = new List<LayerEffectDescriptor>();
LayerEffectDescriptors = new ReadOnlyCollection<LayerEffectDescriptor>(_layerEffectDescriptors);
Disabled += OnDisabled;
}
/// <summary>
/// A read-only collection of all layer effects added with <see cref="RegisterLayerEffectDescriptor{T}" />
/// </summary>
public ReadOnlyCollection<LayerEffectDescriptor> LayerEffectDescriptors { get; }
/// <summary>
/// A read-only collection of all layer effects added with <see cref="RegisterLayerEffectDescriptor{T}" />
/// </summary>
public ReadOnlyCollection<LayerEffectDescriptor> LayerEffectDescriptors { get; }
/// <summary>
/// Adds a layer effect descriptor for a given layer effect, so that it appears in the UI.
/// <para>Note: You do not need to manually remove these on disable</para>
/// </summary>
/// <typeparam name="T">The type of the layer effect you wish to register</typeparam>
/// <param name="displayName">The name to display in the UI</param>
/// <param name="description">The description to display in the UI</param>
/// <param name="icon">
/// The Material icon to display in the UI, a full reference can be found
/// <see href="https://materialdesignicons.com">here</see>.
/// <para>May also be a path to an SVG file relative to the directory of the plugin.</para>
/// </param>
protected void RegisterLayerEffectDescriptor<T>(string displayName, string description, string icon) where T : BaseLayerEffect
{
if (!IsEnabled)
throw new ArtemisPluginFeatureException(this, "Can only add a layer effect descriptor when the plugin is enabled");
/// <summary>
/// Adds a layer effect descriptor for a given layer effect, so that it appears in the UI.
/// <para>Note: You do not need to manually remove these on disable</para>
/// </summary>
/// <typeparam name="T">The type of the layer effect you wish to register</typeparam>
/// <param name="displayName">The name to display in the UI</param>
/// <param name="description">The description to display in the UI</param>
/// <param name="icon">
/// The Material icon to display in the UI, a full reference can be found <see href="https://materialdesignicons.com">here</see>.
/// <para>May also be a path to an SVG file relative to the directory of the plugin.</para>
/// </param>
protected void RegisterLayerEffectDescriptor<T>(string displayName, string description, string icon) where T : BaseLayerEffect
{
if (!IsEnabled)
throw new ArtemisPluginFeatureException(this, "Can only add a layer effect descriptor when the plugin is enabled");
if (icon.Contains('.'))
icon = Plugin.ResolveRelativePath(icon);
LayerEffectDescriptor descriptor = new(displayName, description, icon, typeof(T), this);
_layerEffectDescriptors.Add(descriptor);
LayerEffectStore.Add(descriptor);
}
if (icon.Contains('.'))
icon = Plugin.ResolveRelativePath(icon);
LayerEffectDescriptor descriptor = new(displayName, description, icon, typeof(T), this);
_layerEffectDescriptors.Add(descriptor);
LayerEffectStore.Add(descriptor);
}
private void OnDisabled(object? sender, EventArgs e)
{
// The store will clean up the registrations by itself, the plugin just needs to clear its own list
_layerEffectDescriptors.Clear();
}
private void OnDisabled(object? sender, EventArgs e)
{
// The store will clean up the registrations by itself, the plugin just needs to clear its own list
_layerEffectDescriptors.Clear();
}
}
@@ -1,74 +1,73 @@
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
namespace Artemis.Core.LayerEffects.Placeholder
namespace Artemis.Core.LayerEffects.Placeholder;
/// <summary>
/// Represents a layer effect that could not be loaded due to a missing plugin
/// </summary>
internal class PlaceholderLayerEffect : LayerEffect<PlaceholderProperties>
{
/// <summary>
/// Represents a layer effect that could not be loaded due to a missing plugin
/// </summary>
internal class PlaceholderLayerEffect : LayerEffect<PlaceholderProperties>
internal PlaceholderLayerEffect(LayerEffectEntity originalEntity, string placeholderFor)
{
internal PlaceholderLayerEffect(LayerEffectEntity originalEntity, string placeholderFor)
{
OriginalEntity = originalEntity;
PlaceholderFor = placeholderFor;
OriginalEntity = originalEntity;
PlaceholderFor = placeholderFor;
LayerEffectEntity = originalEntity;
Order = OriginalEntity.Order;
Name = OriginalEntity.Name;
HasBeenRenamed = OriginalEntity.HasBeenRenamed;
}
public string PlaceholderFor { get; }
internal LayerEffectEntity OriginalEntity { get; }
/// <inheritdoc />
public override void EnableLayerEffect()
{
}
/// <inheritdoc />
public override void DisableLayerEffect()
{
}
/// <inheritdoc />
public override void Update(double deltaTime)
{
}
/// <inheritdoc />
public override void PreProcess(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
}
/// <inheritdoc />
public override void PostProcess(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
}
internal override string GetEffectTypeName()
{
return OriginalEntity.EffectType;
}
LayerEffectEntity = originalEntity;
Order = OriginalEntity.Order;
Name = OriginalEntity.Name;
HasBeenRenamed = OriginalEntity.HasBeenRenamed;
}
/// <summary>
/// This is in place so that the UI has something to show
/// </summary>
internal class PlaceholderProperties : LayerEffectPropertyGroup
public string PlaceholderFor { get; }
internal LayerEffectEntity OriginalEntity { get; }
/// <inheritdoc />
public override void EnableLayerEffect()
{
protected override void PopulateDefaults()
{
}
}
protected override void EnableProperties()
{
}
/// <inheritdoc />
public override void DisableLayerEffect()
{
}
protected override void DisableProperties()
{
}
/// <inheritdoc />
public override void Update(double deltaTime)
{
}
/// <inheritdoc />
public override void PreProcess(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
}
/// <inheritdoc />
public override void PostProcess(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
}
internal override string GetEffectTypeName()
{
return OriginalEntity.EffectType;
}
}
/// <summary>
/// This is in place so that the UI has something to show
/// </summary>
internal class PlaceholderProperties : LayerEffectPropertyGroup
{
protected override void PopulateDefaults()
{
}
protected override void EnableProperties()
{
}
protected override void DisableProperties()
{
}
}
@@ -1,11 +1,10 @@
namespace Artemis.Core.LayerEffects.Placeholder
namespace Artemis.Core.LayerEffects.Placeholder;
internal static class PlaceholderLayerEffectDescriptor
{
internal static class PlaceholderLayerEffectDescriptor
public static LayerEffectDescriptor Create(string missingProviderId)
{
public static LayerEffectDescriptor Create(string missingProviderId)
{
LayerEffectDescriptor descriptor = new(missingProviderId, Constants.EffectPlaceholderPlugin);
return descriptor;
}
LayerEffectDescriptor descriptor = new(missingProviderId, Constants.EffectPlaceholderPlugin);
return descriptor;
}
}
@@ -1,25 +1,24 @@
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// Evaluates to true or false by returning the value of ActivationMet
/// </summary>
public class BooleanActivationRequirement : IModuleActivationRequirement
{
/// <summary>
/// Evaluates to true or false by returning the value of ActivationMet
/// Gets or sets whether the activation requirement is met
/// </summary>
public class BooleanActivationRequirement : IModuleActivationRequirement
public bool ActivationMet { get; set; }
/// <inheritdoc />
public bool Evaluate()
{
/// <summary>
/// Gets or sets whether the activation requirement is met
/// </summary>
public bool ActivationMet { get; set; }
return ActivationMet;
}
/// <inheritdoc />
public bool Evaluate()
{
return ActivationMet;
}
/// <inheritdoc />
public string GetUserFriendlyDescription()
{
return "No description available";
}
/// <inheritdoc />
public string GetUserFriendlyDescription()
{
return "No description available";
}
}
@@ -1,20 +1,19 @@
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// Evaluates to true or false by checking requirements specific to the implementation
/// </summary>
public interface IModuleActivationRequirement
{
/// <summary>
/// Evaluates to true or false by checking requirements specific to the implementation
/// Called to determine whether the activation requirement is met
/// </summary>
public interface IModuleActivationRequirement
{
/// <summary>
/// Called to determine whether the activation requirement is met
/// </summary>
/// <returns></returns>
bool Evaluate();
/// <returns></returns>
bool Evaluate();
/// <summary>
/// Returns a user-friendly description of the activation requirement, should include parameters if applicable
/// </summary>
/// <returns>A user-friendly description of the activation requirement</returns>
string GetUserFriendlyDescription();
}
/// <summary>
/// Returns a user-friendly description of the activation requirement, should include parameters if applicable
/// </summary>
/// <returns>A user-friendly description of the activation requirement</returns>
string GetUserFriendlyDescription();
}
@@ -5,62 +5,61 @@ using System.IO;
using System.Linq;
using Artemis.Core.Services;
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// Evaluates to true or false by checking if the specified process is running
/// </summary>
public class ProcessActivationRequirement : IModuleActivationRequirement
{
/// <summary>
/// Evaluates to true or false by checking if the specified process is running
/// Creates a new instance of the <see cref="ProcessActivationRequirement" /> class
/// </summary>
public class ProcessActivationRequirement : IModuleActivationRequirement
/// <param name="processName">The name of the process that must run</param>
/// <param name="location">The location of where the process must be running from (optional)</param>
public ProcessActivationRequirement(string? processName, string? location = null)
{
/// <summary>
/// Creates a new instance of the <see cref="ProcessActivationRequirement" /> class
/// </summary>
/// <param name="processName">The name of the process that must run</param>
/// <param name="location">The location of where the process must be running from (optional)</param>
public ProcessActivationRequirement(string? processName, string? location = null)
{
if (string.IsNullOrWhiteSpace(processName) && string.IsNullOrWhiteSpace(location))
throw new ArgumentNullException($"Atleast one {nameof(processName)} and {nameof(location)} must not be null");
if (string.IsNullOrWhiteSpace(processName) && string.IsNullOrWhiteSpace(location))
throw new ArgumentNullException($"Atleast one {nameof(processName)} and {nameof(location)} must not be null");
ProcessName = processName;
Location = location;
}
ProcessName = processName;
Location = location;
}
/// <summary>
/// The name of the process that must run
/// </summary>
public string? ProcessName { get; set; }
/// <summary>
/// The name of the process that must run
/// </summary>
public string? ProcessName { get; set; }
/// <summary>
/// The location of where the process must be running from
/// </summary>
public string? Location { get; set; }
/// <summary>
/// The location of where the process must be running from
/// </summary>
public string? Location { get; set; }
internal static IProcessMonitorService? ProcessMonitorService { get; set; }
internal static IProcessMonitorService? ProcessMonitorService { get; set; }
/// <inheritdoc />
public bool Evaluate()
{
if (ProcessMonitorService == null || ProcessName == null && Location == null)
return false;
/// <inheritdoc />
public bool Evaluate()
{
if (ProcessMonitorService == null || (ProcessName == null && Location == null))
return false;
IEnumerable<Process> processes = ProcessMonitorService.GetRunningProcesses();
if (ProcessName != null)
processes = processes.Where(p => string.Equals(p.ProcessName, ProcessName, StringComparison.InvariantCultureIgnoreCase));
if (Location != null)
processes = processes.Where(p => string.Equals(Path.GetDirectoryName(p.GetProcessFilename()), Location, StringComparison.InvariantCultureIgnoreCase));
IEnumerable<Process> processes = ProcessMonitorService.GetRunningProcesses();
if (ProcessName != null)
processes = processes.Where(p => string.Equals(p.ProcessName, ProcessName, StringComparison.InvariantCultureIgnoreCase));
if (Location != null)
processes = processes.Where(p => string.Equals(Path.GetDirectoryName(p.GetProcessFilename()), Location, StringComparison.InvariantCultureIgnoreCase));
return processes.Any();
}
return processes.Any();
}
/// <inheritdoc />
public string GetUserFriendlyDescription()
{
string description = $"Requirement met when \"{ProcessName}.exe\" is running";
if (Location != null)
description += $" from \"{Location}\"";
/// <inheritdoc />
public string GetUserFriendlyDescription()
{
string description = $"Requirement met when \"{ProcessName}.exe\" is running";
if (Location != null)
description += $" from \"{Location}\"";
return description;
}
return description;
}
}
@@ -1,11 +1,10 @@
using System;
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// Represents an attribute that marks a data model property to be ignored by the UI
/// </summary>
public class DataModelIgnoreAttribute : Attribute
{
/// <summary>
/// Represents an attribute that marks a data model property to be ignored by the UI
/// </summary>
public class DataModelIgnoreAttribute : Attribute
{
}
}
@@ -1,51 +1,50 @@
using System;
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// Represents an attribute that describes a data model property
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class DataModelPropertyAttribute : Attribute
{
/// <summary>
/// Represents an attribute that describes a data model property
/// Gets or sets the user-friendly name for this property, shown in the UI.
/// </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; }
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 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 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 affix to show behind displaying elements in the UI.
/// </summary>
public string? Affix { get; set; }
/// <summary>
/// Gets or sets the name of list items, only applicable to enumerable data model properties
/// </summary>
public string? ListItemName { get; set; }
/// <summary>
/// Gets or sets the name of list items, only applicable to enumerable data model properties
/// </summary>
public string? ListItemName { 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 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 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;
}
/// <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;
}
+345 -354
View File
@@ -7,381 +7,372 @@ using System.Reflection;
using Humanizer;
using Newtonsoft.Json;
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// Represents a data model that contains information on a game/application etc.
/// </summary>
public abstract class DataModel
{
private readonly List<DataModelPath> _activePaths = new();
private readonly HashSet<string> _activePathsHashSet = new();
private readonly Dictionary<string, DynamicChild> _dynamicChildren = new();
/// <summary>
/// Represents a data model that contains information on a game/application etc.
/// Creates a new instance of the <see cref="DataModel" /> class
/// </summary>
public abstract class DataModel
protected DataModel()
{
private readonly HashSet<string> _activePathsHashSet = new();
private readonly List<DataModelPath> _activePaths = new();
private readonly Dictionary<string, DynamicChild> _dynamicChildren = new();
// These are both set right after construction to keep the constructor of inherited classes clean
Module = null!;
DataModelDescription = null!;
/// <summary>
/// Creates a new instance of the <see cref="DataModel" /> class
/// </summary>
protected DataModel()
ActivePaths = new ReadOnlyCollection<DataModelPath>(_activePaths);
DynamicChildren = new ReadOnlyDictionary<string, DynamicChild>(_dynamicChildren);
}
/// <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 { get; }
/// <summary>
/// Gets a read-only list of <see cref="DataModelPath" />s targeting this data model
/// </summary>
[DataModelIgnore]
public ReadOnlyCollection<DataModelPath> ActivePaths { get; }
/// <summary>
/// Returns a read-only collection of all properties in this datamodel that are to be ignored
/// </summary>
/// <returns></returns>
public ReadOnlyCollection<PropertyInfo> GetHiddenProperties()
{
return Module.HiddenProperties;
}
/// <summary>
/// Gets the property description of the provided property info
/// </summary>
/// <returns>If found, the property description attribute, otherwise <see langword="null" />.</returns>
public virtual DataModelPropertyAttribute? GetPropertyDescription(PropertyInfo propertyInfo)
{
return (DataModelPropertyAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(DataModelPropertyAttribute));
}
/// <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>
/// Occurs when a dynamic child has been added to this data model
/// </summary>
public event EventHandler<DataModelPathEventArgs>? ActivePathAdded;
/// <summary>
/// Occurs when a dynamic child has been removed from this data model
/// </summary>
public event EventHandler<DataModelPathEventArgs>? ActivePathRemoved;
/// <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);
}
/// <summary>
/// Invokes the <see cref="ActivePathAdded" /> event
/// </summary>
protected virtual void OnActivePathAdded(DataModelPathEventArgs e)
{
ActivePathAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="ActivePathRemoved" /> event
/// </summary>
protected virtual void OnActivePathRemoved(DataModelPathEventArgs e)
{
ActivePathRemoved?.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)
{
// These are both set right after construction to keep the constructor of inherited classes clean
Module = null!;
DataModelDescription = null!;
ActivePaths = new ReadOnlyCollection<DataModelPath>(_activePaths);
DynamicChildren = new ReadOnlyDictionary<string, DynamicChild>(_dynamicChildren);
dynamicDataModel.Module = Module;
dynamicDataModel.DataModelDescription = attribute;
}
/// <summary>
/// Gets the module this data model belongs to
/// </summary>
[JsonIgnore]
[DataModelIgnore]
public Module Module { get; internal set; }
DynamicChild<T> dynamicChild = new(initialValue, key, attribute);
_dynamicChildren.Add(key, dynamicChild);
/// <summary>
/// Gets the <see cref="DataModelPropertyAttribute" /> describing this data model
/// </summary>
[JsonIgnore]
[DataModelIgnore]
public DataModelPropertyAttribute DataModelDescription { get; internal set; }
OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key));
return dynamicChild;
}
/// <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 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 an read-only dictionary of all dynamic children
/// </summary>
[DataModelIgnore]
public ReadOnlyDictionary<string, DynamicChild> DynamicChildren { get; }
/// <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 read-only list of <see cref="DataModelPath" />s targeting this data model
/// </summary>
[DataModelIgnore]
public ReadOnlyCollection<DataModelPath> ActivePaths { get; }
/// <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));
/// <summary>
/// Returns a read-only collection of all properties in this datamodel that are to be ignored
/// </summary>
/// <returns></returns>
public ReadOnlyCollection<PropertyInfo> GetHiddenProperties()
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)
{
return Module.HiddenProperties;
}
/// <summary>
/// Gets the property description of the provided property info
/// </summary>
/// <returns>If found, the property description attribute, otherwise <see langword="null"/>.</returns>
public virtual DataModelPropertyAttribute? GetPropertyDescription(PropertyInfo propertyInfo)
{
return (DataModelPropertyAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(DataModelPropertyAttribute));
}
#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)
/// <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)
{
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)
{
if (dynamicChild.BaseValue is T value)
return value;
return default;
}
if (dynamicChild.BaseValue is T value)
return value;
return default;
}
#endregion
#region Paths
/// <summary>
/// Determines whether the provided dot-separated path is in use
/// </summary>
/// <param name="path">The path to check per example: <c>MyDataModelChild.MyDataModelProperty</c></param>
/// <param name="includeChildren">
/// If <see langword="true" /> any child of the given path will return true as well; if
/// <see langword="false" /> only an exact path match returns <see langword="true" />.
/// </param>
internal bool IsPropertyInUse(string path, bool includeChildren)
{
path = path.ToUpperInvariant();
lock (_activePaths)
{
return includeChildren
? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal))
: _activePathsHashSet.Contains(path);
}
}
internal void AddDataModelPath(DataModelPath path)
{
lock (_activePaths)
{
if (_activePaths.Contains(path))
return;
_activePaths.Add(path);
// Add to the hashset if this is the first path pointing
string hashPath = path.Path.ToUpperInvariant();
if (!_activePathsHashSet.Contains(hashPath))
_activePathsHashSet.Add(hashPath);
}
OnActivePathAdded(new DataModelPathEventArgs(path));
}
internal void RemoveDataModelPath(DataModelPath path)
{
lock (_activePaths)
{
if (!_activePaths.Remove(path))
return;
// Remove from the hashset if this was the last path pointing there
if (_activePaths.All(p => p.Path != path.Path))
_activePathsHashSet.Remove(path.Path.ToUpperInvariant());
}
OnActivePathRemoved(new DataModelPathEventArgs(path));
}
#endregion
#region Events
/// <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>
/// Occurs when a dynamic child has been added to this data model
/// </summary>
public event EventHandler<DataModelPathEventArgs>? ActivePathAdded;
/// <summary>
/// Occurs when a dynamic child has been removed from this data model
/// </summary>
public event EventHandler<DataModelPathEventArgs>? ActivePathRemoved;
/// <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);
}
/// <summary>
/// Invokes the <see cref="ActivePathAdded" /> event
/// </summary>
protected virtual void OnActivePathAdded(DataModelPathEventArgs e)
{
ActivePathAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="ActivePathRemoved" /> event
/// </summary>
protected virtual void OnActivePathRemoved(DataModelPathEventArgs e)
{
ActivePathRemoved?.Invoke(this, e);
}
#endregion
return default;
}
#endregion
#region Paths
/// <summary>
/// Determines whether the provided dot-separated path is in use
/// </summary>
/// <param name="path">The path to check per example: <c>MyDataModelChild.MyDataModelProperty</c></param>
/// <param name="includeChildren">
/// If <see langword="true" /> any child of the given path will return true as well; if
/// <see langword="false" /> only an exact path match returns <see langword="true" />.
/// </param>
internal bool IsPropertyInUse(string path, bool includeChildren)
{
path = path.ToUpperInvariant();
lock (_activePaths)
{
return includeChildren
? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal))
: _activePathsHashSet.Contains(path);
}
}
internal void AddDataModelPath(DataModelPath path)
{
lock (_activePaths)
{
if (_activePaths.Contains(path))
return;
_activePaths.Add(path);
// Add to the hashset if this is the first path pointing
string hashPath = path.Path.ToUpperInvariant();
if (!_activePathsHashSet.Contains(hashPath))
_activePathsHashSet.Add(hashPath);
}
OnActivePathAdded(new DataModelPathEventArgs(path));
}
internal void RemoveDataModelPath(DataModelPath path)
{
lock (_activePaths)
{
if (!_activePaths.Remove(path))
return;
// Remove from the hashset if this was the last path pointing there
if (_activePaths.All(p => p.Path != path.Path))
_activePathsHashSet.Remove(path.Path.ToUpperInvariant());
}
OnActivePathRemoved(new DataModelPathEventArgs(path));
}
#endregion
}
@@ -1,66 +1,65 @@
using System;
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// Represents a dynamic child value with its property attribute
/// </summary>
public class DynamicChild<T> : DynamicChild
{
/// <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))
{
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 T Value { get; set; }
/// <inheritdoc />
protected override object? GetValue()
{
return Value;
}
Value = value;
}
/// <summary>
/// Represents a dynamic child value with its property attribute
/// Gets or sets the current value of the dynamic child
/// </summary>
public abstract class DynamicChild
public T Value { get; set; }
/// <inheritdoc />
protected override object? GetValue()
{
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();
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();
}
@@ -1,10 +1,8 @@
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// The base class for any view model that belongs to a module
/// </summary>
public interface IModuleViewModel
{
/// <summary>
/// The base class for any view model that belongs to a module
/// </summary>
public interface IModuleViewModel
{
}
}
+299 -300
View File
@@ -7,341 +7,340 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace Artemis.Core.Modules
namespace Artemis.Core.Modules;
/// <summary>
/// Allows you to add new data to the Artemis data model
/// </summary>
public abstract class Module<T> : Module where T : DataModel, new()
{
/// <summary>
/// Allows you to add new data to the Artemis data model
/// The data model driving this module
/// <para>Note: This default data model is automatically registered and instantiated upon plugin enable</para>
/// </summary>
public abstract class Module<T> : Module where T : DataModel, new()
public T DataModel
{
/// <summary>
/// The data model driving this module
/// <para>Note: This default data model is automatically registered and instantiated 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.
/// <c>HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC)</c>
/// </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.
/// <c>ShowProperty(dm => dm.TimeDataModel.CurrentTimeUTC)</c>
/// </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));
}
/// <summary>
/// Determines whether the provided dot-separated path is actively being used by Artemis
/// <para>Note: <see cref="IsPropertyInUse" /> is slightly faster but string-based.</para>
/// </summary>
/// <param name="propertyLambda">
/// The path to check per example: <c>IsPropertyInUse(dm => dm.TimeDataModel.CurrentTimeUTC)</c>
/// </param>
/// <param name="includeChildren">
/// If <see langword="true" /> any child of the given path will return true as well; if
/// <see langword="false" /> only an exact path match returns <see langword="true" />.
/// </param>
public bool IsPropertyInUse<TProperty>(Expression<Func<T, TProperty>> propertyLambda, bool includeChildren)
{
string path = GetMemberPath((MemberExpression) propertyLambda.Body);
return IsPropertyInUse(path, includeChildren);
}
/// <summary>
/// Determines whether the provided dot-separated path is actively being used by Artemis
/// </summary>
/// <param name="path">The path to check per example: <c>MyDataModelChild.MyDataModelProperty</c></param>
/// <param name="includeChildren">
/// If <see langword="true" /> any child of the given path will return true as well; if
/// <see langword="false" /> only an exact path match returns <see langword="true" />.
/// </param>
public bool IsPropertyInUse(string path, bool includeChildren)
{
return DataModel.IsPropertyInUse(path, includeChildren);
}
internal override void InternalEnable()
{
DataModel = new T();
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();
}
get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model");
internal set => InternalDataModel = value;
}
/// <summary>
/// For internal use only, please use <see cref="Module{T}" />.
/// Hide the provided property using a lambda expression, e.g.
/// <c>HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC)</c>
/// </summary>
public abstract class Module : PluginFeature
/// <param name="propertyLambda">A lambda expression pointing to the property to ignore</param>
public void HideProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new();
private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new();
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo)))
HiddenPropertiesList.Add(propertyInfo);
}
/// <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>
/// Stop hiding the provided property using a lambda expression, e.g.
/// <c>ShowProperty(dm => dm.TimeDataModel.CurrentTimeUTC)</c>
/// </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));
}
/// <summary>
/// The base constructor of the <see cref="Module" /> class.
/// </summary>
protected Module()
/// <summary>
/// Determines whether the provided dot-separated path is actively being used by Artemis
/// <para>Note: <see cref="IsPropertyInUse" /> is slightly faster but string-based.</para>
/// </summary>
/// <param name="propertyLambda">
/// The path to check per example: <c>IsPropertyInUse(dm => dm.TimeDataModel.CurrentTimeUTC)</c>
/// </param>
/// <param name="includeChildren">
/// If <see langword="true" /> any child of the given path will return true as well; if
/// <see langword="false" /> only an exact path match returns <see langword="true" />.
/// </param>
public bool IsPropertyInUse<TProperty>(Expression<Func<T, TProperty>> propertyLambda, bool includeChildren)
{
string path = GetMemberPath((MemberExpression) propertyLambda.Body);
return IsPropertyInUse(path, includeChildren);
}
/// <summary>
/// Determines whether the provided dot-separated path is actively being used by Artemis
/// </summary>
/// <param name="path">The path to check per example: <c>MyDataModelChild.MyDataModelProperty</c></param>
/// <param name="includeChildren">
/// If <see langword="true" /> any child of the given path will return true as well; if
/// <see langword="false" /> only an exact path match returns <see langword="true" />.
/// </param>
public bool IsPropertyInUse(string path, bool includeChildren)
{
return DataModel.IsPropertyInUse(path, includeChildren);
}
internal override void InternalEnable()
{
DataModel = new T();
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)
{
DefaultProfilePaths = new ReadOnlyCollection<(DefaultCategoryName, string)>(_defaultProfilePaths);
HiddenProperties = new ReadOnlyCollection<PropertyInfo>(HiddenPropertiesList);
builder.Insert(0, me.Member.Name);
me = me.Expression as MemberExpression;
if (me != null)
builder.Insert(0, ".");
}
/// <summary>
/// Gets a read only collection of default profile paths
/// </summary>
public IReadOnlyCollection<(DefaultCategoryName, string)> DefaultProfilePaths { get; }
return builder.ToString();
}
}
/// <summary>
/// A list of activation requirements
/// <para>
/// If this list is not <see langword="null" /> and not empty <see cref="IsAlwaysAvailable" /> becomes
/// <see langword="false" /> and the data of this module is only available to profiles specifically targeting it.
/// </para>
/// </summary>
public abstract List<IModuleActivationRequirement>? ActivationRequirements { get; }
/// <summary>
/// For internal use only, please use <see cref="Module{T}" />.
/// </summary>
public abstract class Module : PluginFeature
{
private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new();
private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new();
/// <summary>
/// Gets whether this module is activated. A module can only be active while its <see cref="ActivationRequirements" />
/// are met
/// </summary>
public bool IsActivated { get; internal set; }
/// <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 whether this module's activation was due to an override, can only be true if <see cref="IsActivated" /> is
/// <see langword="true" />
/// </summary>
public bool IsActivatedOverride { get; private set; }
/// <summary>
/// The base constructor of the <see cref="Module" /> class.
/// </summary>
protected Module()
{
DefaultProfilePaths = new ReadOnlyCollection<(DefaultCategoryName, string)>(_defaultProfilePaths);
HiddenProperties = new ReadOnlyCollection<PropertyInfo>(HiddenPropertiesList);
}
/// <summary>
/// Gets whether this module should update while <see cref="IsActivatedOverride" /> is <see langword="true" />. When
/// set to <see langword="false" /> <see cref="Update" /> and any timed updates will not get called during an
/// activation override.
/// <para>Defaults to <see langword="false" /></para>
/// </summary>
public bool UpdateDuringActivationOverride { get; protected set; }
/// <summary>
/// Gets a read only collection of default profile paths
/// </summary>
public IReadOnlyCollection<(DefaultCategoryName, string)> DefaultProfilePaths { get; }
/// <summary>
/// Gets or sets the activation requirement mode, defaults to <see cref="ActivationRequirementType.Any" />
/// </summary>
public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any;
/// <summary>
/// A list of activation requirements
/// <para>
/// If this list is not <see langword="null" /> and not empty <see cref="IsAlwaysAvailable" /> becomes
/// <see langword="false" /> and the data of this module is only available to profiles specifically targeting it.
/// </para>
/// </summary>
public abstract List<IModuleActivationRequirement>? ActivationRequirements { get; }
/// <summary>
/// Gets a boolean indicating whether this module is always available to profiles or only to profiles that specifically
/// target this module.
/// <para>
/// Note: <see langword="true" /> if there are any <see cref="ActivationRequirements" />; otherwise
/// <see langword="false" />
/// </para>
/// </summary>
public bool IsAlwaysAvailable => ActivationRequirements == null || ActivationRequirements.Count == 0;
/// <summary>
/// Gets whether this module is activated. A module can only be active while its <see cref="ActivationRequirements" />
/// are met
/// </summary>
public bool IsActivated { get; internal set; }
/// <summary>
/// Gets whether updating this module is currently allowed
/// </summary>
public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride);
/// <summary>
/// Gets whether this module's activation was due to an override, can only be true if <see cref="IsActivated" /> is
/// <see langword="true" />
/// </summary>
public bool IsActivatedOverride { get; private set; }
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
public ReadOnlyCollection<PropertyInfo> HiddenProperties { get; }
/// <summary>
/// Gets whether this module should update while <see cref="IsActivatedOverride" /> is <see langword="true" />. When
/// set to <see langword="false" /> <see cref="Update" /> and any timed updates will not get called during an
/// activation override.
/// <para>Defaults to <see langword="false" /></para>
/// </summary>
public bool UpdateDuringActivationOverride { get; protected set; }
internal DataModel? InternalDataModel { get; set; }
/// <summary>
/// Gets or sets the activation requirement mode, defaults to <see cref="ActivationRequirementType.Any" />
/// </summary>
public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any;
/// <summary>
/// Called each frame when the module should update
/// </summary>
/// <param name="deltaTime">Time in seconds since the last update</param>
public abstract void Update(double deltaTime);
/// <summary>
/// Gets a boolean indicating whether this module is always available to profiles or only to profiles that specifically
/// target this module.
/// <para>
/// Note: <see langword="true" /> if there are any <see cref="ActivationRequirements" />; otherwise
/// <see langword="false" />
/// </para>
/// </summary>
public bool IsAlwaysAvailable => ActivationRequirements == null || ActivationRequirements.Count == 0;
/// <summary>
/// Called when the <see cref="ActivationRequirements" /> are met or during an override
/// </summary>
/// <param name="isOverride">
/// If true, the activation was due to an override. This usually means the module was activated
/// by the profile editor
/// </param>
public virtual void ModuleActivated(bool isOverride)
/// <summary>
/// Gets whether updating this module is currently allowed
/// </summary>
public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride);
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
public ReadOnlyCollection<PropertyInfo> HiddenProperties { get; }
internal DataModel? InternalDataModel { get; set; }
/// <summary>
/// Called each frame when the module should update
/// </summary>
/// <param name="deltaTime">Time in seconds since the last update</param>
public abstract void Update(double deltaTime);
/// <summary>
/// Called when the <see cref="ActivationRequirements" /> are met or during an override
/// </summary>
/// <param name="isOverride">
/// If true, the activation was due to an override. This usually means the module was activated
/// by the profile editor
/// </param>
public virtual void ModuleActivated(bool isOverride)
{
}
/// <summary>
/// Called when the <see cref="ActivationRequirements" /> are no longer met or during an override
/// </summary>
/// <param name="isOverride">
/// If true, the deactivation was due to an override. This usually means the module was deactivated
/// by the profile editor
/// </param>
public virtual void ModuleDeactivated(bool isOverride)
{
}
/// <summary>
/// Evaluates the activation requirements following the <see cref="ActivationRequirementMode" /> and returns the result
/// </summary>
/// <returns>The evaluated result of the activation requirements</returns>
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;
}
/// <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 DataModelPropertyAttribute {Name = Info.Name, Description = 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!)
{
}
/// <summary>
/// Called when the <see cref="ActivationRequirements" /> are no longer met or during an override
/// </summary>
/// <param name="isOverride">
/// If true, the deactivation was due to an override. This usually means the module was deactivated
/// by the profile editor
/// </param>
public virtual void ModuleDeactivated(bool isOverride)
{
}
/// <summary>
/// Evaluates the activation requirements following the <see cref="ActivationRequirementMode" /> and returns the result
/// </summary>
/// <returns>The evaluated result of the activation requirements</returns>
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;
}
/// <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 DataModelPropertyAttribute {Name = Info.Name, Description = 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)))
if (_pendingDefaultProfilePaths.Contains((category, file)))
return false;
_defaultProfilePaths.Add((category, file));
_pendingDefaultProfilePaths.Add((category, file));
return true;
}
internal virtual void InternalUpdate(double deltaTime)
{
StartUpdateMeasure();
if (IsUpdateAllowed)
Update(deltaTime);
StopUpdateMeasure();
}
if (!Path.IsPathRooted(file))
file = Plugin.ResolveRelativePath(file);
internal virtual void Activate(bool isOverride)
{
if (IsActivated)
return;
// Ensure the file exists
if (!File.Exists(file))
throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}.");
IsActivatedOverride = isOverride;
ModuleActivated(isOverride);
IsActivated = true;
}
if (_defaultProfilePaths.Contains((category, file)))
return false;
_defaultProfilePaths.Add((category, file));
internal virtual void Deactivate(bool isOverride)
{
if (!IsActivated)
return;
IsActivatedOverride = false;
IsActivated = false;
ModuleDeactivated(isOverride);
}
#region Overrides of PluginFeature
/// <inheritdoc />
internal override void InternalEnable()
{
foreach ((DefaultCategoryName categoryName, string? 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);
}
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
/// <inheritdoc />
internal override void InternalEnable()
{
foreach ((DefaultCategoryName categoryName, string? 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);
}
}
/// <summary>
/// Describes in what way the activation requirements of a module must be met
/// </summary>
public enum ActivationRequirementType
{
/// <summary>
/// Any activation requirement must be met for the module to activate
/// </summary>
Any,
/// <summary>
/// Describes in what way the activation requirements of a module must be met
/// All activation requirements must be met for the module to activate
/// </summary>
public enum ActivationRequirementType
{
/// <summary>
/// Any activation requirement must be met for the module to activate
/// </summary>
Any,
/// <summary>
/// All activation requirements must be met for the module to activate
/// </summary>
All
}
All
}
+294 -297
View File
@@ -10,337 +10,334 @@ using Artemis.Storage.Entities.Plugins;
using McMaster.NETCore.Plugins;
using Ninject;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin
/// </summary>
public class Plugin : CorePropertyChanged, IDisposable
{
/// <summary>
/// Represents a plugin
/// </summary>
public class Plugin : CorePropertyChanged, IDisposable
private readonly List<PluginFeatureInfo> _features;
private readonly bool _loadedFromStorage;
private readonly List<Profiler> _profilers;
private bool _isEnabled;
internal Plugin(PluginInfo info, DirectoryInfo directory, PluginEntity? pluginEntity)
{
private readonly bool _loadedFromStorage;
private readonly List<PluginFeatureInfo> _features;
private readonly List<Profiler> _profilers;
Info = info;
Directory = directory;
Entity = pluginEntity ?? new PluginEntity {Id = Guid};
Info.Plugin = this;
private bool _isEnabled;
_loadedFromStorage = pluginEntity != null;
_features = new List<PluginFeatureInfo>();
_profilers = new List<Profiler>();
internal Plugin(PluginInfo info, DirectoryInfo directory, PluginEntity? pluginEntity)
{
Info = info;
Directory = directory;
Entity = pluginEntity ?? new PluginEntity {Id = Guid};
Info.Plugin = this;
Features = new ReadOnlyCollection<PluginFeatureInfo>(_features);
Profilers = new ReadOnlyCollection<Profiler>(_profilers);
}
_loadedFromStorage = pluginEntity != null;
_features = new List<PluginFeatureInfo>();
_profilers = new List<Profiler>();
/// <summary>
/// Gets the plugin GUID
/// </summary>
public Guid Guid => Info.Guid;
Features = new ReadOnlyCollection<PluginFeatureInfo>(_features);
Profilers = new ReadOnlyCollection<Profiler>(_profilers);
}
/// <summary>
/// Gets the plugin info related to this plugin
/// </summary>
public PluginInfo Info { get; }
/// <summary>
/// Gets the plugin GUID
/// </summary>
public Guid Guid => Info.Guid;
/// <summary>
/// The plugins root directory
/// </summary>
public DirectoryInfo Directory { get; }
/// <summary>
/// Gets the plugin info related to this plugin
/// </summary>
public PluginInfo Info { get; }
/// <summary>
/// Gets or sets a configuration dialog for this plugin that is accessible in the UI under Settings > Plugins
/// </summary>
public IPluginConfigurationDialog? ConfigurationDialog { get; set; }
/// <summary>
/// The plugins root directory
/// </summary>
public DirectoryInfo Directory { get; }
/// <summary>
/// Indicates whether the user enabled the plugin or not
/// </summary>
public bool IsEnabled
{
get => _isEnabled;
private set => SetAndNotify(ref _isEnabled, value);
}
/// <summary>
/// Gets or sets a configuration dialog for this plugin that is accessible in the UI under Settings > Plugins
/// </summary>
public IPluginConfigurationDialog? ConfigurationDialog { get; set; }
/// <summary>
/// Gets a read-only collection of all features this plugin provides
/// </summary>
public ReadOnlyCollection<PluginFeatureInfo> Features { get; }
/// <summary>
/// Indicates whether the user enabled the plugin or not
/// </summary>
public bool IsEnabled
{
get => _isEnabled;
private set => SetAndNotify(ref _isEnabled, value);
}
/// <summary>
/// Gets a read-only collection of profiles running on the plugin
/// </summary>
public ReadOnlyCollection<Profiler> Profilers { get; }
/// <summary>
/// Gets a read-only collection of all features this plugin provides
/// </summary>
public ReadOnlyCollection<PluginFeatureInfo> Features { get; }
/// <summary>
/// The assembly the plugin code lives in
/// </summary>
public Assembly? Assembly { get; internal set; }
/// <summary>
/// Gets a read-only collection of profiles running on the plugin
/// </summary>
public ReadOnlyCollection<Profiler> Profilers { get; }
/// <summary>
/// Gets the plugin bootstrapper
/// </summary>
public PluginBootstrapper? Bootstrapper { get; internal set; }
/// <summary>
/// The assembly the plugin code lives in
/// </summary>
public Assembly? Assembly { get; internal set; }
/// <summary>
/// The Ninject kernel of the plugin
/// </summary>
public IKernel? Kernel { get; internal set; }
/// <summary>
/// Gets the plugin bootstrapper
/// </summary>
public PluginBootstrapper? Bootstrapper { get; internal set; }
/// <summary>
/// The PluginLoader backing this plugin
/// </summary>
internal PluginLoader? PluginLoader { get; set; }
/// <summary>
/// The Ninject kernel of the plugin
/// </summary>
public IKernel? Kernel { get; internal set; }
/// <summary>
/// The entity representing the plugin
/// </summary>
internal PluginEntity Entity { get; set; }
/// <summary>
/// The PluginLoader backing this plugin
/// </summary>
internal PluginLoader? PluginLoader { get; set; }
/// <summary>
/// Populated when plugin settings are first loaded
/// </summary>
internal PluginSettings? Settings { get; set; }
/// <summary>
/// The entity representing the plugin
/// </summary>
internal PluginEntity Entity { get; set; }
/// <summary>
/// Resolves the relative path provided in the <paramref name="path" /> parameter to an absolute path
/// </summary>
/// <param name="path">The path to resolve</param>
/// <returns>An absolute path pointing to the provided relative path</returns>
[return: NotNullIfNotNull("path")]
public string? ResolveRelativePath(string? path)
{
return path == null ? null : Path.Combine(Directory.FullName, path);
}
/// <summary>
/// Populated when plugin settings are first loaded
/// </summary>
internal PluginSettings? Settings { get; set; }
/// <summary>
/// Looks up the instance of the feature of type <typeparamref name="T" />
/// </summary>
/// <typeparam name="T">The type of feature to find</typeparam>
/// <returns>If found, the instance of the feature</returns>
public T? GetFeature<T>() where T : PluginFeature
{
return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T;
}
/// <summary>
/// Resolves the relative path provided in the <paramref name="path" /> parameter to an absolute path
/// </summary>
/// <param name="path">The path to resolve</param>
/// <returns>An absolute path pointing to the provided relative path</returns>
[return: NotNullIfNotNull("path")]
public string? ResolveRelativePath(string? path)
{
return path == null ? null : Path.Combine(Directory.FullName, path);
}
/// <summary>
/// Looks up the feature info the feature of type <typeparamref name="T" />
/// </summary>
/// <typeparam name="T">The type of feature to find</typeparam>
/// <returns>Feature info of the feature</returns>
public PluginFeatureInfo GetFeatureInfo<T>() where T : PluginFeature
{
// This should be a safe assumption because any type of PluginFeature is registered and added
return _features.First(i => i.FeatureType == typeof(T));
}
/// <summary>
/// Looks up the instance of the feature of type <typeparamref name="T" />
/// </summary>
/// <typeparam name="T">The type of feature to find</typeparam>
/// <returns>If found, the instance of the feature</returns>
public T? GetFeature<T>() where T : PluginFeature
{
return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T;
}
/// <summary>
/// Looks up the feature info the feature of type <typeparamref name="T" />
/// </summary>
/// <typeparam name="T">The type of feature to find</typeparam>
/// <returns>Feature info of the feature</returns>
public PluginFeatureInfo GetFeatureInfo<T>() where T : PluginFeature
{
// This should be a safe assumption because any type of PluginFeature is registered and added
return _features.First(i => i.FeatureType == typeof(T));
}
/// <summary>
/// Gets a profiler with the provided <paramref name="name" />, if it does not yet exist it will be created.
/// </summary>
/// <param name="name">The name of the profiler</param>
/// <returns>A new or existing profiler with the provided <paramref name="name" /></returns>
public Profiler GetProfiler(string name)
{
Profiler? profiler = _profilers.FirstOrDefault(p => p.Name == name);
if (profiler != null)
return profiler;
profiler = new Profiler(this, name);
_profilers.Add(profiler);
/// <summary>
/// Gets a profiler with the provided <paramref name="name" />, if it does not yet exist it will be created.
/// </summary>
/// <param name="name">The name of the profiler</param>
/// <returns>A new or existing profiler with the provided <paramref name="name" /></returns>
public Profiler GetProfiler(string name)
{
Profiler? profiler = _profilers.FirstOrDefault(p => p.Name == name);
if (profiler != null)
return profiler;
}
/// <summary>
/// Removes a profiler from the plugin
/// </summary>
/// <param name="profiler">The profiler to remove</param>
public void RemoveProfiler(Profiler profiler)
profiler = new Profiler(this, name);
_profilers.Add(profiler);
return profiler;
}
/// <summary>
/// Removes a profiler from the plugin
/// </summary>
/// <param name="profiler">The profiler to remove</param>
public void RemoveProfiler(Profiler profiler)
{
_profilers.Remove(profiler);
}
/// <summary>
/// Gets an instance of the specified service using the plugins dependency injection container.
/// <para>Note: To use parameters reference Ninject and use <see cref="Kernel" /> directly.</para>
/// </summary>
/// <typeparam name="T">The service to resolve.</typeparam>
/// <returns>An instance of the service.</returns>
public T Get<T>()
{
if (Kernel == null)
throw new ArtemisPluginException("Cannot use Get<T> before the plugin finished loading");
return Kernel.Get<T>();
}
/// <inheritdoc />
public override string ToString()
{
return Info.ToString();
}
/// <summary>
/// Occurs when the plugin is enabled
/// </summary>
public event EventHandler? Enabled;
/// <summary>
/// Occurs when the plugin is disabled
/// </summary>
public event EventHandler? Disabled;
/// <summary>
/// Occurs when an feature is loaded and added to the plugin
/// </summary>
public event EventHandler<PluginFeatureInfoEventArgs>? FeatureAdded;
/// <summary>
/// Occurs when an feature is disabled and removed from the plugin
/// </summary>
public event EventHandler<PluginFeatureInfoEventArgs>? FeatureRemoved;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_profilers.Remove(profiler);
}
foreach (PluginFeatureInfo feature in Features)
feature.Instance?.Dispose();
SetEnabled(false);
/// <summary>
/// Gets an instance of the specified service using the plugins dependency injection container.
/// <para>Note: To use parameters reference Ninject and use <see cref="Kernel" /> directly.</para>
/// </summary>
/// <typeparam name="T">The service to resolve.</typeparam>
/// <returns>An instance of the service.</returns>
public T Get<T>()
Kernel?.Dispose();
PluginLoader?.Dispose();
GC.Collect();
GC.WaitForPendingFinalizers();
_features.Clear();
}
}
/// <summary>
/// Invokes the Enabled event
/// </summary>
protected virtual void OnEnabled()
{
Enabled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the Disabled event
/// </summary>
protected virtual void OnDisabled()
{
Disabled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the FeatureAdded event
/// </summary>
protected virtual void OnFeatureAdded(PluginFeatureInfoEventArgs e)
{
FeatureAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the FeatureRemoved event
/// </summary>
protected virtual void OnFeatureRemoved(PluginFeatureInfoEventArgs e)
{
FeatureRemoved?.Invoke(this, e);
}
internal void ApplyToEntity()
{
Entity.Id = Guid;
Entity.IsEnabled = IsEnabled;
}
internal void AddFeature(PluginFeatureInfo featureInfo)
{
if (featureInfo.Plugin != this)
throw new ArtemisCoreException("Feature is not associated with this plugin");
_features.Add(featureInfo);
OnFeatureAdded(new PluginFeatureInfoEventArgs(featureInfo));
}
internal void RemoveFeature(PluginFeatureInfo featureInfo)
{
if (featureInfo.Instance != null && featureInfo.Instance.IsEnabled)
throw new ArtemisCoreException("Cannot remove an enabled feature from a plugin");
_features.Remove(featureInfo);
featureInfo.Instance?.Dispose();
OnFeatureRemoved(new PluginFeatureInfoEventArgs(featureInfo));
}
internal void SetEnabled(bool enable)
{
if (IsEnabled == enable)
return;
if (!enable && Features.Any(e => e.Instance != null && e.Instance.IsEnabled))
throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features");
IsEnabled = enable;
if (enable)
{
if (Kernel == null)
throw new ArtemisPluginException("Cannot use Get<T> before the plugin finished loading");
return Kernel.Get<T>();
Bootstrapper?.OnPluginEnabled(this);
OnEnabled();
}
/// <inheritdoc />
public override string ToString()
else
{
return Info.ToString();
Bootstrapper?.OnPluginDisabled(this);
OnDisabled();
}
}
/// <summary>
/// Occurs when the plugin is enabled
/// </summary>
public event EventHandler? Enabled;
internal bool HasEnabledFeatures()
{
return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled);
}
/// <summary>
/// Occurs when the plugin is disabled
/// </summary>
public event EventHandler? Disabled;
internal void AutoEnableIfNew()
{
if (_loadedFromStorage)
return;
/// <summary>
/// Occurs when an feature is loaded and added to the plugin
/// </summary>
public event EventHandler<PluginFeatureInfoEventArgs>? FeatureAdded;
// Enabled is preset to true if the plugin meets the following criteria
// - Requires no admin rights
// - No always-enabled device providers
// - Either has no prerequisites or they are all met
Entity.IsEnabled = !Info.RequiresAdmin &&
Features.All(f => !f.AlwaysEnabled || !f.FeatureType.IsAssignableTo(typeof(DeviceProvider))) &&
Info.ArePrerequisitesMet();
/// <summary>
/// Occurs when an feature is disabled and removed from the plugin
/// </summary>
public event EventHandler<PluginFeatureInfoEventArgs>? FeatureRemoved;
if (!Entity.IsEnabled)
return;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
foreach (PluginFeatureInfo feature in Features)
feature.Instance?.Dispose();
SetEnabled(false);
// Also auto-enable any non-device provider feature
foreach (PluginFeatureInfo pluginFeatureInfo in Features)
pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider));
}
Kernel?.Dispose();
PluginLoader?.Dispose();
GC.Collect();
GC.WaitForPendingFinalizers();
_features.Clear();
}
}
/// <summary>
/// Invokes the Enabled event
/// </summary>
protected virtual void OnEnabled()
{
Enabled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the Disabled event
/// </summary>
protected virtual void OnDisabled()
{
Disabled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the FeatureAdded event
/// </summary>
protected virtual void OnFeatureAdded(PluginFeatureInfoEventArgs e)
{
FeatureAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the FeatureRemoved event
/// </summary>
protected virtual void OnFeatureRemoved(PluginFeatureInfoEventArgs e)
{
FeatureRemoved?.Invoke(this, e);
}
internal void ApplyToEntity()
{
Entity.Id = Guid;
Entity.IsEnabled = IsEnabled;
}
internal void AddFeature(PluginFeatureInfo featureInfo)
{
if (featureInfo.Plugin != this)
throw new ArtemisCoreException("Feature is not associated with this plugin");
_features.Add(featureInfo);
OnFeatureAdded(new PluginFeatureInfoEventArgs(featureInfo));
}
internal void RemoveFeature(PluginFeatureInfo featureInfo)
{
if (featureInfo.Instance != null && featureInfo.Instance.IsEnabled)
throw new ArtemisCoreException("Cannot remove an enabled feature from a plugin");
_features.Remove(featureInfo);
featureInfo.Instance?.Dispose();
OnFeatureRemoved(new PluginFeatureInfoEventArgs(featureInfo));
}
internal void SetEnabled(bool enable)
{
if (IsEnabled == enable)
return;
if (!enable && Features.Any(e => e.Instance != null && e.Instance.IsEnabled))
throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features");
IsEnabled = enable;
if (enable)
{
Bootstrapper?.OnPluginEnabled(this);
OnEnabled();
}
else
{
Bootstrapper?.OnPluginDisabled(this);
OnDisabled();
}
}
internal bool HasEnabledFeatures()
{
return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled);
}
internal void AutoEnableIfNew()
{
if (_loadedFromStorage)
return;
// Enabled is preset to true if the plugin meets the following criteria
// - Requires no admin rights
// - No always-enabled device providers
// - Either has no prerequisites or they are all met
Entity.IsEnabled = !Info.RequiresAdmin &&
Features.All(f => !f.AlwaysEnabled || !f.FeatureType.IsAssignableTo(typeof(DeviceProvider))) &&
Info.ArePrerequisitesMet();
if (!Entity.IsEnabled)
return;
// Also auto-enable any non-device provider feature
foreach (PluginFeatureInfo pluginFeatureInfo in Features)
pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider));
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
+82 -83
View File
@@ -1,100 +1,99 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// An optional entry point for your plugin
/// </summary>
public abstract class PluginBootstrapper
{
private Plugin? _plugin;
/// <summary>
/// An optional entry point for your plugin
/// Called when the plugin is loaded
/// </summary>
public abstract class PluginBootstrapper
/// <param name="plugin"></param>
public virtual void OnPluginLoaded(Plugin plugin)
{
private Plugin? _plugin;
}
/// <summary>
/// Called when the plugin is loaded
/// </summary>
/// <param name="plugin"></param>
public virtual void OnPluginLoaded(Plugin plugin)
{
}
/// <summary>
/// Called when the plugin is activated
/// </summary>
/// <param name="plugin">The plugin instance of your plugin</param>
public virtual void OnPluginEnabled(Plugin plugin)
{
}
/// <summary>
/// Called when the plugin is activated
/// </summary>
/// <param name="plugin">The plugin instance of your plugin</param>
public virtual void OnPluginEnabled(Plugin plugin)
{
}
/// <summary>
/// Called when the plugin is deactivated or when Artemis shuts down
/// </summary>
/// <param name="plugin">The plugin instance of your plugin</param>
public virtual void OnPluginDisabled(Plugin plugin)
{
}
/// <summary>
/// Called when the plugin is deactivated or when Artemis shuts down
/// </summary>
/// <param name="plugin">The plugin instance of your plugin</param>
public virtual void OnPluginDisabled(Plugin plugin)
{
}
/// <summary>
/// Adds the provided prerequisite to the plugin.
/// </summary>
/// <param name="prerequisite">The prerequisite to add</param>
public void AddPluginPrerequisite(PluginPrerequisite prerequisite)
{
// TODO: We can keep track of them and add them after load, same goes for the others
if (_plugin == null)
throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded");
/// <summary>
/// Adds the provided prerequisite to the plugin.
/// </summary>
/// <param name="prerequisite">The prerequisite to add</param>
public void AddPluginPrerequisite(PluginPrerequisite prerequisite)
{
// TODO: We can keep track of them and add them after load, same goes for the others
if (_plugin == null)
throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded");
if (!_plugin.Info.Prerequisites.Contains(prerequisite))
_plugin.Info.Prerequisites.Add(prerequisite);
}
if (!_plugin.Info.Prerequisites.Contains(prerequisite))
_plugin.Info.Prerequisites.Add(prerequisite);
}
/// <summary>
/// Removes the provided prerequisite from the plugin.
/// </summary>
/// <param name="prerequisite">The prerequisite to remove</param>
/// <returns>
/// <see langword="true" /> is successfully removed; otherwise <see langword="false" />. This method also returns
/// <see langword="false" /> if the prerequisite was not found.
/// </returns>
public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite)
{
if (_plugin == null)
throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded");
/// <summary>
/// Removes the provided prerequisite from the plugin.
/// </summary>
/// <param name="prerequisite">The prerequisite to remove</param>
/// <returns>
/// <see langword="true" /> is successfully removed; otherwise <see langword="false" />. This method also returns
/// <see langword="false" /> if the prerequisite was not found.
/// </returns>
public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite)
{
if (_plugin == null)
throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded");
return _plugin.Info.Prerequisites.Remove(prerequisite);
}
return _plugin.Info.Prerequisites.Remove(prerequisite);
}
/// <summary>
/// Adds the provided prerequisite to the feature of type <typeparamref name="T" />.
/// </summary>
/// <param name="prerequisite">The prerequisite to add</param>
public void AddFeaturePrerequisite<T>(PluginPrerequisite prerequisite) where T : PluginFeature
{
if (_plugin == null)
throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
/// <summary>
/// Adds the provided prerequisite to the feature of type <typeparamref name="T"/>.
/// </summary>
/// <param name="prerequisite">The prerequisite to add</param>
public void AddFeaturePrerequisite<T>(PluginPrerequisite prerequisite) where T : PluginFeature
{
if (_plugin == null)
throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
PluginFeatureInfo info = _plugin.GetFeatureInfo<T>();
if (!info.Prerequisites.Contains(prerequisite))
info.Prerequisites.Add(prerequisite);
}
PluginFeatureInfo info = _plugin.GetFeatureInfo<T>();
if (!info.Prerequisites.Contains(prerequisite))
info.Prerequisites.Add(prerequisite);
}
/// <summary>
/// Removes the provided prerequisite from the feature of type <typeparamref name="T" />.
/// </summary>
/// <param name="prerequisite">The prerequisite to remove</param>
/// <returns>
/// <see langword="true" /> is successfully removed; otherwise <see langword="false" />. This method also returns
/// <see langword="false" /> if the prerequisite was not found.
/// </returns>
public bool RemoveFeaturePrerequisite<T>(PluginPrerequisite prerequisite) where T : PluginFeature
{
if (_plugin == null)
throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
/// <summary>
/// Removes the provided prerequisite from the feature of type <typeparamref name="T"/>.
/// </summary>
/// <param name="prerequisite">The prerequisite to remove</param>
/// <returns>
/// <see langword="true" /> is successfully removed; otherwise <see langword="false" />. This method also returns
/// <see langword="false" /> if the prerequisite was not found.
/// </returns>
public bool RemoveFeaturePrerequisite<T>(PluginPrerequisite prerequisite) where T : PluginFeature
{
if (_plugin == null)
throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
return _plugin.GetFeatureInfo<T>().Prerequisites.Remove(prerequisite);
}
return _plugin.GetFeatureInfo<T>().Prerequisites.Remove(prerequisite);
}
internal void InternalOnPluginLoaded(Plugin plugin)
{
_plugin = plugin;
OnPluginLoaded(plugin);
}
internal void InternalOnPluginLoaded(Plugin plugin)
{
_plugin = plugin;
OnPluginLoaded(plugin);
}
}
+237 -237
View File
@@ -3,263 +3,263 @@ using System.IO;
using System.Threading.Tasks;
using Artemis.Storage.Entities.Plugins;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an feature of a certain type provided by a plugin
/// </summary>
public abstract class PluginFeature : CorePropertyChanged, IDisposable
{
private bool _isEnabled;
/// <summary>
/// Represents an feature of a certain type provided by a plugin
/// Gets the plugin feature info related to this feature
/// </summary>
public abstract class PluginFeature : CorePropertyChanged, IDisposable
public PluginFeatureInfo Info { get; internal set; } = null!; // Will be set right after construction
/// <summary>
/// Gets the plugin that provides this feature
/// </summary>
public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction
/// <summary>
/// Gets the profiler that can be used to take profiling measurements
/// </summary>
public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction
/// <summary>
/// Gets whether the plugin is enabled
/// </summary>
public bool IsEnabled
{
private bool _isEnabled;
get => _isEnabled;
internal set => SetAndNotify(ref _isEnabled, value);
}
/// <summary>
/// Gets the identifier of this plugin feature
/// </summary>
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
/// <summary>
/// Gets the plugin feature info related to this feature
/// </summary>
public PluginFeatureInfo Info { get; internal set; } = null!; // Will be set right after construction
internal int AutoEnableAttempts { get; set; }
/// <summary>
/// Gets the plugin that provides this feature
/// </summary>
public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
/// <summary>
/// Gets the profiler that can be used to take profiling measurements
/// </summary>
public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction
/// <summary>
/// Called when the feature is activated
/// </summary>
public abstract void Enable();
/// <summary>
/// Gets whether the plugin is enabled
/// </summary>
public bool IsEnabled
/// <summary>
/// Called when the feature is deactivated or when Artemis shuts down
/// </summary>
public abstract void Disable();
/// <summary>
/// Occurs when the feature is enabled
/// </summary>
public event EventHandler? Enabled;
/// <summary>
/// Occurs when the feature is disabled
/// </summary>
public event EventHandler? Disabled;
/// <summary>
/// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
SetEnabled(false);
}
/// <summary>
/// Triggers the Enabled event
/// </summary>
protected virtual void OnEnabled()
{
Enabled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Triggers the Disabled event
/// </summary>
protected virtual void OnDisabled()
{
Disabled?.Invoke(this, EventArgs.Empty);
}
internal void StartUpdateMeasure()
{
Profiler.StartMeasurement("Update");
}
internal void StopUpdateMeasure()
{
Profiler.StopMeasurement("Update");
}
internal void SetEnabled(bool enable, bool isAutoEnable = false)
{
if (enable == IsEnabled)
return;
if (Plugin == null)
throw new ArtemisCoreException("Cannot enable a plugin feature that is not associated with a plugin");
lock (Plugin)
{
get => _isEnabled;
internal set => SetAndNotify(ref _isEnabled, value);
}
if (!Plugin.IsEnabled)
throw new ArtemisCoreException("Cannot enable a plugin feature of a disabled plugin");
internal int AutoEnableAttempts { get; set; }
/// <summary>
/// Gets the identifier of this plugin feature
/// </summary>
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
/// <summary>
/// Called when the feature is activated
/// </summary>
public abstract void Enable();
/// <summary>
/// Called when the feature is deactivated or when Artemis shuts down
/// </summary>
public abstract void Disable();
/// <summary>
/// Occurs when the feature is enabled
/// </summary>
public event EventHandler? Enabled;
/// <summary>
/// Occurs when the feature is disabled
/// </summary>
public event EventHandler? Disabled;
/// <summary>
/// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
SetEnabled(false);
}
/// <summary>
/// Triggers the Enabled event
/// </summary>
protected virtual void OnEnabled()
{
Enabled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Triggers the Disabled event
/// </summary>
protected virtual void OnDisabled()
{
Disabled?.Invoke(this, EventArgs.Empty);
}
internal void StartUpdateMeasure()
{
Profiler.StartMeasurement("Update");
}
internal void StopUpdateMeasure()
{
Profiler.StopMeasurement("Update");
}
internal void SetEnabled(bool enable, bool isAutoEnable = false)
{
if (enable == IsEnabled)
return;
if (Plugin == null)
throw new ArtemisCoreException("Cannot enable a plugin feature that is not associated with a plugin");
lock (Plugin)
if (!enable)
{
if (!Plugin.IsEnabled)
throw new ArtemisCoreException("Cannot enable a plugin feature of a disabled plugin");
// Even if disable failed, still leave it in a disabled state to avoid more issues
InternalDisable();
IsEnabled = false;
if (!enable)
OnDisabled();
return;
}
try
{
if (isAutoEnable)
AutoEnableAttempts++;
if (isAutoEnable && GetLockFileCreated())
{
// Even if disable failed, still leave it in a disabled state to avoid more issues
InternalDisable();
IsEnabled = false;
// Don't wrap existing lock exceptions, simply rethrow them
if (Info.LoadException is ArtemisPluginLockException)
throw Info.LoadException;
OnDisabled();
return;
throw new ArtemisPluginLockException(Info.LoadException);
}
try
{
if (isAutoEnable)
AutoEnableAttempts++;
CreateLockFile();
IsEnabled = true;
if (isAutoEnable && GetLockFileCreated())
{
// Don't wrap existing lock exceptions, simply rethrow them
if (Info.LoadException is ArtemisPluginLockException)
throw Info.LoadException;
// Allow up to 15 seconds for plugins to activate.
// This means plugins that need more time should do their long running tasks in a background thread, which is intentional
// This would've been a perfect match for Thread.Abort but that didn't make it into .NET Core
Task enableTask = Task.Run(InternalEnable);
if (!enableTask.Wait(TimeSpan.FromSeconds(15)))
throw new ArtemisPluginException(Plugin, "Plugin load timeout");
throw new ArtemisPluginLockException(Info.LoadException);
}
CreateLockFile();
IsEnabled = true;
// Allow up to 15 seconds for plugins to activate.
// This means plugins that need more time should do their long running tasks in a background thread, which is intentional
// This would've been a perfect match for Thread.Abort but that didn't make it into .NET Core
Task enableTask = Task.Run(InternalEnable);
if (!enableTask.Wait(TimeSpan.FromSeconds(15)))
throw new ArtemisPluginException(Plugin, "Plugin load timeout");
Info.LoadException = null;
AutoEnableAttempts = 0;
OnEnabled();
}
// If enable failed, put it back in a disabled state
catch (Exception e)
{
IsEnabled = false;
Info.LoadException = e;
throw;
}
finally
{
// Clean up the lock file unless the failure was due to the lock file
// After all, we failed but not miserably :)
if (Info.LoadException is not ArtemisPluginLockException)
DeleteLockFile();
}
Info.LoadException = null;
AutoEnableAttempts = 0;
OnEnabled();
}
// If enable failed, put it back in a disabled state
catch (Exception e)
{
IsEnabled = false;
Info.LoadException = e;
throw;
}
finally
{
// Clean up the lock file unless the failure was due to the lock file
// After all, we failed but not miserably :)
if (Info.LoadException is not ArtemisPluginLockException)
DeleteLockFile();
}
}
internal virtual void InternalEnable()
{
Enable();
}
internal virtual void InternalDisable()
{
if (IsEnabled)
Disable();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Loading
internal void CreateLockFile()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
File.Create(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")).Close();
}
internal void DeleteLockFile()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
if (GetLockFileCreated())
File.Delete(Plugin.ResolveRelativePath($"{GetType().FullName}.lock"));
}
internal bool GetLockFileCreated()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
return File.Exists(Plugin.ResolveRelativePath($"{GetType().FullName}.lock"));
}
#endregion
#region Timed updates
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="action" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="action">
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action, string? name = null)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action, name);
}
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="asyncAction" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="asyncAction">
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func<double, Task> asyncAction, string? name = null)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction, name);
}
#endregion
}
internal virtual void InternalEnable()
{
Enable();
}
internal virtual void InternalDisable()
{
if (IsEnabled)
Disable();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Loading
internal void CreateLockFile()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
File.Create(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")).Close();
}
internal void DeleteLockFile()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
if (GetLockFileCreated())
File.Delete(Plugin.ResolveRelativePath($"{GetType().FullName}.lock"));
}
internal bool GetLockFileCreated()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
return File.Exists(Plugin.ResolveRelativePath($"{GetType().FullName}.lock"));
}
#endregion
#region Timed updates
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="action" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="action">
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action, string? name = null)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action, name);
}
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="asyncAction" /> at
/// the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="asyncAction">
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func<double, Task> asyncAction, string? name = null)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction, name);
}
#endregion
}
@@ -1,33 +1,32 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an attribute that describes a plugin feature
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class PluginFeatureAttribute : Attribute
{
/// <summary>
/// Represents an attribute that describes a plugin feature
/// Gets or sets the user-friendly name for this property, shown in the UI.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class PluginFeatureAttribute : Attribute
{
/// <summary>
/// Gets or sets the user-friendly name for this property, shown in the UI.
/// </summary>
public string? Name { get; set; }
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 user-friendly description for this property, shown in the UI.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available icons
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available icons
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// Marks the feature to always be enabled as long as the plugin is enabled
/// <para>Note: always <see langword="true"/> if this is the plugin's only feature</para>
/// </summary>
public bool AlwaysEnabled { get; set; }
}
/// <summary>
/// Marks the feature to always be enabled as long as the plugin is enabled
/// <para>Note: always <see langword="true" /> if this is the plugin's only feature</para>
/// </summary>
public bool AlwaysEnabled { get; set; }
}
+156 -157
View File
@@ -9,171 +9,170 @@ using Artemis.Storage.Entities.Plugins;
using Humanizer;
using Newtonsoft.Json;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents basic info about a plugin feature and contains a reference to the instance of said feature
/// </summary>
[JsonObject(MemberSerialization.OptIn)]
public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
{
/// <summary>
/// Represents basic info about a plugin feature and contains a reference to the instance of said feature
/// </summary>
[JsonObject(MemberSerialization.OptIn)]
public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
private string? _description;
private string? _icon;
private PluginFeature? _instance;
private Exception? _loadException;
private string _name = null!;
internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute)
{
private Exception? _loadException;
private string? _description;
private string? _icon;
private PluginFeature? _instance;
private string _name = null!;
Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType));
Entity = pluginFeatureEntity;
internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute)
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
if (Icon != null) return;
if (typeof(DeviceProvider).IsAssignableFrom(featureType))
Icon = "Devices";
else if (typeof(Module).IsAssignableFrom(featureType))
Icon = "VectorRectangle";
else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType))
Icon = "Brush";
else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType))
Icon = "AutoAwesome";
else
Icon = "Plugin";
}
internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance)
{
if (instance == null) throw new ArgumentNullException(nameof(instance));
Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
FeatureType = instance.GetType();
Entity = new PluginFeatureEntity();
Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
Instance = instance;
if (Icon != null) return;
Icon = Instance switch
{
Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType));
Entity = pluginFeatureEntity;
DeviceProvider => "Devices",
Module => "VectorRectangle",
LayerBrushProvider => "Brush",
LayerEffectProvider => "AutoAwesome",
_ => "Plugin"
};
}
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
/// <summary>
/// Gets the plugin this feature info is associated with
/// </summary>
public Plugin Plugin { get; }
if (Icon != null) return;
if (typeof(DeviceProvider).IsAssignableFrom(featureType))
Icon = "Devices";
else if (typeof(Module).IsAssignableFrom(featureType))
Icon = "VectorRectangle";
else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType))
Icon = "Brush";
else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType))
Icon = "AutoAwesome";
else
Icon = "Plugin";
}
/// <summary>
/// Gets the type of the feature
/// </summary>
public Type FeatureType { get; }
internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance)
/// <summary>
/// Gets the exception thrown while loading
/// </summary>
public Exception? LoadException
{
get => _loadException;
internal set => SetAndNotify(ref _loadException, value);
}
/// <summary>
/// The name of the feature
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Name
{
get => _name;
internal set => SetAndNotify(ref _name, value);
}
/// <summary>
/// A short description of the feature
/// </summary>
[JsonProperty]
public string? Description
{
get => _description;
set => SetAndNotify(ref _description, value);
}
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available icons
/// </summary>
[JsonProperty]
public string? Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
/// <summary>
/// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled.
/// <para>Note: always <see langword="true" /> if this is the plugin's only feature</para>
/// </summary>
[JsonProperty]
public bool AlwaysEnabled { get; internal set; }
/// <summary>
/// Gets a boolean indicating whether the feature is enabled in persistent storage
/// </summary>
public bool EnabledInStorage => Entity.IsEnabled;
/// <summary>
/// Gets the feature this info is associated with
/// <para>Note: <see langword="null" /> if the associated <see cref="Plugin" /> is disabled</para>
/// </summary>
public PluginFeature? Instance
{
get => _instance;
internal set => SetAndNotify(ref _instance, value);
}
/// <summary>
/// Gets a string representing either a full path pointing to an svg or the markdown icon
/// </summary>
public string? ResolvedIcon
{
get
{
if (instance == null) throw new ArgumentNullException(nameof(instance));
Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
FeatureType = instance.GetType();
Entity = new PluginFeatureEntity();
Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
Instance = instance;
if (Icon != null) return;
Icon = Instance switch
{
DeviceProvider => "Devices",
Module => "VectorRectangle",
LayerBrushProvider => "Brush",
LayerEffectProvider => "AutoAwesome",
_ => "Plugin"
};
}
/// <summary>
/// Gets the plugin this feature info is associated with
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the type of the feature
/// </summary>
public Type FeatureType { get; }
/// <summary>
/// Gets the exception thrown while loading
/// </summary>
public Exception? LoadException
{
get => _loadException;
internal set => SetAndNotify(ref _loadException, value);
}
/// <summary>
/// The name of the feature
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Name
{
get => _name;
internal set => SetAndNotify(ref _name, value);
}
/// <summary>
/// A short description of the feature
/// </summary>
[JsonProperty]
public string? Description
{
get => _description;
set => SetAndNotify(ref _description, value);
}
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available icons
/// </summary>
[JsonProperty]
public string? Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
/// <summary>
/// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled.
/// <para>Note: always <see langword="true"/> if this is the plugin's only feature</para>
/// </summary>
[JsonProperty]
public bool AlwaysEnabled { get; internal set; }
/// <summary>
/// Gets a boolean indicating whether the feature is enabled in persistent storage
/// </summary>
public bool EnabledInStorage => Entity.IsEnabled;
/// <summary>
/// Gets the feature this info is associated with
/// <para>Note: <see langword="null" /> if the associated <see cref="Plugin" /> is disabled</para>
/// </summary>
public PluginFeature? Instance
{
get => _instance;
internal set => SetAndNotify(ref _instance, value);
}
/// <summary>
/// Gets a string representing either a full path pointing to an svg or the markdown icon
/// </summary>
public string? ResolvedIcon
{
get
{
if (Icon == null)
return null;
return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon;
}
}
internal PluginFeatureEntity Entity { get; }
/// <inheritdoc />
public override string ToString()
{
return Instance?.Id ?? "Uninitialized feature";
}
/// <inheritdoc />
public List<PluginPrerequisite> Prerequisites { get; } = new();
/// <inheritdoc />
public IEnumerable<PluginPrerequisite> PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform());
/// <inheritdoc />
public bool ArePrerequisitesMet()
{
return PlatformPrerequisites.All(p => p.IsMet());
if (Icon == null)
return null;
return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon;
}
}
internal PluginFeatureEntity Entity { get; }
/// <inheritdoc />
public override string ToString()
{
return Instance?.Id ?? "Uninitialized feature";
}
/// <inheritdoc />
public List<PluginPrerequisite> Prerequisites { get; } = new();
/// <inheritdoc />
public IEnumerable<PluginPrerequisite> PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform());
/// <inheritdoc />
public bool ArePrerequisitesMet()
{
return PlatformPrerequisites.All(p => p.IsMet());
}
}
+200 -201
View File
@@ -4,211 +4,210 @@ using System.ComponentModel;
using System.Linq;
using Newtonsoft.Json;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents basic info about a plugin and contains a reference to the instance of said plugin
/// </summary>
[JsonObject(MemberSerialization.OptIn)]
public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
{
/// <summary>
/// Represents basic info about a plugin and contains a reference to the instance of said plugin
/// </summary>
[JsonObject(MemberSerialization.OptIn)]
public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
private Version? _api;
private string? _author;
private bool _autoEnableFeatures = true;
private string? _description;
private Guid _guid;
private string? _icon;
private string _main = null!;
private string _name = null!;
private PluginPlatform? _platforms;
private Plugin _plugin = null!;
private Uri? _repository;
private bool _requiresAdmin;
private Version _version = null!;
private Uri? _website;
internal PluginInfo()
{
private Guid _guid;
private string? _description;
private string? _author;
private Uri? _website;
private Uri? _repository;
private string? _icon;
private string _main = null!;
private bool _autoEnableFeatures = true;
private string _name = null!;
private Plugin _plugin = null!;
private Version _version = null!;
private bool _requiresAdmin;
private PluginPlatform? _platforms;
private Version? _api;
}
internal PluginInfo()
/// <summary>
/// The plugins GUID
/// </summary>
[JsonProperty(Required = Required.Always)]
public Guid Guid
{
get => _guid;
internal set => SetAndNotify(ref _guid, value);
}
/// <summary>
/// The name of the plugin
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Name
{
get => _name;
internal set => SetAndNotify(ref _name, value);
}
/// <summary>
/// A short description of the plugin
/// </summary>
[JsonProperty]
public string? Description
{
get => _description;
set => SetAndNotify(ref _description, value);
}
/// <summary>
/// Gets or sets the author of this plugin
/// </summary>
[JsonProperty]
public string? Author
{
get => _author;
set => SetAndNotify(ref _author, value);
}
/// <summary>
/// Gets or sets the website of this plugin or its author
/// </summary>
[JsonProperty]
public Uri? Website
{
get => _website;
set => SetAndNotify(ref _website, value);
}
/// <summary>
/// Gets or sets the repository of this plugin
/// </summary>
[JsonProperty]
public Uri? Repository
{
get => _repository;
set => SetAndNotify(ref _repository, value);
}
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available icons
/// </summary>
[JsonProperty]
public string? Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
/// <summary>
/// The version of the plugin
/// </summary>
[JsonProperty(Required = Required.Always)]
public Version Version
{
get => _version;
internal set => SetAndNotify(ref _version, value);
}
/// <summary>
/// The main entry DLL, should contain a class implementing Plugin
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Main
{
get => _main;
internal set => SetAndNotify(ref _main, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this plugin should automatically enable all its features when it is first
/// loaded
/// </summary>
[DefaultValue(true)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool AutoEnableFeatures
{
get => _autoEnableFeatures;
set => SetAndNotify(ref _autoEnableFeatures, value);
}
/// <summary>
/// Gets a boolean indicating whether this plugin requires elevated admin privileges
/// </summary>
[JsonProperty]
public bool RequiresAdmin
{
get => _requiresAdmin;
internal set => SetAndNotify(ref _requiresAdmin, value);
}
/// <summary>
/// Gets
/// </summary>
[JsonProperty]
public PluginPlatform? Platforms
{
get => _platforms;
internal set => SetAndNotify(ref _platforms, value);
}
/// <summary>
/// Gets the API version the plugin was built for
/// </summary>
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public Version? Api
{
get => _api;
internal set => SetAndNotify(ref _api, value);
}
/// <summary>
/// Gets the plugin this info is associated with
/// </summary>
public Plugin Plugin
{
get => _plugin;
internal set => SetAndNotify(ref _plugin, value);
}
/// <summary>
/// Gets a string representing either a full path pointing to an svg or the markdown icon
/// </summary>
public string? ResolvedIcon
{
get
{
}
/// <summary>
/// The plugins GUID
/// </summary>
[JsonProperty(Required = Required.Always)]
public Guid Guid
{
get => _guid;
internal set => SetAndNotify(ref _guid, value);
}
/// <summary>
/// The name of the plugin
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Name
{
get => _name;
internal set => SetAndNotify(ref _name, value);
}
/// <summary>
/// A short description of the plugin
/// </summary>
[JsonProperty]
public string? Description
{
get => _description;
set => SetAndNotify(ref _description, value);
}
/// <summary>
/// Gets or sets the author of this plugin
/// </summary>
[JsonProperty]
public string? Author
{
get => _author;
set => SetAndNotify(ref _author, value);
}
/// <summary>
/// Gets or sets the website of this plugin or its author
/// </summary>
[JsonProperty]
public Uri? Website
{
get => _website;
set => SetAndNotify(ref _website, value);
}
/// <summary>
/// Gets or sets the repository of this plugin
/// </summary>
[JsonProperty]
public Uri? Repository
{
get => _repository;
set => SetAndNotify(ref _repository, value);
}
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available icons
/// </summary>
[JsonProperty]
public string? Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
/// <summary>
/// The version of the plugin
/// </summary>
[JsonProperty(Required = Required.Always)]
public Version Version
{
get => _version;
internal set => SetAndNotify(ref _version, value);
}
/// <summary>
/// The main entry DLL, should contain a class implementing Plugin
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Main
{
get => _main;
internal set => SetAndNotify(ref _main, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this plugin should automatically enable all its features when it is first
/// loaded
/// </summary>
[DefaultValue(true)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool AutoEnableFeatures
{
get => _autoEnableFeatures;
set => SetAndNotify(ref _autoEnableFeatures, value);
}
/// <summary>
/// Gets a boolean indicating whether this plugin requires elevated admin privileges
/// </summary>
[JsonProperty]
public bool RequiresAdmin
{
get => _requiresAdmin;
internal set => SetAndNotify(ref _requiresAdmin, value);
}
/// <summary>
/// Gets
/// </summary>
[JsonProperty]
public PluginPlatform? Platforms
{
get => _platforms;
internal set => SetAndNotify(ref _platforms , value);
}
/// <summary>
/// Gets the API version the plugin was built for
/// </summary>
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public Version? Api
{
get => _api;
internal set => SetAndNotify(ref _api, value);
}
/// <summary>
/// Gets the plugin this info is associated with
/// </summary>
public Plugin Plugin
{
get => _plugin;
internal set => SetAndNotify(ref _plugin, value);
}
/// <summary>
/// Gets a string representing either a full path pointing to an svg or the markdown icon
/// </summary>
public string? ResolvedIcon
{
get
{
if (Icon == null)
return null;
return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon;
}
}
/// <summary>
/// Gets a boolean indicating whether this plugin is compatible with the current operating system and API version
/// </summary>
public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem() && Api != null && Api >= Constants.PluginApi;
internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}";
/// <inheritdoc />
public override string ToString()
{
return $"{Name} v{Version} - {Guid}";
}
/// <inheritdoc />
public List<PluginPrerequisite> Prerequisites { get; } = new();
/// <inheritdoc />
public IEnumerable<PluginPrerequisite> PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform());
/// <inheritdoc />
public bool ArePrerequisitesMet()
{
return PlatformPrerequisites.All(p => p.IsMet());
if (Icon == null)
return null;
return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon;
}
}
/// <summary>
/// Gets a boolean indicating whether this plugin is compatible with the current operating system and API version
/// </summary>
public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem() && Api != null && Api >= Constants.PluginApi;
internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}";
/// <inheritdoc />
public override string ToString()
{
return $"{Name} v{Version} - {Guid}";
}
/// <inheritdoc />
public List<PluginPrerequisite> Prerequisites { get; } = new();
/// <inheritdoc />
public IEnumerable<PluginPrerequisite> PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform());
/// <inheritdoc />
public bool ArePrerequisitesMet()
{
return PlatformPrerequisites.All(p => p.IsMet());
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Artemis.Core;
@@ -7,7 +8,7 @@ namespace Artemis.Core;
/// Specifies OS platforms a plugin may support.
/// </summary>
[Flags]
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
[JsonConverter(typeof(StringEnumConverter))]
public enum PluginPlatform
{
/// <summary>The Windows platform.</summary>
@@ -1,25 +1,24 @@
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a type that has prerequisites
/// </summary>
public interface IPrerequisitesSubject
{
/// <summary>
/// Represents a type that has prerequisites
/// Gets a list of prerequisites for this plugin
/// </summary>
public interface IPrerequisitesSubject
{
/// <summary>
/// Gets a list of prerequisites for this plugin
/// </summary>
List<PluginPrerequisite> Prerequisites { get; }
/// <summary>
/// Gets a list of prerequisites of the current platform for this plugin
/// </summary>
IEnumerable<PluginPrerequisite> PlatformPrerequisites { get; }
List<PluginPrerequisite> Prerequisites { get; }
/// <summary>
/// Determines whether the prerequisites of this plugin are met
/// </summary>
bool ArePrerequisitesMet();
}
/// <summary>
/// Gets a list of prerequisites of the current platform for this plugin
/// </summary>
IEnumerable<PluginPrerequisite> PlatformPrerequisites { get; }
/// <summary>
/// Determines whether the prerequisites of this plugin are met
/// </summary>
bool ArePrerequisitesMet();
}
@@ -1,135 +1,133 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a prerequisite for a <see cref="Plugin" /> or <see cref="PluginFeature" />
/// </summary>
public abstract class PluginPrerequisite : CorePropertyChanged
{
private PluginPrerequisiteAction? _currentAction;
/// <summary>
/// Represents a prerequisite for a <see cref="Plugin" /> or <see cref="PluginFeature" />
/// Gets the name of the prerequisite
/// </summary>
public abstract class PluginPrerequisite : CorePropertyChanged
public abstract string Name { get; }
/// <summary>
/// Gets the description of the prerequisite
/// </summary>
public abstract string Description { get; }
/// <summary>
/// Gets or sets the platform(s) this prerequisite applies to.
/// </summary>
public PluginPlatform? Platform { get; protected set; }
/// <summary>
/// Gets a list of actions to execute when <see cref="Install" /> is called
/// </summary>
public abstract List<PluginPrerequisiteAction> InstallActions { get; }
/// <summary>
/// Gets a list of actions to execute when <see cref="Uninstall" /> is called
/// </summary>
public abstract List<PluginPrerequisiteAction> UninstallActions { get; }
/// <summary>
/// Gets or sets the action currently being executed
/// </summary>
public PluginPrerequisiteAction? CurrentAction
{
private PluginPrerequisiteAction? _currentAction;
get => _currentAction;
private set => SetAndNotify(ref _currentAction, value);
}
/// <summary>
/// Gets the name of the prerequisite
/// </summary>
public abstract string Name { get; }
/// <summary>
/// Gets the description of the prerequisite
/// </summary>
public abstract string Description { get; }
/// <summary>
/// Gets or sets the platform(s) this prerequisite applies to.
/// </summary>
public PluginPlatform? Platform { get; protected set; }
/// <summary>
/// Gets a list of actions to execute when <see cref="Install" /> is called
/// </summary>
public abstract List<PluginPrerequisiteAction> InstallActions { get; }
/// <summary>
/// Gets a list of actions to execute when <see cref="Uninstall" /> is called
/// </summary>
public abstract List<PluginPrerequisiteAction> UninstallActions { get; }
/// <summary>
/// Gets or sets the action currently being executed
/// </summary>
public PluginPrerequisiteAction? CurrentAction
/// <summary>
/// Execute all install actions
/// </summary>
public async Task Install(CancellationToken cancellationToken)
{
try
{
get => _currentAction;
private set => SetAndNotify(ref _currentAction, value);
}
/// <summary>
/// Execute all install actions
/// </summary>
public async Task Install(CancellationToken cancellationToken)
{
try
OnInstallStarting();
foreach (PluginPrerequisiteAction installAction in InstallActions)
{
OnInstallStarting();
foreach (PluginPrerequisiteAction installAction in InstallActions)
{
cancellationToken.ThrowIfCancellationRequested();
CurrentAction = installAction;
await installAction.Execute(cancellationToken);
}
}
finally
{
CurrentAction = null;
OnInstallFinished();
cancellationToken.ThrowIfCancellationRequested();
CurrentAction = installAction;
await installAction.Execute(cancellationToken);
}
}
/// <summary>
/// Execute all uninstall actions
/// </summary>
public async Task Uninstall(CancellationToken cancellationToken)
{
try
{
OnUninstallStarting();
foreach (PluginPrerequisiteAction uninstallAction in UninstallActions)
{
cancellationToken.ThrowIfCancellationRequested();
CurrentAction = uninstallAction;
await uninstallAction.Execute(cancellationToken);
}
}
finally
{
CurrentAction = null;
OnUninstallFinished();
}
}
/// <summary>
/// Called to determine whether the prerequisite is met
/// </summary>
/// <returns><see langword="true" /> if the prerequisite is met; otherwise <see langword="false" /></returns>
public abstract bool IsMet();
/// <summary>
/// Determines whether this prerequisite applies to the current operating system.
/// </summary>
public bool AppliesToPlatform()
{
return Platform.MatchesCurrentOperatingSystem();
}
/// <summary>
/// Called before installation starts
/// </summary>
protected virtual void OnInstallStarting()
{
}
/// <summary>
/// Called after installation finishes
/// </summary>
protected virtual void OnInstallFinished()
{
}
/// <summary>
/// Called before uninstall starts
/// </summary>
protected virtual void OnUninstallStarting()
{
}
/// <summary>
/// Called after uninstall finished
/// </summary>
protected virtual void OnUninstallFinished()
finally
{
CurrentAction = null;
OnInstallFinished();
}
}
/// <summary>
/// Execute all uninstall actions
/// </summary>
public async Task Uninstall(CancellationToken cancellationToken)
{
try
{
OnUninstallStarting();
foreach (PluginPrerequisiteAction uninstallAction in UninstallActions)
{
cancellationToken.ThrowIfCancellationRequested();
CurrentAction = uninstallAction;
await uninstallAction.Execute(cancellationToken);
}
}
finally
{
CurrentAction = null;
OnUninstallFinished();
}
}
/// <summary>
/// Called to determine whether the prerequisite is met
/// </summary>
/// <returns><see langword="true" /> if the prerequisite is met; otherwise <see langword="false" /></returns>
public abstract bool IsMet();
/// <summary>
/// Determines whether this prerequisite applies to the current operating system.
/// </summary>
public bool AppliesToPlatform()
{
return Platform.MatchesCurrentOperatingSystem();
}
/// <summary>
/// Called before installation starts
/// </summary>
protected virtual void OnInstallStarting()
{
}
/// <summary>
/// Called after installation finishes
/// </summary>
protected virtual void OnInstallFinished()
{
}
/// <summary>
/// Called before uninstall starts
/// </summary>
protected virtual void OnUninstallStarting()
{
}
/// <summary>
/// Called after uninstall finished
/// </summary>
protected virtual void OnUninstallFinished()
{
}
}
@@ -2,95 +2,94 @@
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an action that must be taken to install or uninstall a plugin prerequisite
/// </summary>
public abstract class PluginPrerequisiteAction : CorePropertyChanged
{
private bool _progressIndeterminate;
private bool _showProgressBar;
private bool _showSubProgressBar;
private string? _status;
private bool _subProgressIndeterminate;
/// <summary>
/// Represents an action that must be taken to install or uninstall a plugin prerequisite
/// The base constructor for all plugin prerequisite actions
/// </summary>
public abstract class PluginPrerequisiteAction : CorePropertyChanged
/// <param name="name">The name of the action</param>
protected PluginPrerequisiteAction(string name)
{
private bool _progressIndeterminate;
private bool _showProgressBar;
private bool _showSubProgressBar;
private string? _status;
private bool _subProgressIndeterminate;
/// <summary>
/// The base constructor for all plugin prerequisite actions
/// </summary>
/// <param name="name">The name of the action</param>
protected PluginPrerequisiteAction(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
#region Implementation of IPluginPrerequisiteAction
/// <summary>
/// Gets the name of the action
/// </summary>
public string Name { get; }
/// <summary>
/// Gets or sets the status of the action
/// </summary>
public string? Status
{
get => _status;
set => SetAndNotify(ref _status, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress is indeterminate or not
/// </summary>
public bool ProgressIndeterminate
{
get => _progressIndeterminate;
set => SetAndNotify(ref _progressIndeterminate, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress is indeterminate or not
/// </summary>
public bool SubProgressIndeterminate
{
get => _subProgressIndeterminate;
set => SetAndNotify(ref _subProgressIndeterminate, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress bar should be shown
/// </summary>
public bool ShowProgressBar
{
get => _showProgressBar;
set => SetAndNotify(ref _showProgressBar, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the sub progress bar should be shown
/// </summary>
public bool ShowSubProgressBar
{
get => _showSubProgressBar;
set => SetAndNotify(ref _showSubProgressBar, value);
}
/// <summary>
/// Gets or sets the progress of the action (0 to 100)
/// </summary>
public PrerequisiteActionProgress Progress { get; } = new();
/// <summary>
/// Gets or sets the sub progress of the action
/// </summary>
public PrerequisiteActionProgress SubProgress { get; } = new();
/// <summary>
/// Called when the action must execute
/// </summary>
public abstract Task Execute(CancellationToken cancellationToken);
#endregion
Name = name ?? throw new ArgumentNullException(nameof(name));
}
#region Implementation of IPluginPrerequisiteAction
/// <summary>
/// Gets the name of the action
/// </summary>
public string Name { get; }
/// <summary>
/// Gets or sets the status of the action
/// </summary>
public string? Status
{
get => _status;
set => SetAndNotify(ref _status, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress is indeterminate or not
/// </summary>
public bool ProgressIndeterminate
{
get => _progressIndeterminate;
set => SetAndNotify(ref _progressIndeterminate, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress is indeterminate or not
/// </summary>
public bool SubProgressIndeterminate
{
get => _subProgressIndeterminate;
set => SetAndNotify(ref _subProgressIndeterminate, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress bar should be shown
/// </summary>
public bool ShowProgressBar
{
get => _showProgressBar;
set => SetAndNotify(ref _showProgressBar, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the sub progress bar should be shown
/// </summary>
public bool ShowSubProgressBar
{
get => _showSubProgressBar;
set => SetAndNotify(ref _showSubProgressBar, value);
}
/// <summary>
/// Gets or sets the progress of the action (0 to 100)
/// </summary>
public PrerequisiteActionProgress Progress { get; } = new();
/// <summary>
/// Gets or sets the sub progress of the action
/// </summary>
public PrerequisiteActionProgress SubProgress { get; } = new();
/// <summary>
/// Called when the action must execute
/// </summary>
public abstract Task Execute(CancellationToken cancellationToken);
#endregion
}
@@ -4,78 +4,77 @@ using System.Threading;
using System.Threading.Tasks;
using Humanizer;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// </summary>
public class CopyFolderAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// Creates a new instance of a copy folder action
/// </summary>
public class CopyFolderAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="source">The source folder to copy</param>
/// <param name="target">The target folder to copy to (will be created if needed)</param>
public CopyFolderAction(string name, string source, string target) : base(name)
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="source">The source folder to copy</param>
/// <param name="target">The target folder to copy to (will be created if needed)</param>
public CopyFolderAction(string name, string source, string target) : base(name)
Source = source;
Target = target;
ShowProgressBar = true;
ShowSubProgressBar = true;
}
/// <summary>
/// Gets the source directory
/// </summary>
public string Source { get; }
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
DirectoryInfo source = new(Source);
DirectoryInfo target = new(Target);
if (!source.Exists)
throw new ArtemisCoreException($"The source directory at '{source}' was not found.");
int filesCopied = 0;
FileInfo[] files = source.GetFiles("*", SearchOption.AllDirectories);
foreach (FileInfo fileInfo in files)
{
Source = source;
Target = target;
string outputPath = fileInfo.FullName.Replace(source.FullName, target.FullName);
string outputDir = Path.GetDirectoryName(outputPath)!;
Utilities.CreateAccessibleDirectory(outputDir);
ShowProgressBar = true;
ShowSubProgressBar = true;
}
/// <summary>
/// Gets the source directory
/// </summary>
public string Source { get; }
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
DirectoryInfo source = new(Source);
DirectoryInfo target = new(Target);
if (!source.Exists)
throw new ArtemisCoreException($"The source directory at '{source}' was not found.");
int filesCopied = 0;
FileInfo[] files = source.GetFiles("*", SearchOption.AllDirectories);
foreach (FileInfo fileInfo in files)
void SubProgressOnProgressReported(object? sender, EventArgs e)
{
string outputPath = fileInfo.FullName.Replace(source.FullName, target.FullName);
string outputDir = Path.GetDirectoryName(outputPath)!;
Utilities.CreateAccessibleDirectory(outputDir);
void SubProgressOnProgressReported(object? sender, EventArgs e)
{
if (SubProgress.ProgressPerSecond != 0)
Status = $"Copying {fileInfo.Name} - {SubProgress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
else
Status = $"Copying {fileInfo.Name}";
}
Progress.Report((filesCopied, files.Length));
SubProgress.ProgressReported += SubProgressOnProgressReported;
await using FileStream sourceStream = fileInfo.OpenRead();
await using FileStream destinationStream = File.Create(outputPath);
await sourceStream.CopyToAsync(fileInfo.Length, destinationStream, SubProgress, cancellationToken);
filesCopied++;
SubProgress.ProgressReported -= SubProgressOnProgressReported;
if (SubProgress.ProgressPerSecond != 0)
Status = $"Copying {fileInfo.Name} - {SubProgress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
else
Status = $"Copying {fileInfo.Name}";
}
Progress.Report((filesCopied, files.Length));
Status = $"Finished copying {filesCopied} file(s)";
SubProgress.ProgressReported += SubProgressOnProgressReported;
await using FileStream sourceStream = fileInfo.OpenRead();
await using FileStream destinationStream = File.Create(outputPath);
await sourceStream.CopyToAsync(fileInfo.Length, destinationStream, SubProgress, cancellationToken);
filesCopied++;
SubProgress.ProgressReported -= SubProgressOnProgressReported;
}
Progress.Report((filesCopied, files.Length));
Status = $"Finished copying {filesCopied} file(s)";
}
}
@@ -2,43 +2,42 @@
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that deletes a file
/// </summary>
public class DeleteFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that deletes a file
/// Creates a new instance of a copy folder action
/// </summary>
public class DeleteFileAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="target">The target folder to delete recursively</param>
public DeleteFileAction(string name, string target) : base(name)
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="target">The target folder to delete recursively</param>
public DeleteFileAction(string name, string target) : base(name)
Target = target;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
ShowProgressBar = true;
Status = $"Removing {Target}";
await Task.Run(() =>
{
Target = target;
ProgressIndeterminate = true;
}
if (File.Exists(Target))
File.Delete(Target);
}, cancellationToken);
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
ShowProgressBar = true;
Status = $"Removing {Target}";
await Task.Run(() =>
{
if (File.Exists(Target))
File.Delete(Target);
}, cancellationToken);
ShowProgressBar = false;
Status = $"Removed {Target}";
}
ShowProgressBar = false;
Status = $"Removed {Target}";
}
}
@@ -4,46 +4,45 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that recursively deletes a folder
/// </summary>
public class DeleteFolderAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that recursively deletes a folder
/// Creates a new instance of a copy folder action
/// </summary>
public class DeleteFolderAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="target">The target folder to delete recursively</param>
public DeleteFolderAction(string name, string target) : base(name)
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="target">The target folder to delete recursively</param>
public DeleteFolderAction(string name, string target) : base(name)
if (Enum.GetValues<Environment.SpecialFolder>().Select(Environment.GetFolderPath).Contains(target))
throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose.");
Target = target;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
ShowProgressBar = true;
Status = $"Removing {Target}";
await Task.Run(() =>
{
if (Enum.GetValues<Environment.SpecialFolder>().Select(Environment.GetFolderPath).Contains(target))
throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose.");
if (Directory.Exists(Target))
Directory.Delete(Target, true);
}, cancellationToken);
Target = target;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
ShowProgressBar = true;
Status = $"Removing {Target}";
await Task.Run(() =>
{
if (Directory.Exists(Target))
Directory.Delete(Target, true);
}, cancellationToken);
ShowProgressBar = false;
Status = $"Removed {Target}";
}
ShowProgressBar = false;
Status = $"Removed {Target}";
}
}
@@ -5,102 +5,101 @@ using System.Threading;
using System.Threading.Tasks;
using Humanizer;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that downloads a file
/// </summary>
public class DownloadFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that downloads a file
/// Creates a new instance of a copy folder action
/// </summary>
public class DownloadFileAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="url">The source URL to download</param>
/// <param name="fileName">The target file to save as (will be created if needed)</param>
public DownloadFileAction(string name, string url, string fileName) : base(name)
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="url">The source URL to download</param>
/// <param name="fileName">The target file to save as (will be created if needed)</param>
public DownloadFileAction(string name, string url, string fileName) : base(name)
{
Url = url ?? throw new ArgumentNullException(nameof(url));
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
Url = url ?? throw new ArgumentNullException(nameof(url));
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
ShowProgressBar = true;
ShowProgressBar = true;
}
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="urlFunction">A function returning the URL to download</param>
/// <param name="fileName">The target file to save as (will be created if needed)</param>
public DownloadFileAction(string name, Func<Task<string>> urlFunction, string fileName) : base(name)
{
UrlFunction = urlFunction ?? throw new ArgumentNullException(nameof(urlFunction));
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
ShowProgressBar = true;
}
/// <summary>
/// Gets the source URL to download
/// </summary>
public string? Url { get; }
/// <summary>
/// Gets the function returning the URL to download
/// </summary>
public Func<Task<string>>? UrlFunction { get; }
/// <summary>
/// Gets the target file to save as (will be created if needed)
/// </summary>
public string FileName { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
using HttpClient client = new();
await using FileStream destinationStream = new(FileName, FileMode.OpenOrCreate);
string? url = Url;
if (url is null)
{
Status = "Retrieving download URL";
url = await UrlFunction!();
}
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="urlFunction">A function returning the URL to download</param>
/// <param name="fileName">The target file to save as (will be created if needed)</param>
public DownloadFileAction(string name, Func<Task<string>> urlFunction, string fileName) : base(name)
void ProgressOnProgressReported(object? sender, EventArgs e)
{
UrlFunction = urlFunction ?? throw new ArgumentNullException(nameof(urlFunction));
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
ShowProgressBar = true;
}
/// <summary>
/// Gets the source URL to download
/// </summary>
public string? Url { get; }
/// <summary>
/// Gets the function returning the URL to download
/// </summary>
public Func<Task<string>>? UrlFunction { get; }
/// <summary>
/// Gets the target file to save as (will be created if needed)
/// </summary>
public string FileName { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
using HttpClient client = new();
await using FileStream destinationStream = new(FileName, FileMode.OpenOrCreate);
string? url = Url;
if (url is null)
{
Status = "Retrieving download URL";
url = await UrlFunction!();
}
void ProgressOnProgressReported(object? sender, EventArgs e)
{
if (Progress.ProgressPerSecond != 0)
Status = $"Downloading {url} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
else
Status = $"Downloading {url}";
}
Progress.ProgressReported += ProgressOnProgressReported;
// Get the http headers first to examine the content length
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken);
long? contentLength = response.Content.Headers.ContentLength;
// Ignore progress reporting when no progress reporter was
// passed or when the content length is unknown
if (!contentLength.HasValue)
{
ProgressIndeterminate = true;
await download.CopyToAsync(destinationStream, Progress, cancellationToken);
ProgressIndeterminate = false;
}
if (Progress.ProgressPerSecond != 0)
Status = $"Downloading {url} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
else
{
ProgressIndeterminate = false;
await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
Progress.ProgressReported -= ProgressOnProgressReported;
Progress.Report((1, 1));
Status = "Finished downloading";
Status = $"Downloading {url}";
}
Progress.ProgressReported += ProgressOnProgressReported;
// Get the http headers first to examine the content length
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken);
long? contentLength = response.Content.Headers.ContentLength;
// Ignore progress reporting when no progress reporter was
// passed or when the content length is unknown
if (!contentLength.HasValue)
{
ProgressIndeterminate = true;
await download.CopyToAsync(destinationStream, Progress, cancellationToken);
ProgressIndeterminate = false;
}
else
{
ProgressIndeterminate = false;
await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
Progress.ProgressReported -= ProgressOnProgressReported;
Progress.Report((1, 1));
Status = "Finished downloading";
}
}
@@ -4,109 +4,108 @@ using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that executes a file
/// </summary>
public class ExecuteFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that executes a file
/// Creates a new instance of <see cref="ExecuteFileAction" />
/// </summary>
public class ExecuteFileAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="fileName">The target file to execute</param>
/// <param name="arguments">A set of command-line arguments to use when starting the application</param>
/// <param name="waitForExit">A boolean indicating whether the action should wait for the process to exit</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name)
{
/// <summary>
/// Creates a new instance of <see cref="ExecuteFileAction" />
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="fileName">The target file to execute</param>
/// <param name="arguments">A set of command-line arguments to use when starting the application</param>
/// <param name="waitForExit">A boolean indicating whether the action should wait for the process to exit</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name)
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
Arguments = arguments;
WaitForExit = waitForExit;
Elevate = elevate;
}
/// <summary>
/// Gets the target file to execute
/// </summary>
public string FileName { get; }
/// <summary>
/// Gets a set of command-line arguments to use when starting the application
/// </summary>
public string? Arguments { get; }
/// <summary>
/// Gets a boolean indicating whether the action should wait for the process to exit
/// </summary>
public bool WaitForExit { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
if (WaitForExit)
{
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
Arguments = arguments;
WaitForExit = waitForExit;
Elevate = elevate;
Status = $"Running {FileName} and waiting for exit..";
ShowProgressBar = true;
ProgressIndeterminate = true;
int result = await RunProcessAsync(FileName, Arguments, Elevate);
Status = $"{FileName} exited with code {result}";
}
/// <summary>
/// Gets the target file to execute
/// </summary>
public string FileName { get; }
/// <summary>
/// Gets a set of command-line arguments to use when starting the application
/// </summary>
public string? Arguments { get; }
/// <summary>
/// Gets a boolean indicating whether the action should wait for the process to exit
/// </summary>
public bool WaitForExit { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
else
{
if (WaitForExit)
{
Status = $"Running {FileName} and waiting for exit..";
ShowProgressBar = true;
ProgressIndeterminate = true;
int result = await RunProcessAsync(FileName, Arguments, Elevate);
Status = $"{FileName} exited with code {result}";
}
else
{
Status = $"Running {FileName}";
Process process = new()
{
StartInfo = {FileName = FileName, Arguments = Arguments!},
EnableRaisingEvents = true
};
process.Start();
}
}
internal static Task<int> RunProcessAsync(string fileName, string? arguments, bool elevate)
{
TaskCompletionSource<int> tcs = new();
Status = $"Running {FileName}";
Process process = new()
{
StartInfo =
{
FileName = fileName,
Arguments = arguments!,
Verb = elevate ? "RunAs" : "",
UseShellExecute = elevate
},
StartInfo = {FileName = FileName, Arguments = Arguments!},
EnableRaisingEvents = true
};
process.Exited += (_, _) =>
{
tcs.SetResult(process.ExitCode);
process.Dispose();
};
try
{
process.Start();
}
catch (Win32Exception e)
{
if (!elevate || e.NativeErrorCode != 0x4c7)
throw;
tcs.SetResult(-1);
}
return tcs.Task;
process.Start();
}
}
internal static Task<int> RunProcessAsync(string fileName, string? arguments, bool elevate)
{
TaskCompletionSource<int> tcs = new();
Process process = new()
{
StartInfo =
{
FileName = fileName,
Arguments = arguments!,
Verb = elevate ? "RunAs" : "",
UseShellExecute = elevate
},
EnableRaisingEvents = true
};
process.Exited += (_, _) =>
{
tcs.SetResult(process.ExitCode);
process.Dispose();
};
try
{
process.Start();
}
catch (Win32Exception e)
{
if (!elevate || e.NativeErrorCode != 0x4c7)
throw;
tcs.SetResult(-1);
}
return tcs.Task;
}
}
@@ -7,89 +7,88 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that extracts a ZIP file
/// </summary>
public class ExtractArchiveAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that extracts a ZIP file
/// Creates a new instance of <see cref="ExtractArchiveAction" />.
/// </summary>
public class ExtractArchiveAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="fileName">The ZIP file to extract</param>
/// <param name="target">The folder into which to extract the file</param>
public ExtractArchiveAction(string name, string fileName, string target) : base(name)
{
/// <summary>
/// Creates a new instance of <see cref="ExtractArchiveAction"/>.
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="fileName">The ZIP file to extract</param>
/// <param name="target">The folder into which to extract the file</param>
public ExtractArchiveAction(string name, string fileName, string target) : base(name)
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
Target = target ?? throw new ArgumentNullException(nameof(target));
ShowProgressBar = true;
}
/// <summary>
/// Gets the file to extract
/// </summary>
public string FileName { get; }
/// <summary>
/// Gets the folder into which to extract the file
/// </summary>
public string Target { get; }
/// <summary>
/// Gets or sets an optional list of files to extract, if <see langword="null" /> all files will be extracted.
/// </summary>
public List<string>? FilesToExtract { get; set; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
using HttpClient client = new();
ShowSubProgressBar = true;
Status = $"Extracting {FileName}";
Utilities.CreateAccessibleDirectory(Target);
await using (FileStream fileStream = new(FileName, FileMode.Open))
{
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
Target = target ?? throw new ArgumentNullException(nameof(target));
ZipArchive archive = new(fileStream);
long count = 0;
ShowProgressBar = true;
}
List<ZipArchiveEntry> entries = new(archive.Entries);
if (FilesToExtract != null)
entries = entries.Where(e => FilesToExtract.Contains(e.FullName)).ToList();
/// <summary>
/// Gets the file to extract
/// </summary>
public string FileName { get; }
/// <summary>
/// Gets the folder into which to extract the file
/// </summary>
public string Target { get; }
/// <summary>
/// Gets or sets an optional list of files to extract, if <see langword="null"/> all files will be extracted.
/// </summary>
public List<string>? FilesToExtract { get; set; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
using HttpClient client = new();
ShowSubProgressBar = true;
Status = $"Extracting {FileName}";
Utilities.CreateAccessibleDirectory(Target);
await using (FileStream fileStream = new(FileName, FileMode.Open))
foreach (ZipArchiveEntry entry in entries)
{
ZipArchive archive = new(fileStream);
long count = 0;
List<ZipArchiveEntry> entries = new(archive.Entries);
if (FilesToExtract != null)
entries = entries.Where(e => FilesToExtract.Contains(e.FullName)).ToList();
foreach (ZipArchiveEntry entry in entries)
await using Stream unzippedEntryStream = entry.Open();
Progress.Report((count, entries.Count));
if (entry.Length > 0)
{
await using Stream unzippedEntryStream = entry.Open();
Progress.Report((count, entries.Count));
if (entry.Length > 0)
{
string path = Path.Combine(Target, entry.FullName);
CreateDirectoryForFile(path);
await using Stream extractStream = new FileStream(path, FileMode.OpenOrCreate);
await unzippedEntryStream.CopyToAsync(entry.Length, extractStream, SubProgress, cancellationToken);
}
count++;
string path = Path.Combine(Target, entry.FullName);
CreateDirectoryForFile(path);
await using Stream extractStream = new FileStream(path, FileMode.OpenOrCreate);
await unzippedEntryStream.CopyToAsync(entry.Length, extractStream, SubProgress, cancellationToken);
}
count++;
}
Progress.Report((1, 1));
ShowSubProgressBar = false;
Status = "Finished extracting";
}
private static void CreateDirectoryForFile(string path)
{
string? directory = Path.GetDirectoryName(path);
if (directory == null)
throw new ArtemisCoreException($"Failed to get directory from path {path}");
Utilities.CreateAccessibleDirectory(directory);
}
Progress.Report((1, 1));
ShowSubProgressBar = false;
Status = "Finished extracting";
}
private static void CreateDirectoryForFile(string path)
{
string? directory = Path.GetDirectoryName(path);
if (directory == null)
throw new ArtemisCoreException($"Failed to get directory from path {path}");
Utilities.CreateAccessibleDirectory(directory);
}
}
@@ -2,55 +2,55 @@
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action runs inline powershell
/// </summary>
public class RunInlinePowerShellAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action runs inline powershell
/// Creates a new instance of a copy folder action
/// </summary>
public class RunInlinePowerShellAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="code">The inline code to run</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
/// <param name="arguments">
/// Optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </param>
public RunInlinePowerShellAction(string name, string code, bool elevate = false, string? arguments = null) : base(name)
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="code">The inline code to run</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
/// <param name="arguments">
/// Optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </param>
public RunInlinePowerShellAction(string name, string code, bool elevate = false, string? arguments = null) : base(name)
Code = code;
Elevate = elevate;
Arguments = arguments;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets the inline code to run
/// </summary>
public string Code { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <summary>
/// Gets optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </summary>
public string? Arguments { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
string file = Path.GetTempFileName().Replace(".tmp", ".ps1");
try
{
Code = code;
Elevate = elevate;
Arguments = arguments;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets the inline code to run
/// </summary>
public string Code { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <summary>
/// Gets optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </summary>
public string? Arguments { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
string file = Path.GetTempFileName().Replace(".tmp", ".ps1");
try
{
string code =
@"try
string code =
@"try
{
" + Code + @"
Start-Sleep 1
@@ -60,22 +60,21 @@ namespace Artemis.Core
Write-Error $_.Exception.ToString()
pause
}";
await File.WriteAllTextAsync(file, code, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
Status = "Running PowerShell script and waiting for exit..";
ShowProgressBar = true;
ProgressIndeterminate = true;
await File.WriteAllTextAsync(file, code, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {file} {Arguments}", Elevate);
Status = "Running PowerShell script and waiting for exit..";
ShowProgressBar = true;
ProgressIndeterminate = true;
Status = $"PowerShell exited with code {result}";
}
finally
{
File.Delete(file);
}
int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {file} {Arguments}", Elevate);
Status = $"PowerShell exited with code {result}";
}
finally
{
File.Delete(file);
}
}
}
@@ -2,63 +2,62 @@
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that runs a PowerShell script
/// <para>Note: To run an inline script instead, use <see cref="RunInlinePowerShellAction" /></para>
/// </summary>
public class RunPowerShellAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that runs a PowerShell script
/// <para>Note: To run an inline script instead, use <see cref="RunInlinePowerShellAction" /></para>
/// Creates a new instance of a copy folder action
/// </summary>
public class RunPowerShellAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="scriptPath">The full path of the script to run</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
/// <param name="arguments">
/// Optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </param>
public RunPowerShellAction(string name, string scriptPath, bool elevate = false, string? arguments = null) : base(name)
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="scriptPath">The full path of the script to run</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
/// <param name="arguments">
/// Optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </param>
public RunPowerShellAction(string name, string scriptPath, bool elevate = false, string? arguments = null) : base(name)
{
ScriptPath = scriptPath;
Elevate = elevate;
Arguments = arguments;
ProgressIndeterminate = true;
}
ScriptPath = scriptPath;
Elevate = elevate;
Arguments = arguments;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets the inline full path of the script to run
/// </summary>
public string ScriptPath { get; }
/// <summary>
/// Gets the inline full path of the script to run
/// </summary>
public string ScriptPath { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <summary>
/// Gets optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </summary>
public string? Arguments { get; }
/// <summary>
/// Gets optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </summary>
public string? Arguments { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
if (!ScriptPath.EndsWith(".ps1"))
throw new ArtemisPluginException($"Script at path {ScriptPath} must have the .ps1 extension or PowerShell will refuse to run it");
if (!File.Exists(ScriptPath))
throw new ArtemisCoreException($"Script not found at path {ScriptPath}");
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
if (!ScriptPath.EndsWith(".ps1"))
throw new ArtemisPluginException($"Script at path {ScriptPath} must have the .ps1 extension or PowerShell will refuse to run it");
if (!File.Exists(ScriptPath))
throw new ArtemisCoreException($"Script not found at path {ScriptPath}");
Status = "Running PowerShell script and waiting for exit..";
ShowProgressBar = true;
ProgressIndeterminate = true;
Status = "Running PowerShell script and waiting for exit..";
ShowProgressBar = true;
ProgressIndeterminate = true;
int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {ScriptPath} {Arguments}", Elevate);
int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {ScriptPath} {Arguments}", Elevate);
Status = $"PowerShell exited with code {result}";
}
Status = $"PowerShell exited with code {result}";
}
}
@@ -1,62 +1,60 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// </summary>
public class WriteBytesToFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// Creates a new instance of a copy folder action
/// </summary>
public class WriteBytesToFileAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="target">The target file to write to (will be created if needed)</param>
/// <param name="content">The contents to write</param>
public WriteBytesToFileAction(string name, string target, byte[] content) : base(name)
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="target">The target file to write to (will be created if needed)</param>
/// <param name="content">The contents to write</param>
public WriteBytesToFileAction(string name, string target, byte[] content) : base(name)
{
Target = target;
ByteContent = content ?? throw new ArgumentNullException(nameof(content));
}
Target = target;
ByteContent = content ?? throw new ArgumentNullException(nameof(content));
}
/// <summary>
/// Gets or sets the target file
/// </summary>
public string Target { get; }
/// <summary>
/// Gets or sets the target file
/// </summary>
public string Target { get; }
/// <summary>
/// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to
/// <see langword="false" /> the file will be deleted and recreated
/// </summary>
public bool Append { get; set; } = false;
/// <summary>
/// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to
/// <see langword="false" /> the file will be deleted and recreated
/// </summary>
public bool Append { get; set; } = false;
/// <summary>
/// Gets the bytes that will be written
/// </summary>
public byte[] ByteContent { get; }
/// <summary>
/// Gets the bytes that will be written
/// </summary>
public byte[] ByteContent { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
string outputDir = Path.GetDirectoryName(Target)!;
Utilities.CreateAccessibleDirectory(outputDir);
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
string outputDir = Path.GetDirectoryName(Target)!;
Utilities.CreateAccessibleDirectory(outputDir);
ShowProgressBar = true;
Status = $"Writing to {Path.GetFileName(Target)}...";
ShowProgressBar = true;
Status = $"Writing to {Path.GetFileName(Target)}...";
if (!Append && File.Exists(Target))
File.Delete(Target);
await using Stream fileStream = File.OpenWrite(Target);
await using MemoryStream sourceStream = new(ByteContent);
await sourceStream.CopyToAsync(sourceStream.Length, fileStream, Progress, cancellationToken);
if (!Append && File.Exists(Target))
File.Delete(Target);
ShowProgressBar = false;
Status = $"Finished writing to {Path.GetFileName(Target)}";
}
await using Stream fileStream = File.OpenWrite(Target);
await using MemoryStream sourceStream = new(ByteContent);
await sourceStream.CopyToAsync(sourceStream.Length, fileStream, Progress, cancellationToken);
ShowProgressBar = false;
Status = $"Finished writing to {Path.GetFileName(Target)}";
}
}
@@ -1,62 +1,60 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// </summary>
public class WriteStringToFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// Creates a new instance of a copy folder action
/// </summary>
public class WriteStringToFileAction : PluginPrerequisiteAction
/// <param name="name">The name of the action</param>
/// <param name="target">The target file to write to (will be created if needed)</param>
/// <param name="content">The contents to write</param>
public WriteStringToFileAction(string name, string target, string content) : base(name)
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="target">The target file to write to (will be created if needed)</param>
/// <param name="content">The contents to write</param>
public WriteStringToFileAction(string name, string target, string content) : base(name)
{
Target = target;
Content = content ?? throw new ArgumentNullException(nameof(content));
Target = target;
Content = content ?? throw new ArgumentNullException(nameof(content));
ProgressIndeterminate = true;
}
ProgressIndeterminate = true;
}
/// <summary>
/// Gets or sets the target file
/// </summary>
public string Target { get; }
/// <summary>
/// Gets or sets the target file
/// </summary>
public string Target { get; }
/// <summary>
/// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to
/// <see langword="false" /> the file will be deleted and recreated
/// </summary>
public bool Append { get; set; } = false;
/// <summary>
/// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to
/// <see langword="false" /> the file will be deleted and recreated
/// </summary>
public bool Append { get; set; } = false;
/// <summary>
/// Gets the string that will be written
/// </summary>
public string Content { get; }
/// <summary>
/// Gets the string that will be written
/// </summary>
public string Content { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
string outputDir = Path.GetDirectoryName(Target)!;
Utilities.CreateAccessibleDirectory(outputDir);
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
string outputDir = Path.GetDirectoryName(Target)!;
Utilities.CreateAccessibleDirectory(outputDir);
ShowProgressBar = true;
Status = $"Writing to {Path.GetFileName(Target)}...";
ShowProgressBar = true;
Status = $"Writing to {Path.GetFileName(Target)}...";
if (Append)
await File.AppendAllTextAsync(Target, Content, cancellationToken);
else
await File.WriteAllTextAsync(Target, Content, cancellationToken);
if (Append)
await File.AppendAllTextAsync(Target, Content, cancellationToken);
else
await File.WriteAllTextAsync(Target, Content, cancellationToken);
ShowProgressBar = false;
Status = $"Finished writing to {Path.GetFileName(Target)}";
}
ShowProgressBar = false;
Status = $"Finished writing to {Path.GetFileName(Target)}";
}
}
@@ -1,94 +1,89 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents progress on a plugin prerequisite action
/// </summary>
public class PrerequisiteActionProgress : CorePropertyChanged, IProgress<(long, long)>
{
private long _current;
private DateTime _lastReport;
private long _lastReportValue;
private double _percentage;
private double _progressPerSecond;
private long _total;
/// <summary>
/// Represents progress on a plugin prerequisite action
/// The current amount
/// </summary>
public class PrerequisiteActionProgress : CorePropertyChanged, IProgress<(long, long)>
public long Current
{
private long _current;
private DateTime _lastReport;
private long _lastReportValue;
private double _percentage;
private double _progressPerSecond;
private long _total;
/// <summary>
/// The current amount
/// </summary>
public long Current
{
get => _current;
set => SetAndNotify(ref _current, value);
}
/// <summary>
/// The total amount
/// </summary>
public long Total
{
get => _total;
set => SetAndNotify(ref _total, value);
}
/// <summary>
/// The percentage
/// </summary>
public double Percentage
{
get => _percentage;
set => SetAndNotify(ref _percentage, value);
}
/// <summary>
/// Gets or sets the progress per second
/// </summary>
public double ProgressPerSecond
{
get => _progressPerSecond;
set => SetAndNotify(ref _progressPerSecond, value);
}
#region Implementation of IProgress<in (long, long)>
/// <inheritdoc />
public void Report((long, long) value)
{
(long newCurrent, long newTotal) = value;
TimeSpan timePassed = DateTime.Now - _lastReport;
if (timePassed >= TimeSpan.FromSeconds(1))
{
ProgressPerSecond = Math.Max(0, Math.Round(1.0 / timePassed.TotalSeconds * (newCurrent - _lastReportValue), 2));
_lastReportValue = newCurrent;
_lastReport = DateTime.Now;
}
Current = newCurrent;
Total = newTotal;
Percentage = Math.Round((double) Current / Total * 100.0, 2);
OnProgressReported();
}
#endregion
#region Events
/// <summary>
/// Occurs when progress has been reported
/// </summary>
public event EventHandler? ProgressReported;
/// <summary>
/// Invokes the <see cref="ProgressReported" /> event
/// </summary>
protected virtual void OnProgressReported()
{
ProgressReported?.Invoke(this, EventArgs.Empty);
}
#endregion
get => _current;
set => SetAndNotify(ref _current, value);
}
/// <summary>
/// The total amount
/// </summary>
public long Total
{
get => _total;
set => SetAndNotify(ref _total, value);
}
/// <summary>
/// The percentage
/// </summary>
public double Percentage
{
get => _percentage;
set => SetAndNotify(ref _percentage, value);
}
/// <summary>
/// Gets or sets the progress per second
/// </summary>
public double ProgressPerSecond
{
get => _progressPerSecond;
set => SetAndNotify(ref _progressPerSecond, value);
}
/// <summary>
/// Occurs when progress has been reported
/// </summary>
public event EventHandler? ProgressReported;
/// <summary>
/// Invokes the <see cref="ProgressReported" /> event
/// </summary>
protected virtual void OnProgressReported()
{
ProgressReported?.Invoke(this, EventArgs.Empty);
}
#region Implementation of IProgress<in (long, long)>
/// <inheritdoc />
public void Report((long, long) value)
{
(long newCurrent, long newTotal) = value;
TimeSpan timePassed = DateTime.Now - _lastReport;
if (timePassed >= TimeSpan.FromSeconds(1))
{
ProgressPerSecond = Math.Max(0, Math.Round(1.0 / timePassed.TotalSeconds * (newCurrent - _lastReportValue), 2));
_lastReportValue = newCurrent;
_lastReport = DateTime.Now;
}
Current = newCurrent;
Total = newTotal;
Percentage = Math.Round((double) Current / Total * 100.0, 2);
OnProgressReported();
}
#endregion
}
+65 -67
View File
@@ -1,84 +1,82 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a profiler that can measure time between calls distinguished by identifiers
/// </summary>
public class Profiler
{
/// <summary>
/// Represents a profiler that can measure time between calls distinguished by identifiers
/// </summary>
public class Profiler
internal Profiler(Plugin plugin, string name)
{
internal Profiler(Plugin plugin, string name)
Plugin = plugin;
Name = name;
}
/// <summary>
/// Gets the plugin this profiler belongs to
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the name of this profiler
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a dictionary containing measurements by their identifiers
/// </summary>
public Dictionary<string, ProfilingMeasurement> Measurements { get; set; } = new();
/// <summary>
/// Starts measuring time for the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier">A unique identifier for this measurement</param>
public void StartMeasurement(string identifier)
{
lock (Measurements)
{
Plugin = plugin;
Name = name;
}
/// <summary>
/// Gets the plugin this profiler belongs to
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the name of this profiler
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a dictionary containing measurements by their identifiers
/// </summary>
public Dictionary<string, ProfilingMeasurement> Measurements { get; set; } = new();
/// <summary>
/// Starts measuring time for the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier">A unique identifier for this measurement</param>
public void StartMeasurement(string identifier)
{
lock (Measurements)
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
measurement.Start();
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
measurement.Start();
}
}
/// <summary>
/// Stops measuring time for the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier">A unique identifier for this measurement</param>
/// <returns>The number of ticks that passed since the <see cref="StartMeasurement" /> call with the same identifier</returns>
public long StopMeasurement(string identifier)
/// <summary>
/// Stops measuring time for the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier">A unique identifier for this measurement</param>
/// <returns>The number of ticks that passed since the <see cref="StartMeasurement" /> call with the same identifier</returns>
public long StopMeasurement(string identifier)
{
long lockRequestedAt = Stopwatch.GetTimestamp();
lock (Measurements)
{
long lockRequestedAt = Stopwatch.GetTimestamp();
lock (Measurements)
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt);
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
}
/// <summary>
/// Clears measurements with the the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier"></param>
public void ClearMeasurements(string identifier)
return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt);
}
}
/// <summary>
/// Clears measurements with the the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier"></param>
public void ClearMeasurements(string identifier)
{
lock (Measurements)
{
lock (Measurements)
{
Measurements.Remove(identifier);
}
Measurements.Remove(identifier);
}
}
}
@@ -1,141 +1,139 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a set of profiling measurements
/// </summary>
public class ProfilingMeasurement
{
/// <summary>
/// Represents a set of profiling measurements
/// </summary>
public class ProfilingMeasurement
private bool _filledArray;
private int _index;
private long _last;
private bool _open;
private long _start;
internal ProfilingMeasurement(string identifier)
{
private bool _filledArray;
private int _index;
private long _last;
private bool _open;
private long _start;
Identifier = identifier;
}
internal ProfilingMeasurement(string identifier)
/// <summary>
/// Gets the unique identifier of this measurement
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the last 1000 measurements
/// </summary>
public long[] Measurements { get; } = new long[1000];
/// <summary>
/// Starts measuring time until <see cref="Stop" /> is called
/// </summary>
public void Start()
{
_start = Stopwatch.GetTimestamp();
_open = true;
}
/// <summary>
/// Stops measuring time and stores the time passed in the <see cref="Measurements" /> list
/// </summary>
/// <param name="correction">An optional correction in ticks to subtract from the measurement</param>
/// <returns>The time passed since the last <see cref="Start" /> call</returns>
public long Stop(long correction = 0)
{
if (!_open)
return 0;
long difference = Stopwatch.GetTimestamp() - _start - correction;
_open = false;
Measurements[_index] = difference;
_index++;
if (_index >= 1000)
{
Identifier = identifier;
_filledArray = true;
_index = 0;
}
/// <summary>
/// Gets the unique identifier of this measurement
/// </summary>
public string Identifier { get; }
_last = difference;
return difference;
}
/// <summary>
/// Gets the last 1000 measurements
/// </summary>
public long[] Measurements { get; } = new long[1000];
/// <summary>
/// Gets the last measured time
/// </summary>
public TimeSpan GetLast()
{
return new TimeSpan(_last);
}
/// <summary>
/// Starts measuring time until <see cref="Stop" /> is called
/// </summary>
public void Start()
{
_start = Stopwatch.GetTimestamp();
_open = true;
}
/// <summary>
/// Gets the average time of the last 1000 measurements
/// </summary>
public TimeSpan GetAverage()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
/// <summary>
/// Stops measuring time and stores the time passed in the <see cref="Measurements" /> list
/// </summary>
/// <param name="correction">An optional correction in ticks to subtract from the measurement</param>
/// <returns>The time passed since the last <see cref="Start" /> call</returns>
public long Stop(long correction = 0)
{
if (!_open)
return 0;
return _filledArray
? new TimeSpan((long) Measurements.Average(m => m))
: new TimeSpan((long) Measurements.Take(_index).Average(m => m));
}
long difference = Stopwatch.GetTimestamp() - _start - correction;
_open = false;
Measurements[_index] = difference;
/// <summary>
/// Gets the min time of the last 1000 measurements
/// </summary>
public TimeSpan GetMin()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
_index++;
if (_index >= 1000)
{
_filledArray = true;
_index = 0;
}
return _filledArray
? new TimeSpan(Measurements.Min())
: new TimeSpan(Measurements.Take(_index).Min());
}
_last = difference;
return difference;
}
/// <summary>
/// Gets the max time of the last 1000 measurements
/// </summary>
public TimeSpan GetMax()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
/// <summary>
/// Gets the last measured time
/// </summary>
public TimeSpan GetLast()
{
return new TimeSpan(_last);
}
return _filledArray
? new TimeSpan(Measurements.Max())
: new TimeSpan(Measurements.Take(_index).Max());
}
/// <summary>
/// Gets the average time of the last 1000 measurements
/// </summary>
public TimeSpan GetAverage()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
/// <summary>
/// Gets the nth percentile of the last 1000 measurements
/// </summary>
public TimeSpan GetPercentile(double percentile)
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
return _filledArray
? new TimeSpan((long) Measurements.Average(m => m))
: new TimeSpan((long) Measurements.Take(_index).Average(m => m));
}
long[] collection = _filledArray
? Measurements.OrderBy(l => l).ToArray()
: Measurements.Take(_index).OrderBy(l => l).ToArray();
/// <summary>
/// Gets the min time of the last 1000 measurements
/// </summary>
public TimeSpan GetMin()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
return new TimeSpan((long) Percentile(collection, percentile));
}
return _filledArray
? new TimeSpan(Measurements.Min())
: new TimeSpan(Measurements.Take(_index).Min());
}
/// <summary>
/// Gets the max time of the last 1000 measurements
/// </summary>
public TimeSpan GetMax()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
return _filledArray
? new TimeSpan(Measurements.Max())
: new TimeSpan(Measurements.Take(_index).Max());
}
/// <summary>
/// Gets the nth percentile of the last 1000 measurements
/// </summary>
public TimeSpan GetPercentile(double percentile)
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
long[] collection = _filledArray
? Measurements.OrderBy(l => l).ToArray()
: Measurements.Take(_index).OrderBy(l => l).ToArray();
return new TimeSpan((long) Percentile(collection, percentile));
}
private static double Percentile(long[] elements, double percentile)
{
Array.Sort(elements);
double realIndex = percentile * (elements.Length - 1);
int index = (int) realIndex;
double frac = realIndex - index;
if (index + 1 < elements.Length)
return elements[index] * (1 - frac) + elements[index + 1] * frac;
return elements[index];
}
private static double Percentile(long[] elements, double percentile)
{
Array.Sort(elements);
double realIndex = percentile * (elements.Length - 1);
int index = (int) realIndex;
double frac = realIndex - index;
if (index + 1 < elements.Length)
return elements[index] * (1 - frac) + elements[index + 1] * frac;
return elements[index];
}
}
@@ -1,24 +1,23 @@
namespace Artemis.Core.ScriptingProviders
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents a view model containing a script editor
/// </summary>
public interface IScriptEditorViewModel
{
/// <summary>
/// Represents a view model containing a script editor
/// Gets the script type this view model was created for
/// </summary>
public interface IScriptEditorViewModel
{
/// <summary>
/// Gets the script type this view model was created for
/// </summary>
ScriptType ScriptType { get; }
ScriptType ScriptType { get; }
/// <summary>
/// Gets the script this editor is editing
/// </summary>
Script? Script { get; }
/// <summary>
/// Gets the script this editor is editing
/// </summary>
Script? Script { get; }
/// <summary>
/// Called whenever the view model must display a different script
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
void ChangeScript(Script? script);
}
/// <summary>
/// Called whenever the view model must display a different script
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
void ChangeScript(Script? script);
}
@@ -1,168 +1,163 @@
using System;
using Artemis.Storage.Entities.General;
namespace Artemis.Core.ScriptingProviders
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents the configuration of a script
/// </summary>
public class ScriptConfiguration : CorePropertyChanged, IStorageModel
{
private bool _hasChanges;
private bool _isSuspended;
private string _name;
private string? _pendingScriptContent;
private string? _scriptContent;
private string _scriptingProviderId;
/// <summary>
/// Represents the configuration of a script
/// Creates a new instance of the <see cref="ScriptConfiguration" /> class
/// </summary>
public class ScriptConfiguration : CorePropertyChanged, IStorageModel
public ScriptConfiguration(ScriptingProvider provider, string name, ScriptType scriptType)
{
private bool _hasChanges;
private bool _isSuspended;
private string _name;
private string? _pendingScriptContent;
private string? _scriptContent;
private string _scriptingProviderId;
/// <summary>
/// Creates a new instance of the <see cref="ScriptConfiguration" /> class
/// </summary>
public ScriptConfiguration(ScriptingProvider provider, string name, ScriptType scriptType)
{
_scriptingProviderId = provider.Id;
_name = name;
Entity = new ScriptConfigurationEntity();
PendingScriptContent = provider.GetDefaultScriptContent(scriptType);
ScriptContent = PendingScriptContent;
}
internal ScriptConfiguration(ScriptConfigurationEntity entity)
{
_scriptingProviderId = null!;
_name = null!;
Entity = entity;
Load();
}
/// <summary>
/// Gets or sets the ID of the scripting provider
/// </summary>
public string ScriptingProviderId
{
get => _scriptingProviderId;
set => SetAndNotify(ref _scriptingProviderId, value);
}
/// <summary>
/// Gets or sets the name of the script
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets the script's content
/// </summary>
public string? ScriptContent
{
get => _scriptContent;
private set
{
if (!SetAndNotify(ref _scriptContent, value)) return;
OnScriptContentChanged();
}
}
/// <summary>
/// Gets or sets the pending changes to the script's content
/// </summary>
public string? PendingScriptContent
{
get => _pendingScriptContent;
set
{
if (string.IsNullOrWhiteSpace(value))
value = null;
if (!SetAndNotify(ref _pendingScriptContent, value)) return;
HasChanges = ScriptContent != PendingScriptContent;
}
}
// TODO: Implement suspension
/// <summary>
/// [NYI] Gets or sets a boolean indicating whether this configuration is suspended
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this configuration has pending changes to it's
/// <see cref="ScriptContent" />
/// </summary>
public bool HasChanges
{
get => _hasChanges;
set => SetAndNotify(ref _hasChanges, value);
}
/// <summary>
/// If active, gets the script
/// </summary>
public Script? Script { get; internal set; }
internal ScriptConfigurationEntity Entity { get; }
/// <summary>
/// Applies the <see cref="PendingScriptContent" /> to the <see cref="ScriptContent" />
/// </summary>
public void ApplyPendingChanges()
{
ScriptContent = PendingScriptContent;
HasChanges = false;
}
/// <summary>
/// Discards the <see cref="PendingScriptContent" />
/// </summary>
public void DiscardPendingChanges()
{
PendingScriptContent = ScriptContent;
HasChanges = false;
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
ScriptingProviderId = Entity.ScriptingProviderId;
ScriptContent = Entity.ScriptContent;
PendingScriptContent = Entity.ScriptContent;
Name = Entity.Name;
}
/// <inheritdoc />
public void Save()
{
Entity.ScriptingProviderId = ScriptingProviderId;
Entity.ScriptContent = ScriptContent;
Entity.Name = Name;
}
#endregion
#region Events
/// <summary>
/// Occurs whenever the contents of the script have changed
/// </summary>
public event EventHandler? ScriptContentChanged;
/// <summary>
/// Invokes the <see cref="ScriptContentChanged" /> event
/// </summary>
protected virtual void OnScriptContentChanged()
{
ScriptContentChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
_scriptingProviderId = provider.Id;
_name = name;
Entity = new ScriptConfigurationEntity();
PendingScriptContent = provider.GetDefaultScriptContent(scriptType);
ScriptContent = PendingScriptContent;
}
internal ScriptConfiguration(ScriptConfigurationEntity entity)
{
_scriptingProviderId = null!;
_name = null!;
Entity = entity;
Load();
}
/// <summary>
/// Gets or sets the ID of the scripting provider
/// </summary>
public string ScriptingProviderId
{
get => _scriptingProviderId;
set => SetAndNotify(ref _scriptingProviderId, value);
}
/// <summary>
/// Gets or sets the name of the script
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets the script's content
/// </summary>
public string? ScriptContent
{
get => _scriptContent;
private set
{
if (!SetAndNotify(ref _scriptContent, value)) return;
OnScriptContentChanged();
}
}
/// <summary>
/// Gets or sets the pending changes to the script's content
/// </summary>
public string? PendingScriptContent
{
get => _pendingScriptContent;
set
{
if (string.IsNullOrWhiteSpace(value))
value = null;
if (!SetAndNotify(ref _pendingScriptContent, value)) return;
HasChanges = ScriptContent != PendingScriptContent;
}
}
// TODO: Implement suspension
/// <summary>
/// [NYI] Gets or sets a boolean indicating whether this configuration is suspended
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this configuration has pending changes to it's
/// <see cref="ScriptContent" />
/// </summary>
public bool HasChanges
{
get => _hasChanges;
set => SetAndNotify(ref _hasChanges, value);
}
/// <summary>
/// If active, gets the script
/// </summary>
public Script? Script { get; internal set; }
internal ScriptConfigurationEntity Entity { get; }
/// <summary>
/// Applies the <see cref="PendingScriptContent" /> to the <see cref="ScriptContent" />
/// </summary>
public void ApplyPendingChanges()
{
ScriptContent = PendingScriptContent;
HasChanges = false;
}
/// <summary>
/// Discards the <see cref="PendingScriptContent" />
/// </summary>
public void DiscardPendingChanges()
{
PendingScriptContent = ScriptContent;
HasChanges = false;
}
/// <summary>
/// Occurs whenever the contents of the script have changed
/// </summary>
public event EventHandler? ScriptContentChanged;
/// <summary>
/// Invokes the <see cref="ScriptContentChanged" /> event
/// </summary>
protected virtual void OnScriptContentChanged()
{
ScriptContentChanged?.Invoke(this, EventArgs.Empty);
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
ScriptingProviderId = Entity.ScriptingProviderId;
ScriptContent = Entity.ScriptContent;
PendingScriptContent = Entity.ScriptContent;
Name = Entity.Name;
}
/// <inheritdoc />
public void Save()
{
Entity.ScriptingProviderId = ScriptingProviderId;
Entity.ScriptContent = ScriptContent;
Entity.Name = Name;
}
#endregion
}
@@ -2,80 +2,79 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Artemis.Core.ScriptingProviders
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Allows you to implement and register your own scripting provider.
/// </summary>
public abstract class ScriptingProvider<TGlobalScript, TProfileScript> : ScriptingProvider
where TGlobalScript : GlobalScript
where TProfileScript : ProfileScript
{
#region Overrides of PluginFeature
/// <inheritdoc />
internal override void InternalDisable()
{
base.InternalDisable();
while (Scripts.Count > 0)
Scripts[0].Dispose();
}
#endregion
#region Overrides of ScriptingProvider
/// <inheritdoc />
internal override Type ProfileScriptType => typeof(TProfileScript);
/// <inheritdoc />
internal override Type GlobalScriptType => typeof(TGlobalScript);
#endregion
}
/// <summary>
/// Allows you to implement and register your own scripting provider.
/// <para>
/// Note: You can't implement this, implement
/// <see cref="ScriptingProvider{TGlobalScript, TProfileScript}" /> instead.
/// </para>
/// </summary>
public abstract class ScriptingProvider : PluginFeature
{
/// <summary>
/// Allows you to implement and register your own scripting provider.
/// The base constructor of the <see cref="ScriptingProvider" /> class
/// </summary>
public abstract class ScriptingProvider<TGlobalScript, TProfileScript> : ScriptingProvider
where TGlobalScript : GlobalScript
where TProfileScript : ProfileScript
protected ScriptingProvider()
{
#region Overrides of PluginFeature
/// <inheritdoc />
internal override void InternalDisable()
{
base.InternalDisable();
while (Scripts.Count > 0)
Scripts[0].Dispose();
}
#endregion
#region Overrides of ScriptingProvider
/// <inheritdoc />
internal override Type ProfileScriptType => typeof(TProfileScript);
/// <inheritdoc />
internal override Type GlobalScriptType => typeof(TGlobalScript);
#endregion
Scripts = new ReadOnlyCollection<Script>(InternalScripts);
}
/// <summary>
/// Allows you to implement and register your own scripting provider.
/// <para>
/// Note: You can't implement this, implement
/// <see cref="ScriptingProvider{TGlobalScript, TProfileScript}" /> instead.
/// </para>
/// Gets the name of the scripting language this provider provides
/// </summary>
public abstract class ScriptingProvider : PluginFeature
{
/// <summary>
/// The base constructor of the <see cref="ScriptingProvider" /> class
/// </summary>
protected ScriptingProvider()
{
Scripts = new ReadOnlyCollection<Script>(InternalScripts);
}
public abstract string LanguageName { get; }
/// <summary>
/// Gets the name of the scripting language this provider provides
/// </summary>
public abstract string LanguageName { get; }
/// <summary>
/// Gets a list of all active scripts belonging to this scripting provider
/// </summary>
public ReadOnlyCollection<Script> Scripts { get; }
/// <summary>
/// Gets a list of all active scripts belonging to this scripting provider
/// </summary>
public ReadOnlyCollection<Script> Scripts { get; }
internal abstract Type GlobalScriptType { get; }
internal abstract Type ProfileScriptType { get; }
internal List<Script> InternalScripts { get; } = new();
internal abstract Type GlobalScriptType { get; }
internal abstract Type ProfileScriptType { get; }
internal List<Script> InternalScripts { get; } = new();
/// <summary>
/// Called when the UI needs a script editor for the specified <paramref name="scriptType" />
/// </summary>
/// <param name="scriptType">The type of script the editor will host</param>
public abstract IScriptEditorViewModel CreateScriptEditor(ScriptType scriptType);
/// <summary>
/// Called when the UI needs a script editor for the specified <paramref name="scriptType" />
/// </summary>
/// <param name="scriptType">The type of script the editor will host</param>
public abstract IScriptEditorViewModel CreateScriptEditor(ScriptType scriptType);
/// <summary>
/// Called when a script for a certain type needs default content.
/// </summary>
/// <param name="scriptType">The type of script the default content is for.</param>
public abstract string GetDefaultScriptContent(ScriptType scriptType);
}
/// <summary>
/// Called when a script for a certain type needs default content.
/// </summary>
/// <param name="scriptType">The type of script the default content is for.</param>
public abstract string GetDefaultScriptContent(ScriptType scriptType);
}
@@ -1,46 +1,45 @@
using Artemis.Core.Services;
namespace Artemis.Core.ScriptingProviders
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents a script running globally
/// </summary>
public abstract class GlobalScript : Script
{
/// <summary>
/// Represents a script running globally
/// </summary>
public abstract class GlobalScript : Script
/// <inheritdoc />
protected GlobalScript(ScriptConfiguration configuration) : base(configuration)
{
/// <inheritdoc />
protected GlobalScript(ScriptConfiguration configuration) : base(configuration)
{
}
internal ScriptingService? ScriptingService { get; set; }
/// <summary>
/// Called whenever the Artemis Core is about to update
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnCoreUpdating(double deltaTime)
{
}
/// <summary>
/// Called whenever the Artemis Core has been updated
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnCoreUpdated(double deltaTime)
{
}
#region Overrides of Script
/// <inheritdoc />
public override ScriptType ScriptType => ScriptType.Global;
/// <inheritdoc />
internal override void InternalCleanup()
{
ScriptingService?.RemoveScript(ScriptConfiguration);
}
#endregion
}
internal ScriptingService? ScriptingService { get; set; }
/// <summary>
/// Called whenever the Artemis Core is about to update
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnCoreUpdating(double deltaTime)
{
}
/// <summary>
/// Called whenever the Artemis Core has been updated
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnCoreUpdated(double deltaTime)
{
}
#region Overrides of Script
/// <inheritdoc />
public override ScriptType ScriptType => ScriptType.Global;
/// <inheritdoc />
internal override void InternalCleanup()
{
ScriptingService?.RemoveScript(ScriptConfiguration);
}
#endregion
}
@@ -1,68 +1,67 @@
using SkiaSharp;
namespace Artemis.Core.ScriptingProviders
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents a script bound to a specific <see cref="Profile" /> processed by a <see cref="ScriptingProvider" />.
/// </summary>
public abstract class ProfileScript : Script
{
/// <summary>
/// Represents a script bound to a specific <see cref="Profile" /> processed by a <see cref="ScriptingProvider" />.
/// </summary>
public abstract class ProfileScript : Script
/// <inheritdoc />
protected ProfileScript(Profile profile, ScriptConfiguration configuration) : base(configuration)
{
/// <inheritdoc />
protected ProfileScript(Profile profile, ScriptConfiguration configuration) : base(configuration)
{
Profile = profile;
}
/// <summary>
/// Gets the profile this script is bound to
/// </summary>
public Profile Profile { get; }
/// <summary>
/// Called whenever the profile is about to update
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnProfileUpdating(double deltaTime)
{
}
/// <summary>
/// Called whenever the profile has been updated
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnProfileUpdated(double deltaTime)
{
}
/// <summary>
/// Called whenever the profile is about to render
/// </summary>
/// <param name="canvas">The profile canvas</param>
/// <param name="bounds">The area to be filled, covers the entire canvas</param>
public virtual void OnProfileRendering(SKCanvas canvas, SKRect bounds)
{
}
/// <summary>
/// Called whenever the profile has been rendered
/// </summary>
/// <param name="canvas">The profile canvas</param>
/// <param name="bounds">The area to be filled, covers the entire canvas</param>
public virtual void OnProfileRendered(SKCanvas canvas, SKRect bounds)
{
}
#region Overrides of Script
/// <inheritdoc />
public override ScriptType ScriptType => ScriptType.Profile;
/// <inheritdoc />
internal override void InternalCleanup()
{
Profile.RemoveScript(this);
}
#endregion
Profile = profile;
}
/// <summary>
/// Gets the profile this script is bound to
/// </summary>
public Profile Profile { get; }
/// <summary>
/// Called whenever the profile is about to update
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnProfileUpdating(double deltaTime)
{
}
/// <summary>
/// Called whenever the profile has been updated
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnProfileUpdated(double deltaTime)
{
}
/// <summary>
/// Called whenever the profile is about to render
/// </summary>
/// <param name="canvas">The profile canvas</param>
/// <param name="bounds">The area to be filled, covers the entire canvas</param>
public virtual void OnProfileRendering(SKCanvas canvas, SKRect bounds)
{
}
/// <summary>
/// Called whenever the profile has been rendered
/// </summary>
/// <param name="canvas">The profile canvas</param>
/// <param name="bounds">The area to be filled, covers the entire canvas</param>
public virtual void OnProfileRendered(SKCanvas canvas, SKRect bounds)
{
}
#region Overrides of Script
/// <inheritdoc />
public override ScriptType ScriptType => ScriptType.Profile;
/// <inheritdoc />
internal override void InternalCleanup()
{
Profile.RemoveScript(this);
}
#endregion
}
@@ -1,127 +1,114 @@
using System;
using System.ComponentModel;
namespace Artemis.Core.ScriptingProviders
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents a script processed by a <see cref="ScriptingProviders.ScriptingProvider" />.
/// </summary>
public abstract class Script : CorePropertyChanged, IDisposable
{
private bool _disposed;
private ScriptingProvider _scriptingProvider = null!;
/// <summary>
/// The base constructor of any script
/// </summary>
/// <param name="configuration">The script configuration this script belongs to</param>
protected Script(ScriptConfiguration configuration)
{
if (configuration.Script != null)
throw new ArtemisCoreException("The provided script configuration already has an active script");
ScriptConfiguration = configuration;
ScriptConfiguration.PropertyChanged += ScriptConfigurationOnPropertyChanged;
}
/// <summary>
/// Gets the scripting provider this script belongs to
/// </summary>
public ScriptingProvider ScriptingProvider
{
get => _scriptingProvider;
internal set => SetAndNotify(ref _scriptingProvider, value);
}
/// <summary>
/// Gets the script configuration this script belongs to
/// </summary>
public ScriptConfiguration ScriptConfiguration { get; }
/// <summary>
/// Gets the script type of this script
/// </summary>
public abstract ScriptType ScriptType { get; }
/// <summary>
/// Occurs when the contents of the script have changed
/// </summary>
public event EventHandler? ScriptContentChanged;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
/// <summary>
/// Invokes the <see cref="ScriptContentChanged" /> event
/// </summary>
protected virtual void OnScriptContentChanged()
{
ScriptContentChanged?.Invoke(this, EventArgs.Empty);
}
internal abstract void InternalCleanup();
private void ScriptConfigurationOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ScriptConfiguration.ScriptContent))
OnScriptContentChanged();
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
ScriptConfiguration.PropertyChanged -= ScriptConfigurationOnPropertyChanged;
ScriptConfiguration.Script = null;
ScriptingProvider.InternalScripts.Remove(this);
// Can't trust those pesky plugin devs!
InternalCleanup();
Dispose(true);
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Represents a type of script
/// </summary>
public enum ScriptType
{
/// <summary>
/// Represents a script processed by a <see cref="ScriptingProviders.ScriptingProvider" />.
/// A global script that's always active
/// </summary>
public abstract class Script : CorePropertyChanged, IDisposable
{
private ScriptingProvider _scriptingProvider = null!;
private bool _disposed;
/// <summary>
/// The base constructor of any script
/// </summary>
/// <param name="configuration">The script configuration this script belongs to</param>
protected Script(ScriptConfiguration configuration)
{
if (configuration.Script != null)
throw new ArtemisCoreException("The provided script configuration already has an active script");
ScriptConfiguration = configuration;
ScriptConfiguration.PropertyChanged += ScriptConfigurationOnPropertyChanged;
}
/// <summary>
/// Gets the scripting provider this script belongs to
/// </summary>
public ScriptingProvider ScriptingProvider
{
get => _scriptingProvider;
internal set => SetAndNotify(ref _scriptingProvider, value);
}
/// <summary>
/// Gets the script configuration this script belongs to
/// </summary>
public ScriptConfiguration ScriptConfiguration { get; }
/// <summary>
/// Gets the script type of this script
/// </summary>
public abstract ScriptType ScriptType { get; }
#region Event handlers
private void ScriptConfigurationOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ScriptConfiguration.ScriptContent))
OnScriptContentChanged();
}
#endregion
#region IDisposable
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
ScriptConfiguration.PropertyChanged -= ScriptConfigurationOnPropertyChanged;
ScriptConfiguration.Script = null;
ScriptingProvider.InternalScripts.Remove(this);
// Can't trust those pesky plugin devs!
InternalCleanup();
Dispose(true);
GC.SuppressFinalize(this);
}
internal abstract void InternalCleanup();
#endregion
#region Events
/// <summary>
/// Occurs when the contents of the script have changed
/// </summary>
public event EventHandler? ScriptContentChanged;
/// <summary>
/// Invokes the <see cref="ScriptContentChanged" /> event
/// </summary>
protected virtual void OnScriptContentChanged()
{
ScriptContentChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
}
Global,
/// <summary>
/// Represents a type of script
/// A script tied to a <see cref="Profile" />
/// </summary>
public enum ScriptType
{
/// <summary>
/// A global script that's always active
/// </summary>
Global,
/// <summary>
/// A script tied to a <see cref="Profile" />
/// </summary>
Profile,
}
Profile
}
@@ -0,0 +1,45 @@
using System;
namespace Artemis.Core;
/// <summary>
/// Represents a setting tied to a plugin
/// </summary>
public interface IPluginSetting
{
/// <summary>
/// The name of the setting, unique to this plugin
/// </summary>
string Name { get; }
/// <summary>
/// Determines whether the setting has been changed
/// </summary>
bool HasChanged { get; }
/// <summary>
/// Gets or sets whether changes must automatically be saved
/// <para>Note: When set to <c>true</c> <see cref="HasChanged" /> is always <c>false</c></para>
/// </summary>
bool AutoSave { get; set; }
/// <summary>
/// Resets the setting to the last saved value
/// </summary>
void RejectChanges();
/// <summary>
/// Saves the setting
/// </summary>
void Save();
/// <summary>
/// Occurs when the value of the setting has been changed
/// </summary>
event EventHandler? SettingChanged;
/// <summary>
/// Occurs when the value of the setting has been saved
/// </summary>
event EventHandler? SettingSaved;
}
@@ -5,149 +5,106 @@ using Artemis.Storage.Entities.Plugins;
using Artemis.Storage.Repositories.Interfaces;
using Newtonsoft.Json;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a setting tied to a plugin of type <typeparamref name="T" />
/// </summary>
/// <typeparam name="T">The value type of the setting</typeparam>
public class PluginSetting<T> : CorePropertyChanged, IPluginSetting
{
/// <summary>
/// Represents a setting tied to a plugin of type <typeparamref name="T" />
/// </summary>
/// <typeparam name="T">The value type of the setting</typeparam>
public class PluginSetting<T> : CorePropertyChanged, IPluginSetting
private readonly IPluginRepository _pluginRepository;
private readonly PluginSettingEntity _pluginSettingEntity;
private T _value;
internal PluginSetting(IPluginRepository pluginRepository, PluginSettingEntity pluginSettingEntity)
{
private readonly IPluginRepository _pluginRepository;
private readonly PluginSettingEntity _pluginSettingEntity;
private T _value;
_pluginRepository = pluginRepository;
_pluginSettingEntity = pluginSettingEntity;
internal PluginSetting(IPluginRepository pluginRepository, PluginSettingEntity pluginSettingEntity)
Name = pluginSettingEntity.Name;
try
{
_pluginRepository = pluginRepository;
_pluginSettingEntity = pluginSettingEntity;
Name = pluginSettingEntity.Name;
try
{
_value = CoreJson.DeserializeObject<T>(pluginSettingEntity.Value)!;
}
catch (JsonReaderException)
{
_value = default!;
}
_value = CoreJson.DeserializeObject<T>(pluginSettingEntity.Value)!;
}
/// <inheritdoc />
public string Name { get; }
/// <summary>
/// The value of the setting
/// </summary>
[AllowNull]
[CanBeNull]
public T Value
catch (JsonReaderException)
{
get => _value;
set
{
if (Equals(_value, value)) return;
_value = value!;
OnSettingChanged();
OnPropertyChanged(nameof(Value));
if (AutoSave)
Save();
}
}
/// <inheritdoc />
public bool HasChanged => CoreJson.SerializeObject(Value) != _pluginSettingEntity.Value;
/// <inheritdoc />
public bool AutoSave { get; set; }
/// <inheritdoc />
public void RejectChanges()
{
Value = CoreJson.DeserializeObject<T>(_pluginSettingEntity.Value);
}
/// <inheritdoc />
public void Save()
{
if (!HasChanged)
return;
_pluginSettingEntity.Value = CoreJson.SerializeObject(Value);
_pluginRepository.SaveSetting(_pluginSettingEntity);
OnSettingSaved();
}
/// <inheritdoc />
public event EventHandler? SettingChanged;
/// <inheritdoc />
public event EventHandler? SettingSaved;
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Value)}: {Value}, {nameof(HasChanged)}: {HasChanged}";
}
/// <summary>
/// Invokes the <see cref="SettingChanged" /> event
/// </summary>
protected internal virtual void OnSettingChanged()
{
SettingChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="OnSettingSaved" /> event
/// </summary>
protected internal virtual void OnSettingSaved()
{
SettingSaved?.Invoke(this, EventArgs.Empty);
_value = default!;
}
}
/// <summary>
/// Represents a setting tied to a plugin
/// The value of the setting
/// </summary>
public interface IPluginSetting
[AllowNull]
[CanBeNull]
public T Value
{
/// <summary>
/// The name of the setting, unique to this plugin
/// </summary>
string Name { get; }
get => _value;
set
{
if (Equals(_value, value)) return;
/// <summary>
/// Determines whether the setting has been changed
/// </summary>
bool HasChanged { get; }
_value = value!;
OnSettingChanged();
OnPropertyChanged(nameof(Value));
/// <summary>
/// Gets or sets whether changes must automatically be saved
/// <para>Note: When set to <c>true</c> <see cref="HasChanged" /> is always <c>false</c></para>
/// </summary>
bool AutoSave { get; set; }
/// <summary>
/// Resets the setting to the last saved value
/// </summary>
void RejectChanges();
/// <summary>
/// Saves the setting
/// </summary>
void Save();
/// <summary>
/// Occurs when the value of the setting has been changed
/// </summary>
event EventHandler? SettingChanged;
/// <summary>
/// Occurs when the value of the setting has been saved
/// </summary>
event EventHandler? SettingSaved;
if (AutoSave)
Save();
}
}
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Value)}: {Value}, {nameof(HasChanged)}: {HasChanged}";
}
/// <summary>
/// Invokes the <see cref="SettingChanged" /> event
/// </summary>
protected internal virtual void OnSettingChanged()
{
SettingChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="OnSettingSaved" /> event
/// </summary>
protected internal virtual void OnSettingSaved()
{
SettingSaved?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public bool HasChanged => CoreJson.SerializeObject(Value) != _pluginSettingEntity.Value;
/// <inheritdoc />
public bool AutoSave { get; set; }
/// <inheritdoc />
public void RejectChanges()
{
Value = CoreJson.DeserializeObject<T>(_pluginSettingEntity.Value);
}
/// <inheritdoc />
public void Save()
{
if (!HasChanged)
return;
_pluginSettingEntity.Value = CoreJson.SerializeObject(Value);
_pluginRepository.SaveSetting(_pluginSettingEntity);
OnSettingSaved();
}
/// <inheritdoc />
public event EventHandler? SettingChanged;
/// <inheritdoc />
public event EventHandler? SettingSaved;
}
@@ -2,82 +2,81 @@
using Artemis.Storage.Entities.Plugins;
using Artemis.Storage.Repositories.Interfaces;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// <para>This contains all the settings for your plugin. To access a setting use <see cref="GetSetting{T}" />.</para>
/// <para>To use this class, inject it into the constructor of your plugin.</para>
/// </summary>
public class PluginSettings
{
/// <summary>
/// <para>This contains all the settings for your plugin. To access a setting use <see cref="GetSetting{T}" />.</para>
/// <para>To use this class, inject it into the constructor of your plugin.</para>
/// </summary>
public class PluginSettings
private readonly IPluginRepository _pluginRepository;
private readonly Dictionary<string, IPluginSetting> _settingEntities;
internal PluginSettings(Plugin plugin, IPluginRepository pluginRepository)
{
private readonly IPluginRepository _pluginRepository;
private readonly Dictionary<string, IPluginSetting> _settingEntities;
Plugin = plugin;
Plugin.Settings = this;
internal PluginSettings(Plugin plugin, IPluginRepository pluginRepository)
_pluginRepository = pluginRepository;
_settingEntities = new Dictionary<string, IPluginSetting>();
}
/// <summary>
/// Gets the plugin these settings belong to
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the setting with the provided name. If the setting does not exist yet, it is created.
/// </summary>
/// <typeparam name="T">The type of the setting, can be any serializable type</typeparam>
/// <param name="name">The name of the setting</param>
/// <param name="defaultValue">The default value to use if the setting does not exist yet</param>
public PluginSetting<T> GetSetting<T>(string name, T? defaultValue = default)
{
lock (_settingEntities)
{
Plugin = plugin;
Plugin.Settings = this;
_pluginRepository = pluginRepository;
_settingEntities = new Dictionary<string, IPluginSetting>();
}
/// <summary>
/// Gets the plugin these settings belong to
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the setting with the provided name. If the setting does not exist yet, it is created.
/// </summary>
/// <typeparam name="T">The type of the setting, can be any serializable type</typeparam>
/// <param name="name">The name of the setting</param>
/// <param name="defaultValue">The default value to use if the setting does not exist yet</param>
public PluginSetting<T> GetSetting<T>(string name, T? defaultValue = default)
{
lock (_settingEntities)
// Return cached value if available
if (_settingEntities.ContainsKey(name))
return (PluginSetting<T>) _settingEntities[name];
// Try to find in database
PluginSettingEntity settingEntity = _pluginRepository.GetSettingByNameAndGuid(name, Plugin.Guid);
// If not found, create a new one
if (settingEntity == null)
{
// Return cached value if available
if (_settingEntities.ContainsKey(name))
return (PluginSetting<T>) _settingEntities[name];
// Try to find in database
PluginSettingEntity settingEntity = _pluginRepository.GetSettingByNameAndGuid(name, Plugin.Guid);
// If not found, create a new one
if (settingEntity == null)
settingEntity = new PluginSettingEntity
{
settingEntity = new PluginSettingEntity
{
Name = name,
PluginGuid = Plugin.Guid,
Value = CoreJson.SerializeObject(defaultValue)
};
_pluginRepository.AddSetting(settingEntity);
}
PluginSetting<T> pluginSetting = new(_pluginRepository, settingEntity);
// This overrides null with the default value, I'm not sure if that's desirable because you
// might expect something to go null and you might not
// if (pluginSetting.Value == null && defaultValue != null)
// pluginSetting.Value = defaultValue;
_settingEntities.Add(name, pluginSetting);
return pluginSetting;
Name = name,
PluginGuid = Plugin.Guid,
Value = CoreJson.SerializeObject(defaultValue)
};
_pluginRepository.AddSetting(settingEntity);
}
}
/// <summary>
/// Saves all currently loaded settings
/// </summary>
public void SaveAllSettings()
{
foreach ((string _, IPluginSetting? pluginSetting) in _settingEntities)
pluginSetting.Save();
}
PluginSetting<T> pluginSetting = new(_pluginRepository, settingEntity);
internal void ClearSettings()
{
_settingEntities.Clear();
// This overrides null with the default value, I'm not sure if that's desirable because you
// might expect something to go null and you might not
// if (pluginSetting.Value == null && defaultValue != null)
// pluginSetting.Value = defaultValue;
_settingEntities.Add(name, pluginSetting);
return pluginSetting;
}
}
/// <summary>
/// Saves all currently loaded settings
/// </summary>
public void SaveAllSettings()
{
foreach ((string _, IPluginSetting? pluginSetting) in _settingEntities)
pluginSetting.Save();
}
internal void ClearSettings()
{
_settingEntities.Clear();
}
}
@@ -3,212 +3,210 @@ using System.Threading.Tasks;
using System.Timers;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Humanizer;
using Ninject;
using Serilog;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a registration for a timed plugin update
/// </summary>
public class TimedUpdateRegistration : IDisposable
{
/// <summary>
/// Represents a registration for a timed plugin update
/// </summary>
public class TimedUpdateRegistration : IDisposable
private readonly object _lock = new();
private readonly ILogger _logger;
private bool _disposed;
private DateTime _lastEvent;
private Timer? _timer;
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action<double> action, string? name)
{
private readonly object _lock = new();
private bool _disposed;
private DateTime _lastEvent;
private readonly ILogger _logger;
private Timer? _timer;
if (CoreService.Kernel == null)
throw new ArtemisCoreException("Cannot create a TimedUpdateRegistration before initializing the Core");
_logger = CoreService.Kernel.Get<ILogger>();
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action<double> action, string? name)
{
if (CoreService.Kernel == null)
throw new ArtemisCoreException("Cannot create a TimedUpdateRegistration before initializing the Core");
_logger = CoreService.Kernel.Get<ILogger>();
Feature = feature;
Interval = interval;
Action = action;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature = feature;
Interval = interval;
Action = action;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled;
if (Feature.IsEnabled)
Start();
}
Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled;
if (Feature.IsEnabled)
Start();
}
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func<double, Task> asyncAction, string? name)
{
if (CoreService.Kernel == null)
throw new ArtemisCoreException("Cannot create a TimedUpdateRegistration before initializing the Core");
_logger = CoreService.Kernel.Get<ILogger>();
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func<double, Task> asyncAction, string? name)
{
if (CoreService.Kernel == null)
throw new ArtemisCoreException("Cannot create a TimedUpdateRegistration before initializing the Core");
_logger = CoreService.Kernel.Get<ILogger>();
Feature = feature;
Interval = interval;
AsyncAction = asyncAction;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature = feature;
Interval = interval;
AsyncAction = asyncAction;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled;
if (Feature.IsEnabled)
Start();
}
Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled;
if (Feature.IsEnabled)
Start();
}
/// <summary>
/// Gets the plugin feature this registration is associated with
/// </summary>
public PluginFeature Feature { get; }
/// <summary>
/// Gets the plugin feature this registration is associated with
/// </summary>
public PluginFeature Feature { get; }
/// <summary>
/// Gets the interval at which the update should occur
/// </summary>
public TimeSpan Interval { get; }
/// <summary>
/// Gets the interval at which the update should occur
/// </summary>
public TimeSpan Interval { get; }
/// <summary>
/// Gets the action that gets called each time the update event fires
/// </summary>
public Action<double>? Action { get; }
/// <summary>
/// Gets the action that gets called each time the update event fires
/// </summary>
public Action<double>? Action { get; }
/// <summary>
/// Gets the task that gets called each time the update event fires
/// </summary>
public Func<double, Task>? AsyncAction { get; }
/// <summary>
/// Gets the task that gets called each time the update event fires
/// </summary>
public Func<double, Task>? AsyncAction { get; }
/// <summary>
/// Gets the name of this timed update
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the name of this timed update
/// </summary>
public string Name { get; }
/// <summary>
/// Starts calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin enables</para>
/// </summary>
public void Start()
{
if (_disposed)
throw new ObjectDisposedException("TimedUpdateRegistration");
/// <summary>
/// Starts calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin enables</para>
/// </summary>
public void Start()
{
if (_disposed)
throw new ObjectDisposedException("TimedUpdateRegistration");
lock (_lock)
{
if (!Feature.IsEnabled)
throw new ArtemisPluginException("Cannot start a timed update for a disabled plugin feature");
if (_timer != null)
return;
_lastEvent = DateTime.Now;
_timer = new Timer(Interval.TotalMilliseconds);
_timer.Elapsed += TimerOnElapsed;
_timer.Start();
}
}
/// <summary>
/// Stops calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin disables</para>
/// </summary>
public void Stop()
{
if (_disposed)
throw new ObjectDisposedException("TimedUpdateRegistration");
lock (_lock)
{
if (_timer == null)
return;
_timer.Elapsed -= TimerOnElapsed;
_timer.Stop();
_timer.Dispose();
_timer = null;
}
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Stop();
Feature.Enabled -= FeatureOnEnabled;
Feature.Disabled -= FeatureOnDisabled;
_disposed = true;
}
}
private void TimerOnElapsed(object? sender, ElapsedEventArgs e)
lock (_lock)
{
if (!Feature.IsEnabled)
throw new ArtemisPluginException("Cannot start a timed update for a disabled plugin feature");
if (_timer != null)
return;
lock (_lock)
{
Feature.Profiler.StartMeasurement(ToString());
TimeSpan interval = DateTime.Now - _lastEvent;
_lastEvent = DateTime.Now;
// Modules don't always want to update, honor that
if (Feature is Module module && !module.IsUpdateAllowed)
return;
try
{
if (Action != null)
{
Action(interval.TotalSeconds);
}
else if (AsyncAction != null)
{
Task task = AsyncAction(interval.TotalSeconds);
task.Wait();
}
}
catch (Exception exception)
{
_logger.Error(exception, "{timedUpdate} uncaught exception in plugin {plugin}", this, Feature.Plugin);
}
finally
{
Feature.Profiler.StopMeasurement(ToString());
}
}
}
private void FeatureOnEnabled(object? sender, EventArgs e)
{
Start();
}
private void FeatureOnDisabled(object? sender, EventArgs e)
{
Stop();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
Feature.Profiler.ClearMeasurements(ToString());
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public sealed override string ToString()
{
if (Interval.TotalSeconds >= 1)
return $"{Name} ({Interval.TotalSeconds} sec)";
return $"{Name} ({Interval.TotalMilliseconds} ms)";
_lastEvent = DateTime.Now;
_timer = new Timer(Interval.TotalMilliseconds);
_timer.Elapsed += TimerOnElapsed;
_timer.Start();
}
}
/// <summary>
/// Stops calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin disables</para>
/// </summary>
public void Stop()
{
if (_disposed)
throw new ObjectDisposedException("TimedUpdateRegistration");
lock (_lock)
{
if (_timer == null)
return;
_timer.Elapsed -= TimerOnElapsed;
_timer.Stop();
_timer.Dispose();
_timer = null;
}
}
/// <inheritdoc />
public sealed override string ToString()
{
if (Interval.TotalSeconds >= 1)
return $"{Name} ({Interval.TotalSeconds} sec)";
return $"{Name} ({Interval.TotalMilliseconds} ms)";
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Stop();
Feature.Enabled -= FeatureOnEnabled;
Feature.Disabled -= FeatureOnDisabled;
_disposed = true;
}
}
private void TimerOnElapsed(object? sender, ElapsedEventArgs e)
{
if (!Feature.IsEnabled)
return;
lock (_lock)
{
Feature.Profiler.StartMeasurement(ToString());
TimeSpan interval = DateTime.Now - _lastEvent;
_lastEvent = DateTime.Now;
// Modules don't always want to update, honor that
if (Feature is Module module && !module.IsUpdateAllowed)
return;
try
{
if (Action != null)
{
Action(interval.TotalSeconds);
}
else if (AsyncAction != null)
{
Task task = AsyncAction(interval.TotalSeconds);
task.Wait();
}
}
catch (Exception exception)
{
_logger.Error(exception, "{timedUpdate} uncaught exception in plugin {plugin}", this, Feature.Plugin);
}
finally
{
Feature.Profiler.StopMeasurement(ToString());
}
}
}
private void FeatureOnEnabled(object? sender, EventArgs e)
{
Start();
}
private void FeatureOnDisabled(object? sender, EventArgs e)
{
Stop();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
Feature.Profiler.ClearMeasurements(ToString());
GC.SuppressFinalize(this);
}
}