diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
index 261d8170b..31cbebcb3 100644
--- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings
+++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
@@ -61,6 +61,7 @@
True
True
True
+ True
True
True
True
diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumContainsConditionOperator.cs
new file mode 100644
index 000000000..dd79f14cb
--- /dev/null
+++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumContainsConditionOperator.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Artemis.Core
+{
+ internal class EnumContainsConditionOperator : ConditionOperator
+ {
+ public override string Description => "Contains";
+ public override string Icon => "Contain";
+
+ public override bool Evaluate(Enum a, Enum b)
+ {
+ return a != null && b != null && a.HasFlag(b);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumNotContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumNotContainsConditionOperator.cs
new file mode 100644
index 000000000..ef78b8470
--- /dev/null
+++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumNotContainsConditionOperator.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Artemis.Core
+{
+ internal class EnumNotContainsConditionOperator : ConditionOperator
+ {
+ public override string Description => "Does not contain";
+ public override string Icon => "FormatStrikethrough";
+
+ public override bool Evaluate(Enum a, Enum b)
+ {
+ return a != null && (b == null || !a.HasFlag(b));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs
index f5b32c35c..1b683710b 100644
--- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs
+++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs
@@ -9,7 +9,7 @@ namespace Artemis.Core
public override bool Evaluate(string a, string b)
{
- return a != null && b != null && !a.Contains(b, StringComparison.InvariantCultureIgnoreCase);
+ return a != null && (b == null || !a.Contains(b, StringComparison.InvariantCultureIgnoreCase));
}
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs
index 9199aa021..6eea123b0 100644
--- a/src/Artemis.Core/Models/Profile/Layer.cs
+++ b/src/Artemis.Core/Models/Profile/Layer.cs
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
+using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Abstract;
using RGB.NET.Core;
@@ -37,6 +38,9 @@ namespace Artemis.Core
Profile = Parent.Profile;
Name = name;
Suspended = false;
+ Scripts = new List();
+ ScriptConfigurations = new List();
+
_general = new LayerGeneralProperties();
_transform = new LayerTransformProperties();
@@ -60,6 +64,9 @@ namespace Artemis.Core
Profile = profile;
Parent = parent;
+ Scripts = new List();
+ ScriptConfigurations = new List();
+
_general = new LayerGeneralProperties();
_transform = new LayerTransformProperties();
@@ -75,6 +82,16 @@ namespace Artemis.Core
///
public ReadOnlyCollection Leds => _leds.AsReadOnly();
+ ///
+ /// Gets a collection of all active scripts assigned to this layer
+ ///
+ public List Scripts { get; }
+
+ ///
+ /// Gets a collection of all script configurations assigned to this layer
+ ///
+ public List ScriptConfigurations { get; }
+
///
/// Defines the shape that is rendered by the .
///
@@ -142,8 +159,10 @@ namespace Artemis.Core
if (LayerBrush?.BaseProperties != null)
result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties());
foreach (BaseLayerEffect layerEffect in LayerEffects)
+ {
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
+ }
return result;
}
@@ -171,6 +190,9 @@ namespace Artemis.Core
Disposed = true;
+ while (Scripts.Count > 1)
+ Scripts[0].Dispose();
+
LayerBrushStore.LayerBrushAdded -= LayerBrushStoreOnLayerBrushAdded;
LayerBrushStore.LayerBrushRemoved -= LayerBrushStoreOnLayerBrushRemoved;
@@ -247,6 +269,11 @@ namespace Artemis.Core
ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups);
LoadRenderElement();
Adapter.Load();
+
+ foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
+ scriptConfiguration.Script?.Dispose();
+ ScriptConfigurations.Clear();
+ ScriptConfigurations.AddRange(LayerEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)));
}
internal override void Save()
@@ -284,6 +311,13 @@ namespace Artemis.Core
// Adaption hints
Adapter.Save();
+ LayerEntity.ScriptConfigurations.Clear();
+ foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
+ {
+ scriptConfiguration.Save();
+ LayerEntity.ScriptConfigurations.Add(scriptConfiguration.Entity);
+ }
+
SaveRenderElement();
}
@@ -316,6 +350,9 @@ namespace Artemis.Core
if (Disposed)
throw new ObjectDisposedException("Layer");
+ foreach (LayerScript layerScript in Scripts)
+ layerScript.OnLayerUpdating(deltaTime);
+
UpdateDisplayCondition();
UpdateTimeline(deltaTime);
@@ -323,6 +360,9 @@ namespace Artemis.Core
Enable();
else if (Timeline.IsFinished)
Disable();
+
+ foreach (LayerScript layerScript in Scripts)
+ layerScript.OnLayerUpdated(deltaTime);
}
///
@@ -473,19 +513,27 @@ namespace Artemis.Core
if (LayerBrush == null)
throw new ArtemisCoreException("The layer is not yet ready for rendering");
+ foreach (LayerScript layerScript in Scripts)
+ layerScript.OnLayerRendering(canvas, bounds, layerPaint);
+
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.PreProcess(canvas, bounds, layerPaint);
try
{
canvas.SaveLayer(layerPaint);
-
- // If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off
canvas.ClipPath(renderPath);
+
+ // Restore the blend mode before doing the actual render
+ layerPaint.BlendMode = SKBlendMode.SrcOver;
+
LayerBrush.InternalRender(canvas, bounds, layerPaint);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.PostProcess(canvas, bounds, layerPaint);
+
+ foreach (LayerScript layerScript in Scripts)
+ layerScript.OnLayerRendered(canvas, bounds, layerPaint);
}
finally
@@ -500,9 +548,7 @@ namespace Artemis.Core
throw new ObjectDisposedException("Layer");
if (!Leds.Any())
- {
Path = new SKPath();
- }
else
{
SKPath path = new() {FillType = SKPathFillType.Winding};
diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs
index c9bdf5872..76b787f1d 100644
--- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs
+++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
@@ -23,6 +24,16 @@ namespace Artemis.Core
///
LayerPropertyGroup LayerPropertyGroup { get; }
+ ///
+ /// Gets a collection of all active scripts assigned to this layer property
+ ///
+ List Scripts { get; }
+
+ ///
+ /// Gets a collection of all script configurations assigned to this layer property
+ ///
+ public List ScriptConfigurations { get; }
+
///
/// Gets the unique path of the property on the layer
///
diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs
index d9ad40690..3bbb7f557 100644
--- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs
+++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
@@ -30,6 +31,8 @@ namespace Artemis.Core
Path = null!;
Entity = null!;
PropertyDescription = null!;
+ Scripts = new List();
+ ScriptConfigurations = new List();
CurrentValue = default!;
DefaultValue = default!;
@@ -55,15 +58,15 @@ namespace Artemis.Core
///
protected virtual void Dispose(bool disposing)
{
- if (disposing)
- {
- _disposed = true;
+ _disposed = true;
- foreach (IDataBinding dataBinding in _dataBindings)
- dataBinding.Dispose();
+ while (Scripts.Count > 1)
+ Scripts[0].Dispose();
- Disposed?.Invoke(this, EventArgs.Empty);
- }
+ foreach (IDataBinding dataBinding in _dataBindings)
+ dataBinding.Dispose();
+
+ Disposed?.Invoke(this, EventArgs.Empty);
}
///
@@ -150,6 +153,12 @@ namespace Artemis.Core
///
public PropertyDescriptionAttribute PropertyDescription { get; internal set; }
+ ///
+ public List Scripts { get; }
+
+ ///
+ public List ScriptConfigurations { get; }
+
///
public string Path { get; private set; }
@@ -162,12 +171,18 @@ namespace Artemis.Core
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
+ foreach (PropertyScript propertyScript in Scripts)
+ propertyScript.OnPropertyUpdating(timeline.Delta.TotalSeconds);
+
CurrentValue = BaseValue;
UpdateKeyframes(timeline);
UpdateDataBindings(timeline);
OnUpdated();
+
+ foreach (PropertyScript propertyScript in Scripts)
+ propertyScript.OnPropertyUpdated(timeline.Delta.TotalSeconds);
}
///
@@ -747,6 +762,11 @@ namespace Artemis.Core
if (dataBinding != null)
_dataBindings.Add(dataBinding);
}
+
+ foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
+ scriptConfiguration.Script?.Dispose();
+ ScriptConfigurations.Clear();
+ ScriptConfigurations.AddRange(Entity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)));
}
///
@@ -768,6 +788,13 @@ namespace Artemis.Core
Entity.DataBindingEntities.Clear();
foreach (IDataBinding dataBinding in _dataBindings)
dataBinding.Save();
+
+ Entity.ScriptConfigurations.Clear();
+ foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
+ {
+ scriptConfiguration.Save();
+ Entity.ScriptConfigurations.Add(scriptConfiguration.Entity);
+ }
}
///
diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs
index 3107d0805..75ec5d66b 100644
--- a/src/Artemis.Core/Models/Profile/Profile.cs
+++ b/src/Artemis.Core/Models/Profile/Profile.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
@@ -13,13 +14,15 @@ namespace Artemis.Core
{
private readonly object _lock = new();
private bool _isFreshImport;
-
+
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
Configuration = configuration;
Profile = this;
ProfileEntity = profileEntity;
EntityId = profileEntity.Id;
+ Scripts = new List();
+ ScriptConfigurations = new List();
UndoStack = new Stack();
RedoStack = new Stack();
@@ -32,6 +35,17 @@ namespace Artemis.Core
///
public ProfileConfiguration Configuration { get; }
+ ///
+ /// Gets a collection of all active scripts assigned to this profile
+ ///
+ public List Scripts { get; }
+
+ ///
+ /// Gets a collection of all script configurations assigned to this profile
+ ///
+ public List ScriptConfigurations { get; }
+
+
///
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
/// since import
@@ -62,8 +76,14 @@ namespace Artemis.Core
if (Disposed)
throw new ObjectDisposedException("Profile");
+ foreach (ProfileScript profileScript in Scripts)
+ profileScript.OnProfileUpdating(deltaTime);
+
foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime);
+
+ foreach (ProfileScript profileScript in Scripts)
+ profileScript.OnProfileUpdated(deltaTime);
}
}
@@ -75,8 +95,14 @@ namespace Artemis.Core
if (Disposed)
throw new ObjectDisposedException("Profile");
+ foreach (ProfileScript profileScript in Scripts)
+ profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
+
foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition);
+
+ foreach (ProfileScript profileScript in Scripts)
+ profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
}
}
@@ -125,6 +151,9 @@ namespace Artemis.Core
if (!disposing)
return;
+ while (Scripts.Count > 1)
+ Scripts[0].Dispose();
+
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
@@ -157,6 +186,11 @@ namespace Artemis.Core
AddChild(new Folder(this, this, rootFolder));
}
}
+
+ foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
+ scriptConfiguration.Script?.Dispose();
+ ScriptConfigurations.Clear();
+ ScriptConfigurations.AddRange(ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)));
}
internal override void Save()
@@ -176,6 +210,13 @@ namespace Artemis.Core
ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
+
+ ProfileEntity.ScriptConfigurations.Clear();
+ foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
+ {
+ scriptConfiguration.Save();
+ ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity);
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs
index c63bbedef..f8d8a6d47 100644
--- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs
+++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs
@@ -124,8 +124,8 @@ namespace Artemis.Core
public void UpdateTimeline(double deltaTime)
{
// The play mode dictates whether to stick to the main segment unless the display conditions contains events
- bool stickToMainSegment = Timeline.PlayMode == TimelinePlayMode.Repeat && DisplayConditionMet;
- if (DisplayCondition != null && DisplayCondition.ContainsEvents)
+ bool stickToMainSegment = (Timeline.PlayMode == TimelinePlayMode.Repeat || Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle) && DisplayConditionMet;
+ if (DisplayCondition != null && DisplayCondition.ContainsEvents && Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle)
stickToMainSegment = false;
Timeline.Update(TimeSpan.FromSeconds(deltaTime), stickToMainSegment);
@@ -356,6 +356,7 @@ namespace Artemis.Core
private DataModelConditionGroup? _displayCondition;
private bool _displayConditionMet;
+ private bool _toggledOnByEvent = false;
///
/// Gets or sets the root display condition group
@@ -383,6 +384,9 @@ namespace Artemis.Core
return;
}
+ if (Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle)
+ _toggledOnByEvent = false;
+
bool conditionMet = DisplayCondition.Evaluate();
if (Parent is RenderProfileElement parent && !parent.DisplayConditionMet)
conditionMet = false;
@@ -398,25 +402,36 @@ namespace Artemis.Core
}
else if (conditionMet)
{
- // Event conditions reset if the timeline finished
- if (Timeline.IsFinished)
+ if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle)
{
- Timeline.JumpToStart();
+ _toggledOnByEvent = !_toggledOnByEvent;
+ if (_toggledOnByEvent)
+ Timeline.JumpToStart();
}
- // and otherwise apply their overlap mode
else
{
- if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Restart)
+ // Event conditions reset if the timeline finished
+ if (Timeline.IsFinished)
+ {
Timeline.JumpToStart();
- else if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Copy)
- Timeline.AddExtraTimeline();
- // The third option is ignore which is handled below:
+ }
+ // and otherwise apply their overlap mode
+ else
+ {
+ if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Restart)
+ Timeline.JumpToStart();
+ else if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Copy)
+ Timeline.AddExtraTimeline();
+ // The third option is ignore which is handled below:
- // done
+ // done
+ }
}
}
- DisplayConditionMet = conditionMet;
+ DisplayConditionMet = Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle
+ ? _toggledOnByEvent
+ : conditionMet;
}
#endregion
diff --git a/src/Artemis.Core/Models/Profile/Timeline.cs b/src/Artemis.Core/Models/Profile/Timeline.cs
index 9fb9a201f..a01b9d232 100644
--- a/src/Artemis.Core/Models/Profile/Timeline.cs
+++ b/src/Artemis.Core/Models/Profile/Timeline.cs
@@ -155,7 +155,7 @@ namespace Artemis.Core
///
/// Gets a boolean indicating whether the timeline has finished its run
///
- public bool IsFinished => (Position > Length || Length == TimeSpan.Zero) && !ExtraTimelines.Any();
+ public bool IsFinished => Position > Length && !ExtraTimelines.Any();
///
/// Gets a boolean indicating whether the timeline progress has been overridden
@@ -516,6 +516,11 @@ namespace Artemis.Core
///
/// Play another copy of the timeline on top of the current run
///
- Copy
+ Copy,
+
+ ///
+ /// Repeat the timeline until the event fires again
+ ///
+ Toggle
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs
index aa9a5c3cd..87ee34783 100644
--- a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs
+++ b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs
@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using Artemis.Core.Services;
-using Ninject;
namespace Artemis.Core.Modules
{
@@ -13,8 +12,6 @@ namespace Artemis.Core.Modules
///
public class ProcessActivationRequirement : IModuleActivationRequirement
{
- private readonly IProcessMonitorService _processMonitorService;
-
///
/// Creates a new instance of the class
///
@@ -25,11 +22,6 @@ namespace Artemis.Core.Modules
if (string.IsNullOrWhiteSpace(processName) && string.IsNullOrWhiteSpace(location))
throw new ArgumentNullException($"Atleast one {nameof(processName)} and {nameof(location)} must not be null");
- // Let's not make a habit out of this :P
- if (CoreService.Kernel == null)
- throw new ArtemisCoreException("Cannot create a ProcessActivationRequirement before initializing the Core");
- _processMonitorService = CoreService.Kernel.Get();
-
ProcessName = processName;
Location = location;
}
@@ -44,13 +36,15 @@ namespace Artemis.Core.Modules
///
public string? Location { get; set; }
+ internal static IProcessMonitorService? ProcessMonitorService { get; set; }
+
///
public bool Evaluate()
{
- if (ProcessName == null && Location == null)
+ if (ProcessMonitorService == null || ProcessName == null && Location == null)
return false;
- IEnumerable processes = _processMonitorService.GetRunningProcesses();
+ IEnumerable processes = ProcessMonitorService.GetRunningProcesses();
if (ProcessName != null)
processes = processes.Where(p => string.Equals(p.ProcessName, ProcessName, StringComparison.InvariantCultureIgnoreCase));
if (Location != null)
diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs
new file mode 100644
index 000000000..10095bf4f
--- /dev/null
+++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs
@@ -0,0 +1,86 @@
+using Artemis.Storage.Entities.General;
+
+namespace Artemis.Core.ScriptingProviders
+{
+ ///
+ /// Represents the configuration of a script
+ ///
+ public class ScriptConfiguration : CorePropertyChanged, IStorageModel
+ {
+ private string _scriptingProviderId;
+ private string _name;
+ private string? _scriptContent;
+
+ ///
+ /// Creates a new instance of the class
+ ///
+ public ScriptConfiguration(ScriptingProvider provider, string name)
+ {
+ ScriptingProviderId = provider.Id;
+ Name = name;
+ Entity = new ScriptConfigurationEntity();
+ }
+
+ internal ScriptConfiguration(ScriptConfigurationEntity entity)
+ {
+ ScriptingProviderId = null!;
+ Name = null!;
+ Entity = entity;
+
+ Load();
+ }
+
+ ///
+ /// Gets or sets the ID of the scripting provider
+ ///
+ public string ScriptingProviderId
+ {
+ get => _scriptingProviderId;
+ set => SetAndNotify(ref _scriptingProviderId, value);
+ }
+
+ ///
+ /// Gets or sets the name of the script
+ ///
+ public string Name
+ {
+ get => _name;
+ set => SetAndNotify(ref _name, value);
+ }
+
+ ///
+ /// Gets or sets the script's content
+ ///
+ public string? ScriptContent
+ {
+ get => _scriptContent;
+ set => SetAndNotify(ref _scriptContent, value);
+ }
+
+ ///
+ /// If active, gets the script
+ ///
+ public Script? Script { get; internal set; }
+
+ internal ScriptConfigurationEntity Entity { get; }
+
+ #region Implementation of IStorageModel
+
+ ///
+ public void Load()
+ {
+ ScriptingProviderId = Entity.ScriptingProviderId;
+ ScriptContent = Entity.ScriptContent;
+ Name = Entity.Name;
+ }
+
+ ///
+ public void Save()
+ {
+ Entity.ScriptingProviderId = ScriptingProviderId;
+ Entity.ScriptContent = ScriptContent;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs
new file mode 100644
index 000000000..26baa7362
--- /dev/null
+++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Artemis.Core.ScriptingProviders
+{
+ ///
+ /// Allows you to implement and register your own scripting provider.
+ ///
+ public abstract class ScriptingProvider : ScriptingProvider
+ where TGlobalScript : GlobalScript
+ where TProfileScript : ProfileScript
+ where TLayerScript : LayerScript
+ where TPropertyScript : PropertyScript
+ {
+ ///
+ /// Called when the UI needs a script editor for a
+ ///
+ /// The script the editor must edit
+ public abstract IScriptEditorViewModel CreateGlobalScriptEditor(TGlobalScript script);
+
+ ///
+ /// Called when the UI needs a script editor for a
+ ///
+ /// The script the editor must edit
+ public abstract IScriptEditorViewModel CreateProfileScriptEditor(TProfileScript script);
+
+ ///
+ /// Called when the UI needs a script editor for a
+ ///
+ /// The script the editor must edit
+ public abstract IScriptEditorViewModel CreateLayerScriptScriptEditor(TLayerScript script);
+
+ ///
+ /// Called when the UI needs a script editor for a
+ ///
+ /// The script the editor must edit
+ public abstract IScriptEditorViewModel CreatePropertyScriptEditor(TPropertyScript script);
+
+ #region Overrides of ScriptingProvider
+
+ ///
+ internal override Type GlobalScriptType => typeof(TGlobalScript);
+
+ ///
+ internal override Type ProfileScriptType => typeof(TProfileScript);
+
+ ///
+ internal override Type LayerScriptType => typeof(TLayerScript);
+
+ ///
+ internal override Type PropertyScriptType => typeof(TPropertyScript);
+
+ ///
+ /// Called when the UI needs a script editor for a
+ ///
+ /// The script the editor must edit
+ public override IScriptEditorViewModel CreateGlobalScriptEditor(GlobalScript script)
+ {
+ if (script == null) throw new ArgumentNullException(nameof(script));
+ if (script.GetType() != GlobalScriptType)
+ throw new ArtemisCoreException($"This scripting provider only supports global scripts of type {GlobalScriptType.Name}");
+
+ return CreateGlobalScriptEditor((TGlobalScript) script);
+ }
+
+ ///
+ /// Called when the UI needs a script editor for a
+ ///
+ /// The script the editor must edit
+ public override IScriptEditorViewModel CreateProfileScriptEditor(ProfileScript script)
+ {
+ if (script == null) throw new ArgumentNullException(nameof(script));
+ if (script.GetType() != ProfileScriptType)
+ throw new ArtemisCoreException($"This scripting provider only supports profile scripts of type {ProfileScriptType.Name}");
+
+ return CreateProfileScriptEditor((TProfileScript) script);
+ }
+
+ ///
+ /// Called when the UI needs a script editor for a
+ ///
+ /// The script the editor must edit
+ public override IScriptEditorViewModel CreateLayerScriptScriptEditor(LayerScript script)
+ {
+ if (script == null) throw new ArgumentNullException(nameof(script));
+ if (script.GetType() != LayerScriptType)
+ throw new ArtemisCoreException($"This scripting provider only supports layer scripts of type {LayerScriptType.Name}");
+
+ return CreateLayerScriptScriptEditor((TLayerScript) script);
+ }
+
+ ///
+ /// Called when the UI needs a script editor for a
+ ///
+ /// The script the editor must edit
+ public override IScriptEditorViewModel CreatePropertyScriptEditor(PropertyScript script)
+ {
+ if (script == null) throw new ArgumentNullException(nameof(script));
+ if (script.GetType() != PropertyScriptType)
+ throw new ArtemisCoreException($"This scripting provider only supports property scripts of type {PropertyScriptType.Name}");
+
+ return CreatePropertyScriptEditor((TPropertyScript) script);
+ }
+
+ #endregion
+
+ #region Overrides of PluginFeature
+
+ ///
+ internal override void InternalDisable()
+ {
+ base.InternalDisable();
+
+ while (Scripts.Count > 0)
+ Scripts[0].Dispose();
+ }
+
+ #endregion
+ }
+
+ ///
+ /// Allows you to implement and register your own scripting provider.
+ ///
+ /// Note: You can't implement this, implement
+ /// instead.
+ ///
+ ///
+ public abstract class ScriptingProvider : PluginFeature
+ {
+ internal abstract Type GlobalScriptType { get; }
+ internal abstract Type PropertyScriptType { get; }
+ internal abstract Type LayerScriptType { get; }
+ internal abstract Type ProfileScriptType { get; }
+ internal List