diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 42daefc59..71aeb0167 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -39,8 +39,9 @@ + - + diff --git a/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs b/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs deleted file mode 100644 index 5b7b353a0..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs +++ /dev/null @@ -1,167 +0,0 @@ -using SkiaSharp; -using System; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Artemis.Core.ColorScience; - -internal readonly struct ColorRanges -{ - public readonly byte RedRange; - public readonly byte GreenRange; - public readonly byte BlueRange; - - public ColorRanges(byte redRange, byte greenRange, byte blueRange) - { - this.RedRange = redRange; - this.GreenRange = greenRange; - this.BlueRange = blueRange; - } -} - -internal readonly struct ColorCube -{ - private const int BYTES_PER_COLOR = 4; - private static readonly int ELEMENTS_PER_VECTOR = Vector.Count / BYTES_PER_COLOR; - private static readonly int BYTES_PER_VECTOR = ELEMENTS_PER_VECTOR * BYTES_PER_COLOR; - - private readonly int _from; - private readonly int _length; - private readonly SortTarget _currentOrder = SortTarget.None; - - public ColorCube(in Span fullColorList, int from, int length, SortTarget preOrdered) - { - this._from = from; - this._length = length; - - if (length < 2) return; - - Span colors = fullColorList.Slice(from, length); - ColorRanges colorRanges = GetColorRanges(colors); - - if ((colorRanges.RedRange > colorRanges.GreenRange) && (colorRanges.RedRange > colorRanges.BlueRange)) - { - if (preOrdered != SortTarget.Red) - QuantizerSort.SortRed(colors); - - _currentOrder = SortTarget.Red; - } - else if (colorRanges.GreenRange > colorRanges.BlueRange) - { - if (preOrdered != SortTarget.Green) - QuantizerSort.SortGreen(colors); - - _currentOrder = SortTarget.Green; - } - else - { - if (preOrdered != SortTarget.Blue) - QuantizerSort.SortBlue(colors); - - _currentOrder = SortTarget.Blue; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ColorRanges GetColorRanges(in ReadOnlySpan colors) - { - if (Vector.IsHardwareAccelerated && (colors.Length >= Vector.Count)) - { - int chunks = colors.Length / ELEMENTS_PER_VECTOR; - int vectorElements = (chunks * ELEMENTS_PER_VECTOR); - int missingElements = colors.Length - vectorElements; - - Vector max = Vector.Zero; - Vector min = new(byte.MaxValue); - foreach (Vector currentVector in MemoryMarshal.Cast>(colors[..vectorElements])) - { - max = Vector.Max(max, currentVector); - min = Vector.Min(min, currentVector); - } - - byte redMin = byte.MaxValue; - byte redMax = byte.MinValue; - byte greenMin = byte.MaxValue; - byte greenMax = byte.MinValue; - byte blueMin = byte.MaxValue; - byte blueMax = byte.MinValue; - - for (int i = 0; i < BYTES_PER_VECTOR; i += BYTES_PER_COLOR) - { - if (min[i + 2] < redMin) redMin = min[i + 2]; - if (max[i + 2] > redMax) redMax = max[i + 2]; - if (min[i + 1] < greenMin) greenMin = min[i + 1]; - if (max[i + 1] > greenMax) greenMax = max[i + 1]; - if (min[i] < blueMin) blueMin = min[i]; - if (max[i] > blueMax) blueMax = max[i]; - } - - for (int i = 0; i < missingElements; i++) - { - SKColor color = colors[^(i + 1)]; - - if (color.Red < redMin) redMin = color.Red; - if (color.Red > redMax) redMax = color.Red; - if (color.Green < greenMin) greenMin = color.Green; - if (color.Green > greenMax) greenMax = color.Green; - if (color.Blue < blueMin) blueMin = color.Blue; - if (color.Blue > blueMax) blueMax = color.Blue; - } - - return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin)); - } - else - { - byte redMin = byte.MaxValue; - byte redMax = byte.MinValue; - byte greenMin = byte.MaxValue; - byte greenMax = byte.MinValue; - byte blueMin = byte.MaxValue; - byte blueMax = byte.MinValue; - - foreach (SKColor color in colors) - { - if (color.Red < redMin) redMin = color.Red; - if (color.Red > redMax) redMax = color.Red; - if (color.Green < greenMin) greenMin = color.Green; - if (color.Green > greenMax) greenMax = color.Green; - if (color.Blue < blueMin) blueMin = color.Blue; - if (color.Blue > blueMax) blueMax = color.Blue; - } - - return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin)); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Split(in Span fullColorList, out ColorCube a, out ColorCube b) - { - Span colors = fullColorList.Slice(_from, _length); - - int median = colors.Length / 2; - - a = new ColorCube(fullColorList, _from, median, _currentOrder); - b = new ColorCube(fullColorList, _from + median, colors.Length - median, _currentOrder); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal SKColor GetAverageColor(in ReadOnlySpan fullColorList) - { - ReadOnlySpan colors = fullColorList.Slice(_from, _length); - - int r = 0, g = 0, b = 0; - foreach (SKColor color in colors) - { - r += color.Red; - g += color.Green; - b += color.Blue; - } - - return new SKColor( - (byte)(r / colors.Length), - (byte)(g / colors.Length), - (byte)(b / colors.Length) - ); - } -} diff --git a/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs b/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs index 1afaa4f3c..b2bf6f797 100644 --- a/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs +++ b/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs @@ -1,7 +1,9 @@ -using SkiaSharp; +using HPPH; +using SkiaSharp; using System; using System.Collections.Generic; using System.Numerics; +using System.Runtime.InteropServices; namespace Artemis.Core.ColorScience; @@ -10,13 +12,27 @@ namespace Artemis.Core.ColorScience; /// public static class ColorQuantizer { + /// + [Obsolete("Use Quantize(Span colors, int amount) in-parameter instead")] + public static SKColor[] Quantize(in Span colors, int amount) + { + return Quantize(colors, amount); + } + + /// + [Obsolete("Use QuantizeSplit(Span colors, int splits) without the in-parameter instead")] + public static SKColor[] QuantizeSplit(in Span colors, int splits) + { + return QuantizeSplit(colors, splits); + } + /// /// Quantizes a span of colors into the desired amount of representative colors. /// /// The colors to quantize /// How many colors to return. Must be a power of two. /// colors. - public static SKColor[] Quantize(in Span colors, int amount) + public static SKColor[] Quantize(Span colors, int amount) { if (!BitOperations.IsPow2(amount)) throw new ArgumentException("Must be power of two", nameof(amount)); @@ -24,38 +40,19 @@ public static class ColorQuantizer int splits = BitOperations.Log2((uint)amount); return QuantizeSplit(colors, splits); } - + /// /// Quantizes a span of colors, splitting the average number of times. /// /// The colors to quantize /// How many splits to execute. Each split doubles the number of colors returned. /// Up to (2 ^ ) number of colors. - public static SKColor[] QuantizeSplit(in Span colors, int splits) + public static SKColor[] QuantizeSplit(Span colors, int splits) { if (colors.Length < (1 << splits)) throw new ArgumentException($"The color array must at least contain ({(1 << splits)}) to perform {splits} splits."); - Span cubes = new ColorCube[1 << splits]; - cubes[0] = new ColorCube(colors, 0, colors.Length, SortTarget.None); - - int currentIndex = 0; - for (int i = 0; i < splits; i++) - { - int currentCubeCount = 1 << i; - Span currentCubes = cubes.Slice(0, currentCubeCount); - for (int j = 0; j < currentCubes.Length; j++) - { - currentCubes[j].Split(colors, out ColorCube a, out ColorCube b); - currentCubes[j] = a; - cubes[++currentIndex] = b; - } - } - - SKColor[] result = new SKColor[cubes.Length]; - for (int i = 0; i < cubes.Length; i++) - result[i] = cubes[i].GetAverageColor(colors); - - return result; + // DarthAffe 22.07.2024: This is not ideal as it allocates an additional array, but i don't see a way to get SKColors out here + return MemoryMarshal.Cast(MemoryMarshal.Cast(colors).CreateSimpleColorPalette(1 << splits)).ToArray(); } /// diff --git a/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs b/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs deleted file mode 100644 index 46b593ece..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs +++ /dev/null @@ -1,121 +0,0 @@ -using SkiaSharp; -using System; -using System.Buffers; - -namespace Artemis.Core.ColorScience; - -//HACK DarthAffe 17.11.2022: Sorting is a really hot path in the quantizer, therefore abstracting this into cleaner code (one method with parameter or something like that) sadly has a well measurable performance impact. -internal static class QuantizerSort -{ - #region Methods - - public static void SortRed(in Span colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Red]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span currentBucketIndex = stackalloc int[256]; - - int offset = 0; - for (int i = 0; i < counts.Length; i++) - { - currentBucketIndex[i] = offset; - offset += counts[i]; - } - - foreach (SKColor color in colors) - { - int index = color.Red; - int bucketIndex = currentBucketIndex[index]; - currentBucketIndex[index]++; - buckets[bucketIndex] = color; - } - - buckets.CopyTo(colors); - } - finally - { - ArrayPool.Shared.Return(bucketsArray); - } - } - - public static void SortGreen(in Span colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Green]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span currentBucketIndex = stackalloc int[256]; - - int offset = 0; - for (int i = 0; i < counts.Length; i++) - { - currentBucketIndex[i] = offset; - offset += counts[i]; - } - - foreach (SKColor color in colors) - { - int index = color.Green; - int bucketIndex = currentBucketIndex[index]; - currentBucketIndex[index]++; - buckets[bucketIndex] = color; - } - - buckets.CopyTo(colors); - } - finally - { - ArrayPool.Shared.Return(bucketsArray); - } - } - - public static void SortBlue(in Span colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Blue]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span currentBucketIndex = stackalloc int[256]; - - int offset = 0; - for (int i = 0; i < counts.Length; i++) - { - currentBucketIndex[i] = offset; - offset += counts[i]; - } - - foreach (SKColor color in colors) - { - int index = color.Blue; - int bucketIndex = currentBucketIndex[index]; - currentBucketIndex[index]++; - buckets[bucketIndex] = color; - } - - buckets.CopyTo(colors); - } - finally - { - ArrayPool.Shared.Return(bucketsArray); - } - } - - #endregion -} diff --git a/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs b/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs deleted file mode 100644 index 72f58517d..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.Core.ColorScience; - -internal enum SortTarget -{ - None, Red, Green, Blue -} diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 073afa540..187d9c423 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; -using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; using SkiaSharp; @@ -14,15 +12,10 @@ namespace Artemis.Core; public sealed class Profile : ProfileElement { private readonly object _lock = new(); - private readonly ObservableCollection _scriptConfigurations; - private readonly ObservableCollection _scripts; private bool _isFreshImport; internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!) { - _scripts = new ObservableCollection(); - _scriptConfigurations = new ObservableCollection(); - Opacity = 0d; ShouldDisplay = true; Configuration = configuration; @@ -31,8 +24,6 @@ public sealed class Profile : ProfileElement EntityId = profileEntity.Id; Exceptions = new List(); - Scripts = new ReadOnlyObservableCollection(_scripts); - ScriptConfigurations = new ReadOnlyObservableCollection(_scriptConfigurations); Load(); } @@ -41,17 +32,7 @@ public sealed class Profile : ProfileElement /// Gets the profile configuration of this profile /// public ProfileConfiguration Configuration { get; } - - /// - /// Gets a collection of all active scripts assigned to this profile - /// - public ReadOnlyObservableCollection Scripts { get; } - - /// - /// Gets a collection of all script configurations assigned to this profile - /// - public ReadOnlyObservableCollection 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 @@ -85,15 +66,9 @@ public sealed class Profile : ProfileElement 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); - const double OPACITY_PER_SECOND = 1; if (ShouldDisplay && Opacity < 1) @@ -111,9 +86,6 @@ public sealed class Profile : ProfileElement if (Disposed) throw new ObjectDisposedException("Profile"); - foreach (ProfileScript profileScript in Scripts) - profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds); - SKPaint? opacityPaint = null; bool applyOpacityLayer = Configuration.FadeInAndOut && Opacity < 1; @@ -133,9 +105,6 @@ public sealed class Profile : ProfileElement opacityPaint?.Dispose(); } - foreach (ProfileScript profileScript in Scripts) - profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds); - if (!Exceptions.Any()) return; @@ -174,7 +143,7 @@ public sealed class Profile : ProfileElement /// public override IEnumerable GetFeatureDependencies() { - return GetRootFolder().GetFeatureDependencies().Concat(Scripts.Select(c => c.ScriptingProvider)); + return GetRootFolder().GetFeatureDependencies(); } /// @@ -205,10 +174,7 @@ public sealed class Profile : ProfileElement { if (!disposing) return; - - while (Scripts.Count > 0) - RemoveScript(Scripts[0]); - + foreach (ProfileElement profileElement in Children) profileElement.Dispose(); ChildrenList.Clear(); @@ -238,61 +204,11 @@ public sealed class Profile : ProfileElement AddChild(new Folder(this, this, rootFolder)); } - while (_scriptConfigurations.Any()) - RemoveScriptConfiguration(_scriptConfigurations[0]); - foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))) - AddScriptConfiguration(scriptConfiguration); - // Load node scripts last since they may rely on the profile structure being in place foreach (RenderProfileElement renderProfileElement in GetAllRenderElements()) renderProfileElement.LoadNodeScript(); } - /// - /// Removes a script configuration from the profile, if the configuration has an active script it is also removed. - /// - internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration) - { - if (!_scriptConfigurations.Contains(scriptConfiguration)) - return; - - Script? script = scriptConfiguration.Script; - if (script != null) - RemoveScript((ProfileScript) script); - - _scriptConfigurations.Remove(scriptConfiguration); - } - - /// - /// Adds a script configuration to the profile but does not instantiate it's script. - /// - internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration) - { - if (!_scriptConfigurations.Contains(scriptConfiguration)) - _scriptConfigurations.Add(scriptConfiguration); - } - - /// - /// Adds a script that has a script configuration belonging to this profile. - /// - internal void AddScript(ProfileScript script) - { - if (!_scriptConfigurations.Contains(script.ScriptConfiguration)) - throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile."); - - if (!_scripts.Contains(script)) - _scripts.Add(script); - } - - /// - /// Removes a script from the profile and disposes it. - /// - internal void RemoveScript(ProfileScript script) - { - _scripts.Remove(script); - script.Dispose(); - } - internal override void Save() { if (Disposed) @@ -310,12 +226,5 @@ public sealed class Profile : ProfileElement 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/Plugins/ScriptingProviders/IScriptEditorViewModel.cs b/src/Artemis.Core/Plugins/ScriptingProviders/IScriptEditorViewModel.cs deleted file mode 100644 index 9e7674b35..000000000 --- a/src/Artemis.Core/Plugins/ScriptingProviders/IScriptEditorViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Artemis.Core.ScriptingProviders; - -/// -/// Represents a view model containing a script editor -/// -public interface IScriptEditorViewModel -{ - /// - /// Gets the script type this view model was created for - /// - ScriptType ScriptType { get; } - - /// - /// Gets the script this editor is editing - /// - Script? Script { get; } - - /// - /// Called whenever the view model must display a different script - /// - /// The script to display or if no script is to be displayed - void ChangeScript(Script? script); -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs deleted file mode 100644 index eb9c2c287..000000000 --- a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using Artemis.Storage.Entities.General; - -namespace Artemis.Core.ScriptingProviders; - -/// -/// Represents the configuration of a script -/// -public class ScriptConfiguration : CorePropertyChanged, IStorageModel -{ - private bool _hasChanges; - private bool _isSuspended; - private string _name; - private string? _pendingScriptContent; - private string? _scriptContent; - private string _scriptingProviderId; - - /// - /// Creates a new instance of the class - /// - 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(); - } - - /// - /// 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; - private set - { - if (!SetAndNotify(ref _scriptContent, value)) return; - OnScriptContentChanged(); - } - } - - /// - /// Gets or sets the pending changes to the script's content - /// - public string? PendingScriptContent - { - get => _pendingScriptContent; - set - { - if (string.IsNullOrWhiteSpace(value)) - value = null; - if (!SetAndNotify(ref _pendingScriptContent, value)) return; - HasChanges = ScriptContent != PendingScriptContent; - } - } - - // TODO: Implement suspension - /// - /// [NYI] Gets or sets a boolean indicating whether this configuration is suspended - /// - public bool IsSuspended - { - get => _isSuspended; - set => SetAndNotify(ref _isSuspended, value); - } - - /// - /// Gets or sets a boolean indicating whether this configuration has pending changes to it's - /// - /// - public bool HasChanges - { - get => _hasChanges; - set => SetAndNotify(ref _hasChanges, value); - } - - /// - /// If active, gets the script - /// - public Script? Script { get; internal set; } - - internal ScriptConfigurationEntity Entity { get; } - - /// - /// Applies the to the - /// - public void ApplyPendingChanges() - { - ScriptContent = PendingScriptContent; - HasChanges = false; - } - - /// - /// Discards the - /// - public void DiscardPendingChanges() - { - PendingScriptContent = ScriptContent; - HasChanges = false; - } - - /// - /// Occurs whenever the contents of the script have changed - /// - public event EventHandler? ScriptContentChanged; - - /// - /// Invokes the event - /// - protected virtual void OnScriptContentChanged() - { - ScriptContentChanged?.Invoke(this, EventArgs.Empty); - } - - #region Implementation of IStorageModel - - /// - public void Load() - { - ScriptingProviderId = Entity.ScriptingProviderId; - ScriptContent = Entity.ScriptContent; - PendingScriptContent = Entity.ScriptContent; - Name = Entity.Name; - } - - /// - public void Save() - { - Entity.ScriptingProviderId = ScriptingProviderId; - Entity.ScriptContent = ScriptContent; - Entity.Name = Name; - } - - #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 deleted file mode 100644 index 5148c823a..000000000 --- a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs +++ /dev/null @@ -1,80 +0,0 @@ -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 -{ - #region Overrides of PluginFeature - - /// - internal override void InternalDisable() - { - base.InternalDisable(); - - while (Scripts.Count > 0) - Scripts[0].Dispose(); - } - - #endregion - - #region Overrides of ScriptingProvider - - /// - internal override Type ProfileScriptType => typeof(TProfileScript); - - /// - internal override Type GlobalScriptType => typeof(TGlobalScript); - - #endregion -} - -/// -/// Allows you to implement and register your own scripting provider. -/// -/// Note: You can't implement this, implement -/// instead. -/// -/// -public abstract class ScriptingProvider : PluginFeature -{ - /// - /// The base constructor of the class - /// - protected ScriptingProvider() - { - Scripts = new ReadOnlyCollection