├── .gitignore ├── CONTRIBUTING.md ├── ElgatoStreamDeck ├── ConcurrentDevice.cs ├── Device.cs ├── DeviceManager.cs ├── DeviceReader.cs ├── ElgatoStreamDeck.csproj ├── IDevice.cs ├── ImageMode.cs ├── ImageUtils.cs ├── Input.cs ├── Kind.cs └── SafeHidHandle.cs ├── LICENSE ├── README.md ├── Streamduck ├── App.cs ├── BaseFunctionality │ ├── Actions │ │ └── TestAction.cs │ ├── CorePlugin.cs │ ├── Renderers │ │ └── DefaultRenderer.cs │ ├── SocketRequests │ │ ├── CoreVersion.cs │ │ ├── Device.cs │ │ ├── DeviceList │ │ │ ├── ListDevices.cs │ │ │ └── SetDeviceAutoconnect.cs │ │ ├── Devices │ │ │ ├── ConnectDevice.cs │ │ │ ├── GetDeviceInputs.cs │ │ │ ├── GetDeviceItems.cs │ │ │ ├── GetDeviceScreenStack.cs │ │ │ ├── PopScreen.cs │ │ │ └── PushNewEmptyScreen.cs │ │ └── SRUtil.cs │ └── Triggers │ │ └── ButtonDownTrigger.cs ├── Configuration │ ├── Config.cs │ ├── Devices │ │ ├── DeviceConfig.cs │ │ ├── SerializedAction.cs │ │ ├── SerializedScreen.cs │ │ ├── SerializedScreenItem.cs │ │ └── SerializedTrigger.cs │ └── GlobalConfig.cs ├── Constants │ └── EventNames.cs ├── Cores │ ├── CoreImpl.cs │ ├── ScreenImpl.cs │ └── ScreenItems │ │ ├── RenderableScreenItem.cs │ │ └── ScreenlessItem.cs ├── Plugins │ ├── Extensions │ │ ├── DeviceSerializationExtensions.cs │ │ ├── NamespacedDriverExtensions.cs │ │ ├── PluginCollectionExtensions.cs │ │ └── StreamduckExtensions.cs │ ├── Loaders │ │ ├── PluginLoadContext.cs │ │ └── PluginLoader.cs │ ├── Methods │ │ ├── ConfigurableReflectedAction.cs │ │ └── ReflectedAction.cs │ ├── PluginAssembly.cs │ ├── PluginCollection.cs │ ├── PluginReflector.cs │ └── WrappedPlugin.cs ├── Program.cs ├── Socket │ ├── Server.cs │ └── SessionRequester.cs └── Streamduck.csproj ├── StreamduckGS ├── Class1.cs ├── Parser │ └── gs.g4 └── StreamduckGS.csproj ├── StreamduckShared ├── Actions │ ├── ActionInstance.cs │ └── PluginAction.cs ├── Attributes │ ├── AutoAddAttribute.cs │ ├── BitmaskAttribute.cs │ ├── DescriptionAttribute.cs │ ├── HeaderAttribute.cs │ ├── IgnoreAttribute.cs │ ├── IncludeAttribute.cs │ ├── NameAttribute.cs │ ├── NumberFieldAttribute.cs │ ├── PluginMethodAttribute.cs │ ├── ReadOnlyAttribute.cs │ ├── StaticTextAttribute.cs │ └── SwitchAttribute.cs ├── Cores │ ├── Core.cs │ ├── Screen.cs │ └── ScreenItem.cs ├── Data │ ├── Double2.cs │ ├── Int2.cs │ ├── Namespaced.cs │ └── UInt2.cs ├── Devices │ ├── Device.cs │ ├── DeviceDisconnectedException.cs │ ├── DeviceIdentifier.cs │ ├── DeviceMetadata.cs │ ├── IDevicePoolable.cs │ └── NamespacedDeviceIdentifier.cs ├── Fields │ ├── Field.cs │ └── FieldReflector.cs ├── IStreamduck.cs ├── Images │ ├── IImageCollection.cs │ └── ImageExtensions.cs ├── Inputs │ ├── IInputButton.cs │ ├── IInputDisplay.cs │ ├── IInputEncoder.cs │ ├── IInputJoystick.cs │ ├── IInputKnob.cs │ ├── IInputPressure.cs │ ├── IInputSlider.cs │ ├── IInputToggle.cs │ ├── IInputTouchScreen.cs │ ├── IInputTrackpad.cs │ ├── Input.cs │ └── InputIcon.cs ├── Interfaces │ ├── IConfigurable.cs │ └── INamed.cs ├── Plugins │ ├── Driver.cs │ ├── IPluginQuery.cs │ ├── NamespacedName.cs │ └── Plugin.cs ├── Rendering │ └── Renderer.cs ├── Socket │ ├── SocketMessage.cs │ ├── SocketRequest.cs │ └── SocketRequester.cs ├── StreamduckShared.csproj ├── Triggers │ ├── Trigger.cs │ └── TriggerInstance.cs └── Utils │ ├── ScreenExtensions.cs │ └── UtilMethods.cs ├── StreamduckStreamDeck ├── Inputs │ ├── StreamDeckButton.cs │ ├── StreamDeckButtonWithoutDisplay.cs │ ├── StreamDeckEncoder.cs │ └── StreamDeckLCDSegment.cs ├── StreamDeckDevice.cs ├── StreamDeckDeviceOptions.cs ├── StreamDeckDriver.cs ├── StreamDeckPlugin.cs └── StreamduckStreamDeck.csproj ├── StreamduckTest ├── FieldReflectorTest.cs ├── PluginReflectorTest.cs ├── StreamduckTest.csproj └── Usings.cs ├── streamduck-sharp.sln └── why-dotnet.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | .idea/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### I currently don't accept issues or pull requests 2 | The software is in very early stage, during which I want to fully realize the vision of the project. Until then, the software is not ready to be used and everything is subject to change 3 | -------------------------------------------------------------------------------- /ElgatoStreamDeck/ConcurrentDevice.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using SixLabors.ImageSharp; 7 | using SixLabors.ImageSharp.PixelFormats; 8 | 9 | namespace ElgatoStreamDeck; 10 | 11 | public class ConcurrentDevice(Device device) : IDevice { 12 | private readonly Kind _kind = device.Kind(); 13 | 14 | public Kind Kind() => _kind; 15 | 16 | public string Manufacturer() { 17 | lock (device) { 18 | return device.Manufacturer(); 19 | } 20 | } 21 | 22 | public string Product() { 23 | lock (device) { 24 | return device.Product(); 25 | } 26 | } 27 | 28 | public string SerialNumber() { 29 | lock (device) { 30 | return device.SerialNumber(); 31 | } 32 | } 33 | 34 | public string FirmwareVersion() { 35 | lock (device) { 36 | return device.FirmwareVersion(); 37 | } 38 | } 39 | 40 | public Input? ReadInput(int? timeout) { 41 | lock (device) { 42 | return device.ReadInput(timeout); 43 | } 44 | } 45 | 46 | public void Reset() { 47 | lock (device) { 48 | device.Reset(); 49 | } 50 | } 51 | 52 | public void SetBrightness(byte percent) { 53 | lock (device) { 54 | device.SetBrightness(percent); 55 | } 56 | } 57 | 58 | public void WriteImage(byte keyIndex, ReadOnlySpan imageData) { 59 | lock (device) { 60 | device.WriteImage(keyIndex, imageData); 61 | } 62 | } 63 | 64 | public void WriteLcd(ushort x, ushort y, ushort w, ushort h, ReadOnlySpan imageData) { 65 | lock (device) { 66 | device.WriteLcd(x, y, w, h, imageData); 67 | } 68 | } 69 | 70 | public void ClearButtonImage(byte keyIndex) { 71 | lock (device) { 72 | device.ClearButtonImage(keyIndex); 73 | } 74 | } 75 | 76 | public void SetButtonImage(byte keyIndex, Image image) { 77 | lock (device) { 78 | device.SetButtonImage(keyIndex, image); 79 | } 80 | } 81 | 82 | public void SetButtonImage(byte keyIndex, Image image) { 83 | lock (device) { 84 | device.SetButtonImage(keyIndex, image); 85 | } 86 | } 87 | 88 | public void SetButtonImage(byte keyIndex, ReadOnlySpan image, int width, int height) { 89 | lock (device) { 90 | device.SetButtonImage(keyIndex, image, width, height); 91 | } 92 | } 93 | 94 | public void Dispose() { 95 | device.Dispose(); 96 | GC.SuppressFinalize(this); 97 | } 98 | } -------------------------------------------------------------------------------- /ElgatoStreamDeck/DeviceManager.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using HidApi; 9 | 10 | namespace ElgatoStreamDeck; 11 | 12 | /** 13 | * Instantiates HID library under the hood, only one instance should exist 14 | */ 15 | public class DeviceManager { 16 | private const ushort ElgatoVendorId = 0x0fd9; 17 | 18 | private static DeviceManager? _instance; 19 | 20 | private readonly SafeHidHandle _hid; 21 | 22 | private DeviceManager() => _hid = new SafeHidHandle(); 23 | 24 | public IEnumerable<(Kind, string)> ListDevices() { 25 | return Hid.Enumerate(ElgatoVendorId) 26 | .Select(info => (info.ProductId.ToKind(), info.SerialNumber)); 27 | } 28 | 29 | public Device ConnectDevice(Kind kind, string serial) { 30 | if (kind == Kind.Unknown) throw new ArgumentException("Can't connect to unrecognized device kind"); 31 | 32 | return new Device(new HidApi.Device(ElgatoVendorId, kind.ToPid(), serial), kind); 33 | } 34 | 35 | public ConcurrentDevice ConnectDeviceConcurrent(Kind kind, string serial) { 36 | if (kind == Kind.Unknown) throw new ArgumentException("Can't connect to unrecognized device kind"); 37 | 38 | return new ConcurrentDevice(new Device(new HidApi.Device(ElgatoVendorId, kind.ToPid(), serial), kind)); 39 | } 40 | 41 | /** 42 | * Should be called when you'll never use the manager again, frees the HID library 43 | */ 44 | public void Dispose() { 45 | _hid.Dispose(); 46 | _instance = null; 47 | } 48 | 49 | public static DeviceManager Get() { 50 | _instance ??= new DeviceManager(); 51 | return _instance; 52 | } 53 | } -------------------------------------------------------------------------------- /ElgatoStreamDeck/DeviceReader.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace ElgatoStreamDeck; 8 | 9 | using DeviceInput = Input; 10 | 11 | public class DeviceReader { 12 | private readonly bool[] _buttonStates; 13 | private readonly IDevice _device; 14 | private readonly bool[] _encoderStates; 15 | 16 | public DeviceReader(IDevice device) { 17 | _device = device; 18 | _buttonStates = new bool[_device.Kind().KeyCount()]; 19 | _encoderStates = new bool[_device.Kind().EncoderCount()]; 20 | } 21 | 22 | /** 23 | * Blocking, should be ran in another thread 24 | */ 25 | public IEnumerable Read() { 26 | if (_device.ReadInput() is not { } input) yield break; 27 | 28 | switch (input) { 29 | case DeviceInput.ButtonStateChange buttonStateChange: { 30 | for (var i = 0; i < buttonStateChange.Buttons.Length; i++) { 31 | var oldState = _buttonStates[i]; 32 | var newState = buttonStateChange.Buttons[i]; 33 | 34 | if (oldState == newState) continue; 35 | 36 | _buttonStates[i] = newState; 37 | yield return newState 38 | ? new Input.ButtonPressed((ushort)i) 39 | : new Input.ButtonReleased((ushort)i); 40 | } 41 | 42 | break; 43 | } 44 | 45 | case DeviceInput.EncoderStateChange encoderStateChange: { 46 | for (var i = 0; i < encoderStateChange.Encoders.Length; i++) { 47 | var oldState = _encoderStates[i]; 48 | var newState = encoderStateChange.Encoders[i]; 49 | 50 | if (oldState == newState) continue; 51 | 52 | _encoderStates[i] = newState; 53 | 54 | yield return newState 55 | ? new Input.EncoderPressed((ushort)i) 56 | : new Input.EncoderReleased((ushort)i); 57 | } 58 | 59 | break; 60 | } 61 | 62 | case DeviceInput.EncoderTwist encoderTwist: { 63 | for (var i = 0; i < encoderTwist.Encoders.Length; i++) 64 | if (encoderTwist.Encoders[i] != 0) 65 | yield return new Input.EncoderTwist((ushort)i, encoderTwist.Encoders[i]); 66 | 67 | break; 68 | } 69 | 70 | case DeviceInput.TouchScreenPress touchScreenPress: { 71 | yield return new Input.TouchScreenPress(touchScreenPress.X, touchScreenPress.Y); 72 | 73 | break; 74 | } 75 | 76 | case DeviceInput.TouchScreenLongPress touchScreenLongPress: { 77 | yield return new Input.TouchScreenLongPress(touchScreenLongPress.X, touchScreenLongPress.Y); 78 | 79 | break; 80 | } 81 | 82 | case DeviceInput.TouchScreenSwipe touchScreenSwipe: { 83 | yield return new Input.TouchScreenSwipe( 84 | touchScreenSwipe.StartX, touchScreenSwipe.StartY, 85 | touchScreenSwipe.EndX, touchScreenSwipe.EndY 86 | ); 87 | 88 | break; 89 | } 90 | } 91 | } 92 | 93 | public record Input { 94 | private Input() { } 95 | 96 | public record ButtonPressed(ushort key) : Input; 97 | 98 | public record ButtonReleased(ushort key) : Input; 99 | 100 | public record EncoderPressed(ushort encoder) : Input; 101 | 102 | public record EncoderReleased(ushort encoder) : Input; 103 | 104 | public record EncoderTwist(ushort encoder, sbyte value) : Input; 105 | 106 | public record TouchScreenPress(ushort X, ushort Y) : Input; 107 | 108 | public record TouchScreenLongPress(ushort X, ushort Y) : Input; 109 | 110 | public record TouchScreenSwipe(ushort StartX, ushort StartY, ushort EndX, ushort EndY) : Input; 111 | } 112 | } -------------------------------------------------------------------------------- /ElgatoStreamDeck/ElgatoStreamDeck.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | disable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ElgatoStreamDeck/IDevice.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using SixLabors.ImageSharp; 7 | using SixLabors.ImageSharp.PixelFormats; 8 | 9 | namespace ElgatoStreamDeck; 10 | 11 | public interface IDevice : IDisposable { 12 | Kind Kind(); 13 | string Manufacturer(); 14 | string Product(); 15 | string SerialNumber(); 16 | string FirmwareVersion(); 17 | Input? ReadInput(int? timeout = null); 18 | void Reset(); 19 | void SetBrightness(byte percent); 20 | void WriteImage(byte keyIndex, ReadOnlySpan imageData); 21 | void WriteLcd(ushort x, ushort y, ushort w, ushort h, ReadOnlySpan imageData); 22 | void ClearButtonImage(byte keyIndex); 23 | void SetButtonImage(byte keyIndex, Image image); 24 | void SetButtonImage(byte keyIndex, Image image); 25 | void SetButtonImage(byte keyIndex, ReadOnlySpan image, int width, int height); 26 | } -------------------------------------------------------------------------------- /ElgatoStreamDeck/ImageMode.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace ElgatoStreamDeck; 6 | 7 | public readonly struct ImageMode { 8 | public ImageFormat Mode { get; init; } 9 | public (uint, uint) Resolution { get; init; } 10 | public ImageRotation Rotation { get; init; } 11 | public ImageMirroring Mirror { get; init; } 12 | } 13 | 14 | public enum ImageFormat { 15 | None, 16 | Bmp, 17 | Jpeg 18 | } 19 | 20 | public enum ImageRotation { 21 | Rot0, 22 | Rot90, 23 | Rot180, 24 | Rot270 25 | } 26 | 27 | public enum ImageMirroring { 28 | None, 29 | X, 30 | Y, 31 | Both 32 | } -------------------------------------------------------------------------------- /ElgatoStreamDeck/ImageUtils.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.IO; 7 | using System.Threading.Tasks; 8 | using SixLabors.ImageSharp; 9 | using SixLabors.ImageSharp.Formats.Jpeg; 10 | using SixLabors.ImageSharp.PixelFormats; 11 | using SixLabors.ImageSharp.Processing; 12 | using SixLabors.ImageSharp.Processing.Processors.Transforms; 13 | 14 | namespace ElgatoStreamDeck; 15 | 16 | public static class ImageUtils { 17 | private static void MutateImageForDevice(Image image, ImageMode mode) { 18 | image.Mutate( 19 | i => { 20 | i.Resize( 21 | new ResizeOptions { 22 | Mode = ResizeMode.Crop, 23 | Size = new Size((int)mode.Resolution.Item1, (int)mode.Resolution.Item2), 24 | Sampler = new BicubicResampler() 25 | } 26 | ); 27 | 28 | i.Rotate( 29 | mode.Rotation switch { 30 | ImageRotation.Rot0 => RotateMode.None, 31 | ImageRotation.Rot90 => RotateMode.Rotate90, 32 | ImageRotation.Rot180 => RotateMode.Rotate180, 33 | ImageRotation.Rot270 => RotateMode.Rotate270, 34 | _ => throw new ArgumentOutOfRangeException(nameof(mode)) 35 | } 36 | ); 37 | 38 | switch (mode.Mirror) { 39 | case ImageMirroring.None: 40 | break; 41 | case ImageMirroring.X: 42 | i.Flip(FlipMode.Horizontal); 43 | break; 44 | case ImageMirroring.Y: 45 | i.Flip(FlipMode.Vertical); 46 | break; 47 | case ImageMirroring.Both: 48 | i.Flip(FlipMode.Horizontal); 49 | i.Flip(FlipMode.Vertical); 50 | break; 51 | default: 52 | throw new ArgumentOutOfRangeException(nameof(mode)); 53 | } 54 | } 55 | ); 56 | } 57 | 58 | public static byte[] EncodeImageForButton(Image image, Kind kind, int jpegQuality = 94) { 59 | var mode = kind.KeyImageMode(); 60 | 61 | if (mode.Mode == ImageFormat.None) return Array.Empty(); 62 | 63 | MutateImageForDevice(image, mode); 64 | 65 | using var buffer = new MemoryStream(); 66 | 67 | switch (mode.Mode) { 68 | case ImageFormat.Bmp: 69 | image.SaveAsBmp(buffer); 70 | break; 71 | case ImageFormat.Jpeg: 72 | image.Save( 73 | buffer, new JpegEncoder { 74 | Quality = jpegQuality 75 | } 76 | ); 77 | break; 78 | case ImageFormat.None: 79 | default: 80 | throw new ArgumentOutOfRangeException(nameof(kind)); 81 | } 82 | 83 | return buffer.ToArray(); 84 | } 85 | 86 | public static byte[] EncodeImageForButton(Image image, Kind kind, int jpegQuality = 94) => 87 | EncodeImageForButton(image.CloneAs(), kind, jpegQuality); 88 | 89 | /** 90 | * Pixels must be expressed as 3 bytes of Red, Green and Blue 91 | */ 92 | public static byte[] EncodeImageForButton(ReadOnlySpan image, int width, int height, Kind kind, 93 | int jpegQuality = 94 94 | ) => 95 | EncodeImageForButton( 96 | Image.LoadPixelData(image, width, height), kind, jpegQuality 97 | ); 98 | 99 | public static async Task EncodeImageForButtonAsync(Image image, Kind kind, int jpegQuality = 94) { 100 | var mode = kind.KeyImageMode(); 101 | 102 | if (mode.Mode == ImageFormat.None) return Array.Empty(); 103 | 104 | MutateImageForDevice(image, mode); 105 | 106 | using var buffer = new MemoryStream(); 107 | 108 | switch (mode.Mode) { 109 | case ImageFormat.Bmp: 110 | await image.SaveAsBmpAsync(buffer); 111 | break; 112 | case ImageFormat.Jpeg: 113 | await image.SaveAsync( 114 | buffer, new JpegEncoder { 115 | Quality = jpegQuality 116 | } 117 | ); 118 | break; 119 | case ImageFormat.None: 120 | default: 121 | throw new ArgumentOutOfRangeException(nameof(kind)); 122 | } 123 | 124 | return buffer.ToArray(); 125 | } 126 | 127 | public static async Task EncodeImageForButtonAsync(Image image, Kind kind, int jpegQuality = 94) => 128 | await EncodeImageForButtonAsync(image.CloneAs(), kind, jpegQuality); 129 | 130 | public static byte[] EncodeImageForLcd(Image image, int targetWidth, int targetHeight, 131 | int jpegQuality = 94 132 | ) { 133 | image.Mutate( 134 | i => { 135 | i.Resize( 136 | new ResizeOptions { 137 | Mode = ResizeMode.Crop, 138 | Size = new Size(targetWidth, targetHeight), 139 | Sampler = new BicubicResampler() 140 | } 141 | ); 142 | } 143 | ); 144 | 145 | using var buffer = new MemoryStream(); 146 | 147 | image.Save( 148 | buffer, new JpegEncoder { 149 | Quality = jpegQuality 150 | } 151 | ); 152 | 153 | return buffer.ToArray(); 154 | } 155 | 156 | public static byte[] EncodeImageForLcd(Image image, int targetWidth, int targetHeight, int jpegQuality = 94) => 157 | EncodeImageForLcd( 158 | image.CloneAs(), targetWidth, targetHeight, jpegQuality 159 | ); 160 | 161 | public static byte[] EncodeImageForLcd(ReadOnlySpan image, int width, int height, int targetWidth, 162 | int targetHeight, int jpegQuality = 94 163 | ) => 164 | EncodeImageForLcd( 165 | Image.LoadPixelData(image, width, height), targetWidth, targetHeight, jpegQuality 166 | ); 167 | 168 | public static async Task EncodeImageForLcdAsync(Image image, int targetWidth, int targetHeight, 169 | int jpegQuality = 94 170 | ) { 171 | image.Mutate( 172 | i => { 173 | i.Resize( 174 | new ResizeOptions { 175 | Mode = ResizeMode.Crop, 176 | Size = new Size(targetWidth, targetHeight), 177 | Sampler = new BicubicResampler() 178 | } 179 | ); 180 | } 181 | ); 182 | 183 | using var buffer = new MemoryStream(); 184 | 185 | await image.SaveAsync( 186 | buffer, new JpegEncoder { 187 | Quality = jpegQuality 188 | } 189 | ); 190 | 191 | return buffer.ToArray(); 192 | } 193 | 194 | public static async Task EncodeImageForLcdAsync(Image image, int targetWidth, int targetHeight, 195 | int jpegQuality = 94 196 | ) => 197 | await EncodeImageForLcdAsync( 198 | image.CloneAs(), targetWidth, targetHeight, jpegQuality 199 | ); 200 | } -------------------------------------------------------------------------------- /ElgatoStreamDeck/Input.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace ElgatoStreamDeck; 6 | 7 | public record Input { 8 | private Input() { } 9 | 10 | public record ButtonStateChange(bool[] Buttons) : Input; 11 | 12 | public record EncoderStateChange(bool[] Encoders) : Input; 13 | 14 | public record EncoderTwist(sbyte[] Encoders) : Input; 15 | 16 | public record TouchScreenPress(ushort X, ushort Y) : Input; 17 | 18 | public record TouchScreenLongPress(ushort X, ushort Y) : Input; 19 | 20 | public record TouchScreenSwipe(ushort StartX, ushort StartY, ushort EndX, ushort EndY) : Input; 21 | } -------------------------------------------------------------------------------- /ElgatoStreamDeck/SafeHidHandle.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using HidApi; 6 | using Microsoft.Win32.SafeHandles; 7 | 8 | namespace ElgatoStreamDeck; 9 | 10 | public class SafeHidHandle : SafeHandleZeroOrMinusOneIsInvalid { 11 | public SafeHidHandle() : base(true) { 12 | SetHandle(1); 13 | Hid.Init(); 14 | } 15 | 16 | public override bool IsInvalid => false; 17 | 18 | protected override bool ReleaseHandle() { 19 | Hid.Exit(); 20 | 21 | return true; 22 | } 23 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streamduck 2 | ![streamducklogo_cut](https://user-images.githubusercontent.com/12719947/151142599-07620c87-3b51-4a65-b956-4a5902f2f52c.png) 3 |
4 | Open Source and Cross Platform software to manage macro devices like Elgato Stream Deck 5 | 6 | ### Currently in heavy development, groundwork is being laid! 7 | 8 | #### [docs.streamduck.org](https://docs.streamduck.org/) will be rewritten once the project is ready to be used 9 | #### [Learn why I switched to C# for this project](why-dotnet.md) 10 | #### If you're looking for Rust version's code, [here it is](https://github.com/streamduck-org/streamduck/tree/old-master) 11 | 12 | ## Project Structure 13 | 14 | ### Streamduck 15 | Main functionality, the daemon executable and plugin loading functionality 16 | 17 | ### StreamduckShared 18 | Definitions that are shared between the app and the plugin 19 | 20 | ### StreamduckStreamDeck 21 | Plugin that adds Stream Deck device support into Streamduck 22 | 23 | ### ElgatoStreamDeck 24 | Library that is used by StreamduckStreamDeck to interact with Stream Deck devices -------------------------------------------------------------------------------- /Streamduck/App.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using NLog; 14 | using Streamduck.Actions; 15 | using Streamduck.BaseFunctionality.Actions; 16 | using Streamduck.BaseFunctionality.Triggers; 17 | using Streamduck.Configuration; 18 | using Streamduck.Configuration.Devices; 19 | using Streamduck.Cores; 20 | using Streamduck.Devices; 21 | using Streamduck.Images; 22 | using Streamduck.Plugins; 23 | using Streamduck.Plugins.Extensions; 24 | using Streamduck.Plugins.Loaders; 25 | using Streamduck.Utils; 26 | 27 | namespace Streamduck; 28 | 29 | public class App : IStreamduck { 30 | private static readonly Logger _l = LogManager.GetCurrentClassLogger(); 31 | 32 | private readonly List _discoveredDevices = new(); 33 | 34 | private Config _config = null!; 35 | 36 | private bool _initialized; 37 | private bool _running; 38 | public static App? CurrentInstance { get; private set; } 39 | 40 | public PluginCollection? PluginCollection { get; private set; } 41 | 42 | public IReadOnlyList DiscoveredDeviceList { 43 | get { 44 | lock (_discoveredDevices) { 45 | return _discoveredDevices.AsEnumerable().ToList(); 46 | } 47 | } 48 | } 49 | 50 | public ConcurrentDictionary ConnectedDeviceList { get; } = new(); 51 | 52 | public IPluginQuery Plugins => PluginCollection!; 53 | public IImageCollection Images { get; } 54 | public IReadOnlyCollection DiscoveredDevices => DiscoveredDeviceList; 55 | public IReadOnlyDictionary ConnectedDevices => ConnectedDeviceList; 56 | 57 | 58 | public event Action? DeviceListRefreshed; 59 | 60 | /** 61 | * Device is connected to Streamduck 62 | */ 63 | public event Action? DeviceConnected; 64 | 65 | /** 66 | * Device is disconnected from Streamduck 67 | */ 68 | public event Action? DeviceDisconnected; 69 | 70 | /** 71 | * Device is discovered by a driver 72 | */ 73 | public event Action? DeviceAppeared; 74 | 75 | /** 76 | * Device is no longer available 77 | */ 78 | public event Action? DeviceDisappeared; 79 | 80 | /** 81 | * Initializes Streamduck (eg. load plugins, load auto-connects) 82 | */ 83 | public async Task Init() { 84 | if (_initialized) throw new ApplicationException("App was already initialized"); 85 | 86 | _config = await Config.Get(); 87 | 88 | // Load built-in plugin and external plugins 89 | var nameSet = new HashSet(); 90 | 91 | _l.Info("Scanning for core plugin..."); 92 | var corePlugin = PluginLoader.Load(GetType().Assembly, nameSet)!; 93 | 94 | _l.Info(string.Join(", ", _config.PluginPaths)); 95 | 96 | var foundPlugins = new[] { corePlugin }.AsEnumerable(); 97 | 98 | foundPlugins = _config.PluginPaths.Aggregate( 99 | foundPlugins, 100 | (current, path) => 101 | current.Concat( 102 | PluginLoader.LoadFromFolder(path, nameSet) 103 | ) 104 | ); 105 | 106 | PluginCollection = new PluginCollection( 107 | foundPlugins, 108 | _config 109 | ); 110 | await PluginCollection.LoadAllPluginConfigs(); 111 | 112 | await this.InvokePluginsLoaded(); 113 | 114 | DeviceConnected += async (identifier, core) => await PluginCollection.InvokeDeviceConnected(identifier, core); 115 | DeviceDisconnected += async identifier => await PluginCollection.InvokeDeviceDisconnected(identifier); 116 | DeviceAppeared += async identifier => await PluginCollection.InvokeDeviceAppeared(identifier); 117 | DeviceDisappeared += async identifier => await PluginCollection.InvokeDeviceDisappeared(identifier); 118 | 119 | _initialized = true; 120 | CurrentInstance = this; 121 | } 122 | 123 | 124 | /** 125 | * Runs the Streamduck software 126 | */ 127 | public Task Run(CancellationTokenSource cts) { 128 | if (!_initialized) throw new ApplicationException("Init method was not called"); 129 | 130 | _running = true; 131 | cts.Token.Register(() => _running = false); 132 | 133 | return Task.WhenAll(DeviceDiscoverTask(cts), TickTask(cts)); 134 | } 135 | 136 | public async Task ConnectDevice(NamespacedDeviceIdentifier deviceIdentifier) { 137 | try { 138 | if (ConnectedDeviceList.ContainsKey(deviceIdentifier)) 139 | throw new ApplicationException("Device is already connected"); 140 | 141 | var driver = PluginCollection!.SpecificDriver(deviceIdentifier.NamespacedName); 142 | 143 | if (driver == null) { 144 | _l.Error("Driver '{}' wasn't found", deviceIdentifier.NamespacedName); 145 | return false; 146 | } 147 | 148 | var device = await driver.ConnectDevice(deviceIdentifier); 149 | device.Died += () => DeviceDisconnected?.Invoke(deviceIdentifier); 150 | var core = new CoreImpl(device, deviceIdentifier, PluginCollection!); 151 | 152 | lock (_discoveredDevices) { 153 | _discoveredDevices.Remove(deviceIdentifier); 154 | } 155 | 156 | if (!ConnectedDeviceList.TryAdd(deviceIdentifier, core)) 157 | throw new ApplicationException("Couldn't add device, another connection was already made?"); 158 | 159 | DeviceConnected?.Invoke(deviceIdentifier, core); 160 | 161 | if (await DeviceConfig.LoadConfig(core.DeviceIdentifier) is { } config) 162 | await core.LoadConfigIntoCore(config, PluginCollection); 163 | 164 | return true; 165 | } catch (Exception e) { 166 | _l.Error(e, "Failed to connect to device"); 167 | } 168 | 169 | return false; 170 | } 171 | 172 | private async Task DeviceDiscoverTask(CancellationTokenSource cts) { 173 | await RefreshDevices(); 174 | while (_running) { 175 | await Task.Delay(TimeSpan.FromSeconds(_config!.DeviceCheckDelay), cts.Token); 176 | await RefreshDevices(); 177 | } 178 | } 179 | 180 | public async Task RefreshDevices() { 181 | _l.Debug("Cleaning up dead devices..."); 182 | foreach (var (identifier, _) in ConnectedDeviceList 183 | .Where(k => !k.Value.IsAlive())) { 184 | ConnectedDeviceList.TryRemove(identifier, out var core); 185 | core?.Dispose(); 186 | } 187 | 188 | _l.Debug("Checking all drivers for devices..."); 189 | 190 | var _newDeviceList = new List(); 191 | 192 | foreach (var driver in PluginCollection!.AllDrivers()) 193 | _newDeviceList.AddRange( 194 | (await driver.ListDevices()) 195 | .Where(device => !ConnectedDeviceList.ContainsKey(device)) 196 | ); 197 | 198 | lock (_discoveredDevices) { 199 | var newDevices = _newDeviceList 200 | .Where(device => !_discoveredDevices.Contains(device)); 201 | 202 | var removedDevices = _discoveredDevices 203 | .Where(device => !_newDeviceList.Contains(device)); 204 | 205 | foreach (var device in newDevices) DeviceAppeared?.Invoke(device); 206 | 207 | foreach (var device in removedDevices) DeviceDisappeared?.Invoke(device); 208 | 209 | _discoveredDevices.Clear(); 210 | _discoveredDevices.AddRange(_newDeviceList); 211 | DeviceListRefreshed?.Invoke(); 212 | } 213 | 214 | // Autoconnect 215 | foreach (var discoveredDevice in _newDeviceList 216 | .Where(discoveredDevice => !ConnectedDeviceList.ContainsKey(discoveredDevice)) 217 | .Where(discoveredDevice => _config.AutoconnectDevices.Contains(discoveredDevice))) { 218 | _l.Info("Autoconnecting to {}", discoveredDevice); 219 | await ConnectDevice(discoveredDevice); 220 | } 221 | } 222 | 223 | private async Task TickTask(CancellationTokenSource cts) { 224 | var stopwatch = Stopwatch.StartNew(); 225 | 226 | var lastTime = stopwatch.Elapsed.TotalSeconds; 227 | var interval = 1.0 / _config.TickRate; 228 | 229 | while (_running) { 230 | foreach (var core in ConnectedDeviceList.Values) 231 | if (core is CoreImpl castedCore) 232 | castedCore.CallTick(); 233 | 234 | var toWait = interval - (stopwatch.Elapsed.TotalSeconds - lastTime); 235 | 236 | if (toWait > 0.0) await Task.Delay(TimeSpan.FromSeconds(toWait)); 237 | 238 | lastTime = stopwatch.Elapsed.TotalSeconds; 239 | } 240 | } 241 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/Actions/TestAction.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using Streamduck.Actions; 8 | using Streamduck.Attributes; 9 | 10 | namespace Streamduck.BaseFunctionality.Actions; 11 | 12 | [AutoAdd] 13 | public class TestAction : PluginAction { 14 | public class Data { 15 | public string? Text { get; set; } 16 | } 17 | 18 | public override string Name => "Test Action"; 19 | public override string? Description => "Runs some random stuff"; 20 | 21 | public override Task Invoke(Data data) { 22 | if (data.Text is { } text) Console.WriteLine($"Button is '{text}'"); 23 | return Task.CompletedTask; 24 | } 25 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/CorePlugin.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | using Streamduck.Configuration; 7 | using Streamduck.Constants; 8 | using Streamduck.Cores; 9 | using Streamduck.Devices; 10 | using Streamduck.Plugins; 11 | using Device = Streamduck.BaseFunctionality.SocketRequests.Device; 12 | 13 | namespace Streamduck.BaseFunctionality; 14 | 15 | public class CorePlugin : Plugin { 16 | public override string Name => "Core"; 17 | 18 | public override async Task OnDeviceConnected(NamespacedDeviceIdentifier identifier, Core deviceCore) => 19 | SendEventToSocket( 20 | EventNames.DeviceConnected, new Device { 21 | Identifier = identifier, 22 | Connected = true, 23 | Autoconnect = (await Config.Get()).AutoconnectDevices.Contains(identifier) 24 | } 25 | ); 26 | 27 | public override Task OnDeviceDisconnected(NamespacedDeviceIdentifier identifier) { 28 | SendEventToSocket(EventNames.DeviceDisconnected, identifier); 29 | return Task.CompletedTask; 30 | } 31 | 32 | public override async Task OnDeviceAppeared(NamespacedDeviceIdentifier identifier) => 33 | SendEventToSocket( 34 | EventNames.DeviceAppeared, new Device { 35 | Identifier = identifier, 36 | Connected = false, 37 | Autoconnect = (await Config.Get()).AutoconnectDevices.Contains(identifier) 38 | } 39 | ); 40 | 41 | public override Task OnDeviceDisappeared(NamespacedDeviceIdentifier identifier) { 42 | SendEventToSocket(EventNames.DeviceDisappeared, identifier); 43 | return Task.CompletedTask; 44 | } 45 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/Renderers/DefaultRenderer.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using SixLabors.ImageSharp; 7 | using Streamduck.Attributes; 8 | using Streamduck.Cores; 9 | using Streamduck.Rendering; 10 | 11 | namespace Streamduck.BaseFunctionality.Renderers; 12 | 13 | [AutoAdd] 14 | public class DefaultRenderer : Renderer { 15 | public override string Name => "Default Renderer"; 16 | 17 | public override long Hash(ScreenItem input, Settings renderConfig) => throw new NotImplementedException(); 18 | 19 | public override Image Render(ScreenItem input, Settings renderConfig, bool forPreview) => throw new NotImplementedException(); 20 | 21 | public class Settings { } 22 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/CoreVersion.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | using Streamduck.Attributes; 7 | using Streamduck.Socket; 8 | 9 | namespace Streamduck.BaseFunctionality.SocketRequests; 10 | 11 | [AutoAdd] 12 | public class CoreVersion : SocketRequest { 13 | public const string VERSION = "0.1"; 14 | public override string Name => "Socket Version"; 15 | 16 | public override Task Received(SocketRequester request) { 17 | request.SendBack(VERSION); 18 | return Task.CompletedTask; 19 | } 20 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/Device.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using Streamduck.Devices; 6 | 7 | namespace Streamduck.BaseFunctionality.SocketRequests; 8 | 9 | public class Device { 10 | public NamespacedDeviceIdentifier Identifier { get; set; } 11 | public bool Connected { get; set; } 12 | public bool Autoconnect { get; set; } 13 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/DeviceList/ListDevices.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using Streamduck.Attributes; 9 | using Streamduck.Configuration; 10 | using Streamduck.Socket; 11 | 12 | namespace Streamduck.BaseFunctionality.SocketRequests.DeviceList; 13 | 14 | [AutoAdd] 15 | public class ListDevices : SocketRequest { 16 | public override string Name => "List Devices"; 17 | 18 | public override async Task Received(SocketRequester request) { 19 | var config = await Config.Get(); 20 | 21 | request.SendBack(Devices()); 22 | return; 23 | 24 | IEnumerable Devices() { 25 | foreach (var connected in App.CurrentInstance!.ConnectedDeviceList.Keys) 26 | yield return new Device { 27 | Identifier = connected, 28 | Connected = true, 29 | Autoconnect = config!.AutoconnectDevices.Contains(connected) 30 | }; 31 | 32 | foreach (var discovered in App.CurrentInstance.DiscoveredDevices) 33 | yield return new Device { 34 | Identifier = discovered, 35 | Connected = false, 36 | Autoconnect = config!.AutoconnectDevices.Contains(discovered) 37 | }; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/DeviceList/SetDeviceAutoconnect.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | using Streamduck.Attributes; 7 | using Streamduck.Configuration; 8 | using Streamduck.Devices; 9 | using Streamduck.Socket; 10 | 11 | namespace Streamduck.BaseFunctionality.SocketRequests.DeviceList; 12 | 13 | [AutoAdd] 14 | public class SetDeviceAutoconnect : SocketRequest { 15 | public class Request { 16 | public NamespacedDeviceIdentifier Identifier { get; set; } 17 | public bool Autoconnect { get; set; } 18 | } 19 | 20 | public override string Name => "Set Device Autoconnect"; 21 | 22 | public override async Task Received(SocketRequester request, Request data) { 23 | var config = await Config.Get(); 24 | 25 | if (data.Autoconnect) 26 | config.AutoconnectDevices.Add(data.Identifier); 27 | else 28 | config.AutoconnectDevices.Remove(data.Identifier); 29 | 30 | await config.SaveConfig(); 31 | await App.CurrentInstance!.RefreshDevices(); 32 | 33 | request.SendBack(null); 34 | } 35 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/Devices/ConnectDevice.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | using Streamduck.Attributes; 7 | using Streamduck.Devices; 8 | using Streamduck.Socket; 9 | 10 | namespace Streamduck.BaseFunctionality.SocketRequests.Devices; 11 | 12 | [AutoAdd] 13 | public class ConnectDevice : SocketRequest { 14 | public class Request { 15 | public required NamespacedDeviceIdentifier Identifier { get; set; } 16 | } 17 | 18 | public override string Name => "Connect Device"; 19 | 20 | public override async Task Received(SocketRequester request, Request data) => 21 | request.SendBack(await App.CurrentInstance!.ConnectDevice(data.Identifier)); 22 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/Devices/GetDeviceInputs.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Linq; 6 | using System.Text.Json.Serialization; 7 | using System.Threading.Tasks; 8 | using Streamduck.Attributes; 9 | using Streamduck.Devices; 10 | using Streamduck.Inputs; 11 | using Streamduck.Socket; 12 | 13 | namespace Streamduck.BaseFunctionality.SocketRequests.Devices; 14 | 15 | [AutoAdd] 16 | public class GetDeviceInputs : SocketRequest { 17 | public class Request { 18 | public required NamespacedDeviceIdentifier Identifier { get; set; } 19 | } 20 | 21 | public class InputData(Input input) { 22 | public int X { get; set; } = input.X; 23 | public int Y { get; set; } = input.Y; 24 | public uint W { get; set; } = input.W; 25 | public uint H { get; set; } = input.H; 26 | [JsonConverter(typeof(JsonStringEnumConverter))] 27 | public InputIcon Icon { get; set; } = input.Icon; 28 | } 29 | 30 | public override string Name => "Get Device Inputs"; 31 | 32 | public override Task Received(SocketRequester request, Request data) { 33 | if (!SRUtil.GetDevice(request, data.Identifier, out var device)) return Task.CompletedTask; 34 | 35 | request.SendBack(device.Inputs.Select(i => new InputData(i))); 36 | return Task.CompletedTask; 37 | } 38 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/Devices/GetDeviceItems.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Streamduck.Attributes; 4 | using Streamduck.Cores; 5 | using Streamduck.Devices; 6 | using Streamduck.Socket; 7 | 8 | namespace Streamduck.BaseFunctionality.SocketRequests.Devices; 9 | 10 | [AutoAdd] 11 | public class GetDeviceItems : SocketRequest { 12 | public class Request { 13 | public required NamespacedDeviceIdentifier Identifier { get; set; } 14 | public bool GetPreviews { get; set; } = false; 15 | } 16 | 17 | public class PartialScreenItemData(ScreenItem item) { 18 | public bool Renderable { get; set; } = item is ScreenItem.IRenderable; 19 | public string? Base64JPG { get; set; } 20 | } 21 | 22 | public override string Name => "Get Device Items"; 23 | 24 | public override Task Received(SocketRequester request, Request data) { 25 | if (!SRUtil.GetDevice(request, data.Identifier, out var device)) return Task.CompletedTask; 26 | 27 | request.SendBack( 28 | device.CurrentScreen?.Items.Select( 29 | i => i is not null 30 | ? new PartialScreenItemData(i) 31 | : null 32 | ) 33 | ); 34 | return Task.CompletedTask; 35 | } 36 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/Devices/GetDeviceScreenStack.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Streamduck.Attributes; 4 | using Streamduck.Devices; 5 | using Streamduck.Socket; 6 | 7 | namespace Streamduck.BaseFunctionality.SocketRequests.Devices; 8 | 9 | [AutoAdd] 10 | public class GetDeviceScreenStack : SocketRequest { 11 | public class Request { 12 | public required NamespacedDeviceIdentifier Identifier { get; set; } 13 | } 14 | 15 | public override string Name => "Get Device Screen Stack"; 16 | public override Task Received(SocketRequester request, Request data) { 17 | if (!SRUtil.GetDevice(request, data.Identifier, out var device)) return Task.CompletedTask; 18 | 19 | request.SendBack(device.ScreenStack.Select(s => s.Name).Reverse()); 20 | return Task.CompletedTask; 21 | } 22 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/Devices/PopScreen.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Streamduck.Attributes; 3 | using Streamduck.Devices; 4 | using Streamduck.Socket; 5 | 6 | namespace Streamduck.BaseFunctionality.SocketRequests.Devices; 7 | 8 | [AutoAdd] 9 | public class PopScreen : SocketRequest { 10 | public class Request { 11 | public required NamespacedDeviceIdentifier Identifier { get; set; } 12 | } 13 | 14 | 15 | public override string Name => "Pop Screen"; 16 | public override Task Received(SocketRequester request, Request data) { 17 | if (!SRUtil.GetDevice(request, data.Identifier, out var device)) return Task.CompletedTask; 18 | 19 | request.SendBack(device.PopScreen() is not null); 20 | return Task.CompletedTask; 21 | } 22 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/Devices/PushNewEmptyScreen.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Streamduck.Attributes; 3 | using Streamduck.Cores; 4 | using Streamduck.Devices; 5 | using Streamduck.Socket; 6 | 7 | namespace Streamduck.BaseFunctionality.SocketRequests.Devices; 8 | 9 | [AutoAdd] 10 | public class PushNewEmptyScreen : SocketRequest { 11 | public class Request { 12 | public required NamespacedDeviceIdentifier Identifier { get; set; } 13 | } 14 | 15 | public override string Name => "Push New Empty Screen"; 16 | public override Task Received(SocketRequester request, Request data) { 17 | if (!SRUtil.GetDevice(request, data.Identifier, out var device)) return Task.CompletedTask; 18 | 19 | device.PushScreen(device.NewScreen()); 20 | request.SendBack(null); 21 | return Task.CompletedTask; 22 | } 23 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/SocketRequests/SRUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Streamduck.Cores; 3 | using Streamduck.Devices; 4 | using Streamduck.Socket; 5 | 6 | namespace Streamduck.BaseFunctionality.SocketRequests; 7 | 8 | public static class SRUtil { 9 | public static bool GetDevice(SocketRequester request, NamespacedDeviceIdentifier identifier, out Core device) { 10 | if (App.CurrentInstance!.ConnectedDevices.TryGetValue(identifier, out device!)) return true; 11 | request.SendBackError("Device is not connected or doesn't exist"); 12 | return false; 13 | } 14 | } -------------------------------------------------------------------------------- /Streamduck/BaseFunctionality/Triggers/ButtonDownTrigger.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using Streamduck.Attributes; 8 | using Streamduck.Data; 9 | using Streamduck.Inputs; 10 | using Streamduck.Triggers; 11 | 12 | namespace Streamduck.BaseFunctionality.Triggers; 13 | 14 | [AutoAdd] 15 | public class ButtonDownTrigger : Trigger { 16 | public override string Name => "Button Down Trigger"; 17 | public override string? Description => "Triggers action when button is pressed down"; 18 | 19 | public override bool IsApplicableTo(Input input) => input is IInputButton or IInputTouchScreen or IInputToggle; 20 | 21 | public override Task CreateInstance() => 22 | Task.FromResult((TriggerInstance)new ButtonDownTriggerInstance(this)); 23 | } 24 | 25 | public class ButtonDownTriggerInstance(Trigger original) : TriggerInstance(original) { 26 | public override void Attach(Input input) { 27 | switch (input) { 28 | case IInputButton button: 29 | button.ButtonPressed += InvokeActions; 30 | break; 31 | case IInputToggle toggle: 32 | toggle.ToggleStateChanged += Toggle; 33 | break; 34 | case IInputTouchScreen touchScreen: 35 | touchScreen.TouchScreenPressed += TouchScreen; 36 | break; 37 | } 38 | } 39 | 40 | private void TouchScreen(Int2 _) { 41 | InvokeActions(); 42 | } 43 | 44 | private void Toggle(bool down) { 45 | if (down) InvokeActions(); 46 | } 47 | 48 | public override void Detach(Input input) { 49 | switch (input) { 50 | case IInputButton button: 51 | button.ButtonPressed -= InvokeActions; 52 | break; 53 | case IInputToggle toggle: 54 | toggle.ToggleStateChanged -= Toggle; 55 | break; 56 | case IInputTouchScreen touchScreen: 57 | touchScreen.TouchScreenPressed -= TouchScreen; 58 | break; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Streamduck/Configuration/Config.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Text.Json; 9 | using System.Text.Json.Serialization; 10 | using System.Threading.Tasks; 11 | using NLog; 12 | using Streamduck.Devices; 13 | using Streamduck.Plugins; 14 | 15 | namespace Streamduck.Configuration; 16 | 17 | /** 18 | * Configuration for Streamduck 19 | */ 20 | public class Config { 21 | public const string StreamduckFolderName = "streamduck"; 22 | public const string ConfigFileName = "config.json"; 23 | 24 | public static readonly string StreamduckFolder = Path.Join( 25 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 26 | StreamduckFolderName 27 | ); 28 | 29 | private static readonly Logger L = LogManager.GetCurrentClassLogger(); 30 | 31 | private static Config? _configInstance; 32 | 33 | /** 34 | * How often to tick all the button scripts, in hz 35 | */ 36 | public double TickRate { get; set; } = 60.0; 37 | 38 | /** 39 | * How often to attempt to render every button script, in frames/second 40 | */ 41 | public double FrameRate { get; set; } = 60.0; 42 | 43 | /** 44 | * How long to wait between checking for new devices from all loaded drivers 45 | */ 46 | public double DeviceCheckDelay { get; set; } = 30.0; 47 | 48 | /** 49 | * Let the websocket connection listen for 0.0.0.0 (open to internet) 50 | */ 51 | public bool OpenToInternet { get; set; } 52 | 53 | /** 54 | * Websocket port to use, defaults to 57234 55 | */ 56 | public int WebSocketPort { get; set; } = 42131; 57 | 58 | /** 59 | * List of paths Streamduck will check for plugins 60 | */ 61 | public List PluginPaths { get; set; } = ["plugins"]; 62 | 63 | /** 64 | * Devices that should be automatically connected to 65 | */ 66 | [JsonInclude] 67 | public HashSet AutoconnectDevices { get; private set; } = []; 68 | 69 | public static NamespacedName DefaultRendererName { get; } 70 | = new("Streamduck Core Plugin", "Default Renderer"); 71 | 72 | /** 73 | * Default renderer to be used for new screen items 74 | */ 75 | public NamespacedName DefaultRenderer { get; set; } = DefaultRendererName; 76 | 77 | public async Task AddDeviceToAutoconnect(NamespacedDeviceIdentifier deviceIdentifier) { 78 | lock (AutoconnectDevices) { 79 | AutoconnectDevices.Add(deviceIdentifier); 80 | } 81 | 82 | L.Info("Added {} to autoconnect", deviceIdentifier.DeviceIdentifier); 83 | 84 | await SaveConfig(); 85 | } 86 | 87 | public async Task RemoveDeviceFromAutoconnect(NamespacedDeviceIdentifier deviceIdentifier) { 88 | lock (AutoconnectDevices) { 89 | AutoconnectDevices.Remove(deviceIdentifier); 90 | } 91 | 92 | L.Info("Removed {} from autoconnect", deviceIdentifier.DeviceIdentifier); 93 | 94 | await SaveConfig(); 95 | } 96 | 97 | private static async Task _loadConfig() { 98 | var path = Path.Join( 99 | StreamduckFolder, 100 | ConfigFileName 101 | ); 102 | 103 | L.Info("Loading config..."); 104 | 105 | if (File.Exists(path)) { 106 | var content = await File.ReadAllBytesAsync(path); 107 | 108 | L.Debug("Trying to read existing config..."); 109 | 110 | try { 111 | using var memoryStream = new MemoryStream(content); 112 | 113 | var deserializedConfig = await JsonSerializer.DeserializeAsync( 114 | memoryStream 115 | ); 116 | 117 | if (deserializedConfig != null) return deserializedConfig; 118 | } catch (Exception e) { 119 | L.Error("Error happened while trying to load config {0}", e); 120 | // TODO: Backup invalid config 121 | } 122 | } 123 | 124 | L.Debug("No config found, creating new one..."); 125 | var config = new Config(); 126 | 127 | await config.SaveConfig(); 128 | 129 | return config; 130 | } 131 | 132 | /** 133 | * Saves config to json file in app data 134 | */ 135 | public async Task SaveConfig() { 136 | try { 137 | Directory.CreateDirectory(StreamduckFolder); 138 | } catch (Exception e) { 139 | L.Error("Error happened while trying to create folders for config {0}", e); 140 | return; 141 | } 142 | 143 | var path = Path.Join( 144 | StreamduckFolder, 145 | ConfigFileName 146 | ); 147 | 148 | try { 149 | using var buffer = new MemoryStream(); 150 | 151 | await JsonSerializer.SerializeAsync( 152 | buffer, 153 | this 154 | ); 155 | 156 | Console.WriteLine(path); 157 | 158 | L.Info("Saving app config..."); 159 | await File.WriteAllBytesAsync(path, buffer.ToArray()); 160 | L.Info("Saved app config"); 161 | } catch (Exception e) { 162 | L.Error("Error happened while trying to save config {0}", e); 163 | } 164 | } 165 | 166 | /** 167 | * If config wasn't loaded yet, loads config from json file. 168 | * If file doesn't exist, creates a default AppConfig and saves it. 169 | * If config is already loaded, provides that config instance 170 | */ 171 | public static async Task Get() { 172 | _configInstance ??= await _loadConfig(); 173 | return _configInstance; 174 | } 175 | 176 | public static Config? IgnorantGet() => _configInstance; 177 | } -------------------------------------------------------------------------------- /Streamduck/Configuration/Devices/DeviceConfig.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.IO; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using System.Threading.Tasks; 10 | using NLog; 11 | using Streamduck.Devices; 12 | 13 | namespace Streamduck.Configuration.Devices; 14 | 15 | public class DeviceConfig { 16 | private static readonly Logger _l = LogManager.GetCurrentClassLogger(); 17 | 18 | public NamespacedDeviceIdentifier Device { get; set; } 19 | public SerializedScreen[] ScreenStack { get; set; } = []; 20 | 21 | public static readonly string DeviceConfigFolder = Path.Join( 22 | Config.StreamduckFolder, 23 | "devices" 24 | ); 25 | 26 | private static string DeviceFolderPath(NamespacedDeviceIdentifier deviceIdentifier) => 27 | Path.Join( 28 | DeviceConfigFolder, 29 | deviceIdentifier.PluginName, 30 | deviceIdentifier.DriverName, 31 | deviceIdentifier.Description 32 | ); 33 | 34 | private static string DeviceFilePath(NamespacedDeviceIdentifier deviceIdentifier) => 35 | Path.Join( 36 | DeviceFolderPath(deviceIdentifier), 37 | $"{deviceIdentifier.Identifier}.json" 38 | ); 39 | 40 | private static string DeviceFilePath(NamespacedDeviceIdentifier deviceIdentifier, string folderPath) => 41 | Path.Join( 42 | folderPath, 43 | $"{deviceIdentifier.Identifier}.json" 44 | ); 45 | 46 | public async Task SaveConfig() { 47 | var folder = DeviceFolderPath(Device); 48 | 49 | try { 50 | Directory.CreateDirectory(folder); 51 | } catch (Exception e) { 52 | _l.Error("Error happened while trying to create folders for config {0}", e); 53 | return; 54 | } 55 | 56 | var jsonData = JsonSerializer.Serialize(this); 57 | var filePath = DeviceFilePath(Device, folder); 58 | 59 | await File.WriteAllTextAsync(filePath, jsonData); 60 | } 61 | 62 | private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { 63 | UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow 64 | }; 65 | 66 | public static async Task LoadConfig(NamespacedDeviceIdentifier deviceIdentifier) { 67 | var filePath = DeviceFilePath(deviceIdentifier); 68 | 69 | if (!File.Exists(filePath)) { 70 | _l.Error($"Failed to locate device config at '{filePath}'"); 71 | return null; 72 | } 73 | 74 | var stringData = await File.ReadAllTextAsync(filePath); 75 | 76 | try { 77 | var data = JsonSerializer.Deserialize(stringData, _jsonSerializerOptions); 78 | return data; 79 | } catch (JsonException e) { 80 | _l.Error($"Failed to load device config at '{filePath}':\n{e.Message}"); 81 | return null; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Streamduck/Configuration/Devices/SerializedAction.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Text.Json; 6 | using Streamduck.Plugins; 7 | 8 | namespace Streamduck.Configuration.Devices; 9 | 10 | public class SerializedAction { 11 | public NamespacedName Action { get; set; } 12 | public JsonElement? Data { get; set; } 13 | } -------------------------------------------------------------------------------- /Streamduck/Configuration/Devices/SerializedScreen.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Configuration.Devices; 6 | 7 | public class SerializedScreen { 8 | public string Name { get; set; } = "Screen"; 9 | public SerializedScreenItem?[] Items { get; set; } = []; 10 | public bool CanWrite { get; set; } 11 | } -------------------------------------------------------------------------------- /Streamduck/Configuration/Devices/SerializedScreenItem.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Text.Json; 6 | using Streamduck.Plugins; 7 | 8 | namespace Streamduck.Configuration.Devices; 9 | 10 | public class SerializedScreenItem { 11 | public SerializedTrigger[] Triggers { get; set; } = []; 12 | public NamespacedName? RendererName { get; set; } 13 | public JsonElement? RendererSettings { get; set; } 14 | } -------------------------------------------------------------------------------- /Streamduck/Configuration/Devices/SerializedTrigger.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Text.Json; 6 | using Streamduck.Plugins; 7 | 8 | namespace Streamduck.Configuration.Devices; 9 | 10 | public class SerializedTrigger { 11 | public NamespacedName Trigger { get; set; } 12 | public SerializedAction[] Actions { get; set; } = []; 13 | public JsonElement? Data { get; set; } 14 | } -------------------------------------------------------------------------------- /Streamduck/Configuration/GlobalConfig.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Text.Json; 11 | using System.Threading.Tasks; 12 | using NLog; 13 | using Streamduck.Interfaces; 14 | using Streamduck.Plugins; 15 | 16 | namespace Streamduck.Configuration; 17 | 18 | public static class GlobalConfig { 19 | private const BindingFlags StaticNonPublic = BindingFlags.Static | BindingFlags.NonPublic; 20 | private static readonly Logger L = LogManager.GetCurrentClassLogger(); 21 | 22 | public static readonly string GlobalConfigFolder = Path.Join( 23 | Config.StreamduckFolder, 24 | "global" 25 | ); 26 | 27 | private static readonly MethodInfo LoadGenericIConfigurableMethod = 28 | typeof(GlobalConfig).GetMethod(nameof(LoadGenericIConfigurable), StaticNonPublic)!; 29 | 30 | private static readonly MethodInfo SaveGenericIConfigurableMethod = 31 | typeof(GlobalConfig).GetMethod(nameof(SaveGenericIConfigurable), StaticNonPublic)!; 32 | 33 | private static string PluginFolderPath(WrappedPlugin plugin) => 34 | Path.Join( 35 | GlobalConfigFolder, 36 | plugin.Name 37 | ); 38 | 39 | private static string PluginFilePath(string pluginFolderPath) => 40 | Path.Join( 41 | pluginFolderPath, 42 | "config.json" 43 | ); 44 | 45 | public static async Task LoadPlugin(WrappedPlugin plugin) { 46 | var pluginFolderPath = PluginFolderPath(plugin); 47 | 48 | // Load plugin config if exists 49 | var pluginConfigurableType = plugin.Instance.GetType().GetInterfaces().FirstOrDefault( 50 | x => 51 | x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IConfigurable<>) 52 | ); 53 | 54 | if (pluginConfigurableType is not null) { 55 | var pluginFilePath = PluginFilePath(pluginFolderPath); 56 | 57 | await (Task)LoadGenericIConfigurableMethod.MakeGenericMethod(pluginConfigurableType.GenericTypeArguments[0]) 58 | .Invoke(null, [plugin.Instance, pluginFilePath])!; 59 | } 60 | 61 | await LoadEnumerable( 62 | plugin.Actions.Select(x => x.Instance), 63 | pluginFolderPath, 64 | "actions" 65 | ); 66 | 67 | await LoadEnumerable( 68 | plugin.Drivers.Select(x => x.Instance), 69 | pluginFolderPath, 70 | "drivers" 71 | ); 72 | 73 | await LoadEnumerable( 74 | plugin.Renderers.Select(x => x.Instance), 75 | pluginFolderPath, 76 | "renderers" 77 | ); 78 | 79 | await LoadEnumerable( 80 | plugin.Triggers.Select(x => x.Instance), 81 | pluginFolderPath, 82 | "triggers" 83 | ); 84 | } 85 | 86 | private static async Task LoadEnumerable(IEnumerable iter, string basePath, string name) where T : INamed { 87 | var collectionPath = Path.Join( 88 | basePath, 89 | name 90 | ); 91 | 92 | foreach (var item in iter) { 93 | var configurableType = item.GetType().GetInterfaces().FirstOrDefault( 94 | x => 95 | x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IConfigurable<>) 96 | ); 97 | 98 | if (configurableType is null) continue; // Only allow items that have generic IConfigurable 99 | 100 | var filePath = Path.Join(collectionPath, $"{item.Name}.json"); 101 | 102 | await (Task)LoadGenericIConfigurableMethod.MakeGenericMethod(configurableType.GenericTypeArguments[0]) 103 | .Invoke(null, [item, filePath])!; 104 | } 105 | } 106 | 107 | private static async Task LoadGenericIConfigurable(IConfigurable obj, string filePath) 108 | where T : class, new() { 109 | if (!File.Exists(filePath)) return; 110 | 111 | try { 112 | var data = await File.ReadAllBytesAsync(filePath); 113 | using var buffer = new MemoryStream(data); 114 | var config = await JsonSerializer.DeserializeAsync(buffer); 115 | 116 | if (config is null) { 117 | L.Error($"Couldn't properly cast data from '{filePath}' to '{typeof(T).FullName}'"); 118 | return; 119 | } 120 | 121 | obj.Config = config; 122 | } catch (Exception e) { 123 | L.Error($"Failed to load config at '{filePath}': {e}"); 124 | } 125 | } 126 | 127 | public static async Task SavePlugin(WrappedPlugin plugin) { 128 | var pluginFolderPath = PluginFolderPath(plugin); 129 | 130 | // Save plugin config if exists 131 | var pluginConfigurableType = plugin.Instance.GetType().GetInterfaces().FirstOrDefault( 132 | x => 133 | x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IConfigurable<>) 134 | ); 135 | 136 | if (pluginConfigurableType is not null) { 137 | var pluginFilePath = PluginFilePath(pluginFolderPath); 138 | 139 | try { 140 | Directory.CreateDirectory(pluginFolderPath); 141 | } catch (Exception e) { 142 | L.Error("Error happened while trying to create folders for config {0}", e); 143 | return; 144 | } 145 | 146 | await (Task)SaveGenericIConfigurableMethod.MakeGenericMethod(pluginConfigurableType.GenericTypeArguments[0]) 147 | .Invoke(null, [plugin.Instance, pluginFilePath])!; 148 | } 149 | 150 | // Save its derivatives 151 | await SaveEnumerable( 152 | plugin.Actions.Select(x => x.Instance), 153 | pluginFolderPath, 154 | "actions" 155 | ); 156 | 157 | await SaveEnumerable( 158 | plugin.Drivers.Select(x => x.Instance), 159 | pluginFolderPath, 160 | "drivers" 161 | ); 162 | 163 | await SaveEnumerable( 164 | plugin.Renderers.Select(x => x.Instance), 165 | pluginFolderPath, 166 | "renderers" 167 | ); 168 | 169 | await SaveEnumerable( 170 | plugin.Triggers.Select(x => x.Instance), 171 | pluginFolderPath, 172 | "triggers" 173 | ); 174 | } 175 | 176 | private static async Task SaveEnumerable(IEnumerable iter, string basePath, string name) where T : INamed { 177 | var collectionPath = Path.Join( 178 | basePath, 179 | name 180 | ); 181 | 182 | var folderCreated = false; 183 | 184 | foreach (var item in iter) { 185 | var configurableType = item.GetType().GetInterfaces().FirstOrDefault( 186 | x => 187 | x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IConfigurable<>) 188 | ); 189 | 190 | if (configurableType is null) continue; // Only allow items that have generic IConfigurable 191 | 192 | if (!folderCreated) { 193 | try { 194 | Directory.CreateDirectory(collectionPath); 195 | } catch (Exception e) { 196 | L.Error("Error happened while trying to create folders for config {0}", e); 197 | return; 198 | } 199 | 200 | folderCreated = true; 201 | } 202 | 203 | var filePath = Path.Join(collectionPath, $"{item.Name}.json"); 204 | 205 | await (Task)SaveGenericIConfigurableMethod.MakeGenericMethod(configurableType.GenericTypeArguments[0]) 206 | .Invoke(null, [item, filePath])!; 207 | } 208 | } 209 | 210 | private static async Task SaveGenericIConfigurable(IConfigurable obj, string filePath) 211 | where T : class, new() { 212 | using var buffer = new MemoryStream(); 213 | 214 | await JsonSerializer.SerializeAsync( 215 | buffer, 216 | obj.Config 217 | ); 218 | 219 | await File.WriteAllBytesAsync(filePath, buffer.ToArray()); 220 | } 221 | } -------------------------------------------------------------------------------- /Streamduck/Constants/EventNames.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Constants; 6 | 7 | public static class EventNames { 8 | public const string DeviceAppeared = "Device Appeared"; 9 | public const string DeviceDisappeared = "Device Disappeared"; 10 | public const string DeviceConnected = "Device Connected"; 11 | public const string DeviceDisconnected = "Device Disconnected"; 12 | } -------------------------------------------------------------------------------- /Streamduck/Cores/CoreImpl.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using NLog; 9 | using Streamduck.Devices; 10 | using Streamduck.Plugins; 11 | 12 | namespace Streamduck.Cores; 13 | 14 | public sealed class CoreImpl : Core { 15 | private static readonly Logger _l = LogManager.GetCurrentClassLogger(); 16 | 17 | private readonly IPluginQuery _pluginQuery; 18 | 19 | internal readonly Stack _screenStack; 20 | 21 | public CoreImpl(Device device, NamespacedDeviceIdentifier deviceIdentifier, IPluginQuery pluginQuery) : 22 | base(device) { 23 | DeviceIdentifier = deviceIdentifier; 24 | _pluginQuery = pluginQuery; 25 | _screenStack = new Stack(); 26 | 27 | PushScreen(NewScreen("Root")); 28 | PushScreen(NewScreen("Item1")); 29 | PushScreen(NewScreen("Item2")); 30 | PushScreen(NewScreen("Item3")); 31 | PushScreen(NewScreen("Item1")); 32 | PushScreen(NewScreen("Item2")); 33 | PushScreen(NewScreen("Item3")); 34 | PushScreen(NewScreen("Item1")); 35 | PushScreen(NewScreen("Item2")); 36 | PushScreen(NewScreen("Item3")); 37 | PushScreen(NewScreen("Item1")); 38 | PushScreen(NewScreen("Item2")); 39 | PushScreen(NewScreen("Item3")); 40 | PushScreen(NewScreen("Item1")); 41 | PushScreen(NewScreen("Item2")); 42 | PushScreen(NewScreen("Item3")); 43 | PushScreen(NewScreen("Item1")); 44 | PushScreen(NewScreen("Item2")); 45 | PushScreen(NewScreen("Item3")); 46 | PushScreen(NewScreen("Item1")); 47 | PushScreen(NewScreen("Item2")); 48 | PushScreen(NewScreen("Item3")); 49 | PushScreen(NewScreen("Item1")); 50 | PushScreen(NewScreen("Item2")); 51 | PushScreen(NewScreen("Item3")); 52 | 53 | 54 | device.Died += () => _l.Warn("Device {} died", DeviceIdentifier); 55 | } 56 | 57 | public override NamespacedDeviceIdentifier DeviceIdentifier { get; } 58 | 59 | public override Screen? CurrentScreen { 60 | get { 61 | lock (_screenStack) { 62 | if (_screenStack.TryPeek(out var screen)) return screen; 63 | } 64 | 65 | return null; 66 | } 67 | } 68 | 69 | internal ScreenImpl? CurrentScreenImpl => CurrentScreen as ScreenImpl; 70 | 71 | public override IEnumerable ScreenStack { 72 | get { 73 | lock (_screenStack) { 74 | return _screenStack; 75 | } 76 | } 77 | } 78 | 79 | public override Screen NewScreen(string name = "Screen", bool canWrite = true) => 80 | new ScreenImpl(this, _pluginQuery, _associatedDevice.Inputs) { 81 | Name = name, 82 | CanWrite = canWrite 83 | }; 84 | 85 | public override void PushScreen(Screen screen) { 86 | lock (_screenStack) { 87 | _screenStack.TryPeek(out var oldScreen); 88 | (oldScreen as ScreenImpl)?.DetachFromInputs(); 89 | _screenStack.Push(screen); 90 | (screen as ScreenImpl)?.AttachToInputs(); 91 | } 92 | } 93 | 94 | public override Screen? PopScreen() { 95 | lock (_screenStack) { 96 | if (_screenStack.Count <= 1) return null; 97 | _screenStack.TryPop(out var screen); 98 | (screen as ScreenImpl)?.DetachFromInputs(); 99 | if (_screenStack.TryPeek(out var newScreen)) (newScreen as ScreenImpl)?.AttachToInputs(); 100 | return screen; 101 | } 102 | } 103 | 104 | public override Screen? ReplaceScreen(Screen newScreen) { 105 | lock (_screenStack) { 106 | _screenStack.TryPop(out var screen); 107 | (screen as ScreenImpl)?.DetachFromInputs(); 108 | _screenStack.Push(newScreen); 109 | (newScreen as ScreenImpl)?.AttachToInputs(); 110 | return screen; 111 | } 112 | } 113 | 114 | public override event Action? Tick; 115 | 116 | internal void CallTick() { 117 | Tick?.Invoke(); 118 | } 119 | } -------------------------------------------------------------------------------- /Streamduck/Cores/ScreenImpl.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Streamduck.Cores.ScreenItems; 9 | using Streamduck.Inputs; 10 | using Streamduck.Plugins; 11 | 12 | namespace Streamduck.Cores; 13 | 14 | public class ScreenImpl(Core core, IPluginQuery pluginQuery, IReadOnlyCollection inputs) : Screen { 15 | private readonly Input[] _inputs = inputs.ToArray(); 16 | private readonly ScreenItem?[] _items = new ScreenItem?[inputs.Count]; 17 | 18 | public override Core AssociatedCore => core; 19 | public override IReadOnlyCollection Items => _items; 20 | 21 | 22 | public override ScreenItem CreateItem(int index) { 23 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _items.Length); 24 | if (_items[index] != null) throw new ArgumentException("Item already exists on the index", nameof(index)); 25 | 26 | var input = _inputs[index]; 27 | var item = CreateFromInput(input); 28 | 29 | _items[index] = item; 30 | 31 | if (core.CurrentScreen == this) item.Attach(input); 32 | 33 | return item; 34 | } 35 | 36 | internal void AssignItem(int index, ScreenItem item) { 37 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _items.Length); 38 | _items[index] = item; 39 | if (core.CurrentScreen == this) item.Attach(_inputs[index]); 40 | } 41 | 42 | private ScreenItem CreateFromInput(Input input) { 43 | if (input is not IInputDisplay display) return new ScreenlessItem(); 44 | 45 | var renderer = pluginQuery.DefaultRenderer(); 46 | 47 | if (renderer is null) return new ScreenlessItem(); 48 | 49 | return new RenderableScreenItem { 50 | RendererName = renderer.NamespacedName, 51 | RendererSettings = renderer.Instance.DefaultRendererConfig 52 | }; 53 | } 54 | 55 | public override ScreenItem DeleteItem(int index) { 56 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _inputs.Length); 57 | if (_items[index] == null) throw new ArgumentException("Item doesn't exist on the index", nameof(index)); 58 | 59 | var item = _items[index]!; 60 | 61 | if (core.CurrentScreen == this) item.Detach(); 62 | 63 | _items[index] = null; 64 | return item; 65 | } 66 | 67 | public void AttachToInputs() { 68 | foreach (var (item, input) in _items.Zip(_inputs)) item?.Attach(input); 69 | } 70 | 71 | public void DetachFromInputs() { 72 | foreach (var item in _items) item?.Detach(); 73 | } 74 | } -------------------------------------------------------------------------------- /Streamduck/Cores/ScreenItems/RenderableScreenItem.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Streamduck.Inputs; 7 | using Streamduck.Plugins; 8 | using Streamduck.Triggers; 9 | 10 | namespace Streamduck.Cores.ScreenItems; 11 | 12 | public class RenderableScreenItem : ScreenlessItem, ScreenItem.IRenderable { 13 | public RenderableScreenItem() { } 14 | internal RenderableScreenItem(Input? input, IEnumerable triggers) : base(input, triggers) { } 15 | 16 | public NamespacedName? RendererName { get; set; } 17 | public object? RendererSettings { get; set; } 18 | } -------------------------------------------------------------------------------- /Streamduck/Cores/ScreenItems/ScreenlessItem.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Streamduck.Inputs; 7 | using Streamduck.Triggers; 8 | 9 | namespace Streamduck.Cores.ScreenItems; 10 | 11 | public class ScreenlessItem : ScreenItem { 12 | private readonly List _triggers = []; 13 | 14 | public Input? AssociatedInput; 15 | public ScreenlessItem() { } 16 | 17 | internal ScreenlessItem(Input? input, IEnumerable triggers) { 18 | AssociatedInput = input; 19 | _triggers.AddRange(triggers); 20 | } 21 | 22 | public override IReadOnlyCollection Triggers => _triggers; 23 | 24 | public override void AddTrigger(TriggerInstance trigger, bool attachToInput = true) { 25 | _triggers.Add(trigger); 26 | 27 | if (attachToInput && AssociatedInput is { } input) trigger.Attach(input); 28 | } 29 | 30 | public override bool RemoveTrigger(TriggerInstance trigger) { 31 | if (AssociatedInput is { } input) trigger.Detach(input); 32 | return _triggers.Remove(trigger); 33 | } 34 | 35 | public override void Attach(Input input) { 36 | AssociatedInput = input; 37 | 38 | foreach (var trigger in _triggers) trigger.Attach(input); 39 | } 40 | 41 | public override void Detach() { 42 | if (AssociatedInput is null) return; 43 | 44 | foreach (var trigger in _triggers) trigger.Detach(AssociatedInput); 45 | 46 | AssociatedInput = null; 47 | } 48 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/Extensions/DeviceSerializationExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 10 | using NLog; 11 | using Streamduck.Actions; 12 | using Streamduck.Configuration.Devices; 13 | using Streamduck.Cores; 14 | using Streamduck.Cores.ScreenItems; 15 | using Streamduck.Inputs; 16 | using Streamduck.Rendering; 17 | using Streamduck.Triggers; 18 | 19 | namespace Streamduck.Plugins.Extensions; 20 | 21 | public static class DeviceSerializationExtensions { 22 | private static readonly Logger _l = LogManager.GetCurrentClassLogger(); 23 | private const BindingFlags StaticNonPublic = BindingFlags.Static | BindingFlags.NonPublic; 24 | 25 | public static SerializedAction Serialize(this ActionInstance actionInstance, IPluginQuery pluginQuery) => 26 | new() { 27 | Action = pluginQuery.ActionName(actionInstance.Original), 28 | Data = JsonSerializer.SerializeToElement(actionInstance.Data) 29 | }; 30 | 31 | public static SerializedTrigger Serialize(this TriggerInstance triggerInstance, IPluginQuery pluginQuery) { 32 | return new SerializedTrigger { 33 | Trigger = pluginQuery.TriggerName(triggerInstance.Original), 34 | Actions = triggerInstance.Actions.Select(a => a.Serialize(pluginQuery)).ToArray(), 35 | Data = SerializeTriggerOptions(triggerInstance) 36 | }; 37 | } 38 | 39 | private static JsonElement? SerializeTriggerOptions(TriggerInstance instance) { 40 | var potentialBaseType = instance.GetType().BaseType; 41 | if (potentialBaseType is null) return null; 42 | if (!potentialBaseType.IsGenericType) return null; 43 | if (potentialBaseType.GetGenericTypeDefinition() != typeof(TriggerInstance<>)) return null; 44 | return (JsonElement?)SerializeGenericTriggerMethod.MakeGenericMethod(potentialBaseType.GenericTypeArguments) 45 | .Invoke(null, [instance]); 46 | } 47 | 48 | private static readonly MethodInfo SerializeGenericTriggerMethod = 49 | typeof(DeviceSerializationExtensions).GetMethod(nameof(SerializeGenericTriggerOptions), StaticNonPublic)!; 50 | 51 | private static JsonElement SerializeGenericTriggerOptions(TriggerInstance instance) where T : class, new() => 52 | JsonSerializer.SerializeToElement(instance.Options); 53 | 54 | public static SerializedScreenItem Serialize(this ScreenItem screenItem, IPluginQuery pluginQuery) { 55 | var triggers = screenItem.Triggers.Select(t => t.Serialize(pluginQuery)).ToArray(); 56 | 57 | if (screenItem is ScreenItem.IRenderable renderable) 58 | return new SerializedScreenItem { 59 | Triggers = triggers, 60 | RendererName = renderable.RendererName, 61 | RendererSettings = renderable.RendererSettings is not null 62 | ? JsonSerializer.SerializeToElement(renderable.RendererSettings) 63 | : null 64 | }; 65 | 66 | return new SerializedScreenItem { 67 | Triggers = triggers 68 | }; 69 | } 70 | 71 | public static SerializedScreen Serialize(this Screen screen, IPluginQuery pluginQuery) { 72 | return new SerializedScreen { 73 | Name = screen.Name, 74 | Items = screen.Items.Select(i => i?.Serialize(pluginQuery)).ToArray(), 75 | CanWrite = screen.CanWrite 76 | }; 77 | } 78 | 79 | public static DeviceConfig Serialize(this Core core, IPluginQuery pluginQuery) { 80 | return new DeviceConfig { 81 | Device = core.DeviceIdentifier, 82 | ScreenStack = core.ScreenStack.Select(s => s.Serialize(pluginQuery)).ToArray() 83 | }; 84 | } 85 | 86 | public static ActionInstance? Deserialize(this SerializedAction serializedAction, IPluginQuery pluginQuery) { 87 | if (pluginQuery.SpecificAction(serializedAction.Action) is not { } action) { 88 | _l.Warn($"Failed to load action, '{serializedAction.Action}' no longer exists"); 89 | return null; 90 | } 91 | 92 | var instance = new ActionInstance(action.Instance); 93 | 94 | if (serializedAction.Data is not { } data) return instance; 95 | 96 | var baseType = action.Instance.GetType().BaseType; 97 | if (baseType is null || 98 | !baseType.IsGenericType || 99 | baseType.GetGenericTypeDefinition() != typeof(PluginAction<>)) return instance; 100 | 101 | instance.Data = data.Deserialize(baseType.GenericTypeArguments[0]); 102 | 103 | return instance; 104 | } 105 | 106 | public static async Task Deserialize(this SerializedTrigger serializedTrigger, 107 | IPluginQuery pluginQuery 108 | ) { 109 | if (pluginQuery.SpecificTrigger(serializedTrigger.Trigger) is not { } trigger) { 110 | _l.Warn($"Failed to load trigger, '{serializedTrigger.Trigger}' no longer exists"); 111 | return null; 112 | } 113 | 114 | var instance = await trigger.Instance.CreateInstance(); 115 | 116 | instance.AddActions( 117 | serializedTrigger.Actions.Select(sA => sA.Deserialize(pluginQuery)) 118 | .Where(a => a is not null)! 119 | ); 120 | 121 | var baseType = trigger.Instance.GetType().BaseType; 122 | if (baseType is null || 123 | !baseType.IsGenericType || 124 | baseType.GetGenericTypeDefinition() != typeof(TriggerInstance<>) || 125 | serializedTrigger.Data is not { } data) return instance; 126 | 127 | DeserializeGenericTriggerMethod.MakeGenericMethod(baseType.GenericTypeArguments) 128 | .Invoke(null, [instance, data]); 129 | 130 | return instance; 131 | } 132 | 133 | private static readonly MethodInfo DeserializeGenericTriggerMethod = 134 | typeof(DeviceSerializationExtensions).GetMethod(nameof(DeserializeGenericTriggerOptions), StaticNonPublic)!; 135 | 136 | private static void DeserializeGenericTriggerOptions(TriggerInstance instance, JsonElement data) 137 | where T : class, new() { 138 | if (data.Deserialize() is not { } dataObject) return; 139 | instance.Options = dataObject; 140 | } 141 | 142 | public static async Task Deserialize(this SerializedScreenItem serializedItem, Input input, 143 | IPluginQuery pluginQuery 144 | ) { 145 | var triggers = (await Task.WhenAll( 146 | serializedItem.Triggers.Select(sT => sT.Deserialize(pluginQuery)) 147 | )).Where(t => t is not null).Select(t => t!); 148 | 149 | var instance = input is IInputDisplay 150 | ? new RenderableScreenItem(null, triggers) 151 | : new ScreenlessItem(null, triggers); 152 | 153 | if (instance is not ScreenItem.IRenderable renderable) return instance; 154 | renderable.RendererName = serializedItem.RendererName; 155 | 156 | if (serializedItem is not { RendererSettings: { } data, RendererName: { } name }) return instance; 157 | 158 | if (pluginQuery.SpecificRenderer(name) is not { } renderer) { 159 | _l.Warn($"Failed to load renderer, '{name}' no longer exists"); 160 | return instance; 161 | } 162 | 163 | var baseType = renderer.Instance.GetType().BaseType; 164 | if (baseType is null || 165 | !baseType.IsGenericType || 166 | baseType.GetGenericTypeDefinition() != typeof(Renderer<>)) return instance; 167 | 168 | renderable.RendererSettings = data.Deserialize(baseType.GenericTypeArguments[0]); 169 | 170 | return instance; 171 | } 172 | 173 | public static async Task Deserialize(this SerializedScreen serializedScreen, Core core, 174 | IPluginQuery pluginQuery 175 | ) { 176 | var screen = core.NewScreen(serializedScreen.Name, serializedScreen.CanWrite); 177 | if (screen is not ScreenImpl screenImpl) return null; // This literally will never happen 178 | 179 | foreach (var ((input, item), index) in core.Inputs.Zip(serializedScreen.Items) 180 | .Select((t, i) => (t, i))) { 181 | if (item is null) continue; 182 | if (await item.Deserialize(input, pluginQuery) is not { } screenItem) continue; 183 | screenImpl.AssignItem(index, screenItem); 184 | } 185 | 186 | return screenImpl; 187 | } 188 | 189 | public static async Task LoadConfigIntoCore(this CoreImpl core, DeviceConfig config, IPluginQuery pluginQuery) { 190 | var screens = (await Task.WhenAll(config.ScreenStack.Select(s => s.Deserialize(core, pluginQuery)))) 191 | .Where(s => s is not null).Select(s => s!); 192 | 193 | core.CurrentScreenImpl?.DetachFromInputs(); 194 | 195 | lock (core._screenStack) { 196 | core._screenStack.Clear(); 197 | 198 | foreach (var screen in screens) core._screenStack.Push(screen); 199 | } 200 | 201 | core.CurrentScreenImpl?.AttachToInputs(); 202 | } 203 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/Extensions/NamespacedDriverExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using Streamduck.Data; 9 | using Streamduck.Devices; 10 | 11 | namespace Streamduck.Plugins.Extensions; 12 | 13 | public static class NamespacedDriverExtensions { 14 | public static async Task ConnectDevice(this Namespaced driver, NamespacedDeviceIdentifier name) => 15 | await driver.Instance.ConnectDevice(name.DeviceIdentifier); 16 | 17 | public static async Task> ListDevices(this Namespaced driver) { 18 | return (await driver.Instance.ListDevices()) 19 | .Select(d => new NamespacedDeviceIdentifier(driver.NamespacedName, d)); 20 | } 21 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/Extensions/PluginCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Streamduck.Configuration; 8 | using Streamduck.Cores; 9 | using Streamduck.Devices; 10 | 11 | namespace Streamduck.Plugins.Extensions; 12 | 13 | public static class PluginCollectionExtension { 14 | public static Task InvokeDeviceConnected(this PluginCollection collection, NamespacedDeviceIdentifier identifier, 15 | Core deviceCore 16 | ) { 17 | return Task.WhenAll(collection.AllPlugins().Select(p => p.OnDeviceConnected(identifier, deviceCore))); 18 | } 19 | 20 | public static Task 21 | InvokeDeviceDisconnected(this PluginCollection collection, NamespacedDeviceIdentifier identifier) { 22 | return Task.WhenAll(collection.AllPlugins().Select(p => p.OnDeviceDisconnected(identifier))); 23 | } 24 | 25 | public static Task InvokeDeviceAppeared(this PluginCollection collection, NamespacedDeviceIdentifier identifier) { 26 | return Task.WhenAll(collection.AllPlugins().Select(p => p.OnDeviceAppeared(identifier))); 27 | } 28 | 29 | public static Task 30 | InvokeDeviceDisappeared(this PluginCollection collection, NamespacedDeviceIdentifier identifier) { 31 | return Task.WhenAll(collection.AllPlugins().Select(p => p.OnDeviceDisappeared(identifier))); 32 | } 33 | 34 | public static Task LoadAllPluginConfigs(this PluginCollection collection) => 35 | Task.WhenAll(collection.AllWrappedPlugins().Select(GlobalConfig.LoadPlugin)); 36 | 37 | public static Task SaveAllPluginConfigs(this PluginCollection collection) => 38 | Task.WhenAll(collection.AllWrappedPlugins().Select(GlobalConfig.SavePlugin)); 39 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/Extensions/StreamduckExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Streamduck.Plugins.Extensions; 9 | 10 | public static class StreamduckExtensions { 11 | public static Task InvokePluginsLoaded(this IStreamduck collection) { 12 | return Task.WhenAll(collection.Plugins.AllPlugins().Select(p => p.OnPluginsLoaded(collection))); 13 | } 14 | 15 | public static Task InvokeNewPluginsLoaded(this IStreamduck collection, Plugin[] plugins) { 16 | return Task.WhenAll(collection.Plugins.AllPlugins().Select(p => p.OnNewPluginsLoaded(plugins, collection))); 17 | } 18 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/Loaders/PluginLoadContext.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Reflection; 7 | using System.Runtime.Loader; 8 | 9 | namespace Streamduck.Plugins.Loaders; 10 | 11 | public class PluginLoadContext(string pluginPath) : AssemblyLoadContext(true) { 12 | private readonly AssemblyDependencyResolver _resolver = new(pluginPath); 13 | 14 | protected override Assembly? Load(AssemblyName assemblyName) { 15 | var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); 16 | return assemblyPath == null ? null : LoadFromAssemblyPath(assemblyPath); 17 | } 18 | 19 | protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { 20 | var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); 21 | return libraryPath != null ? LoadUnmanagedDllFromPath(libraryPath) : IntPtr.Zero; 22 | } 23 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/Loaders/PluginLoader.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Runtime.Loader; 11 | using NLog; 12 | 13 | namespace Streamduck.Plugins.Loaders; 14 | 15 | public static class PluginLoader { 16 | private static readonly Logger L = LogManager.GetCurrentClassLogger(); 17 | 18 | public static PluginAssembly? Load(string path, ISet? nameSet = null) { 19 | var curDir = Directory.GetCurrentDirectory(); 20 | var fullPath = Path.Combine(curDir, path); 21 | 22 | var context = new PluginLoadContext(fullPath); 23 | 24 | try { 25 | var plugins = CreatePlugins(context, LoadPlugin(context, fullPath), nameSet); 26 | return new PluginAssembly(context, plugins.ToArray()); 27 | } catch (Exception e) { 28 | L.Error("Failed to load plugin at {0}\nReason: {1}", path, e); 29 | return null; 30 | } 31 | } 32 | 33 | public static PluginAssembly? Load(Assembly assembly, ISet? nameSet = null) { 34 | var context = new PluginLoadContext(assembly.Location); 35 | 36 | try { 37 | var plugins = CreatePlugins(context, assembly, nameSet); 38 | return new PluginAssembly(context, plugins.ToArray()); 39 | } catch (Exception e) { 40 | L.Error("Failed to load plugin at {0}\nReason: {1}", assembly.Location, e); 41 | return null; 42 | } 43 | } 44 | 45 | public static IEnumerable LoadFromFolder(string pathToFolder, ISet? nameSet = null) { 46 | L.Info("Loading plugins in '{0}' folder...", pathToFolder); 47 | 48 | var curDir = Directory.GetCurrentDirectory(); 49 | var fullPath = Path.Exists(pathToFolder) ? pathToFolder : Path.Combine(curDir, pathToFolder); 50 | nameSet ??= new HashSet(); 51 | 52 | if (!Path.Exists(fullPath)) 53 | yield break; 54 | 55 | foreach (var filePath in Directory.GetFiles(fullPath)) { 56 | if (!filePath.EndsWith("dll")) continue; 57 | 58 | var assembly = Load(filePath, nameSet); 59 | 60 | if (assembly == null) continue; 61 | 62 | yield return assembly; 63 | } 64 | 65 | foreach (var directory in Directory.GetDirectories(fullPath)) { 66 | var directoryName = Path.GetFileName(directory); 67 | 68 | var dllPath = Path.Combine(directory, $"{directoryName}.dll"); 69 | 70 | var assembly = Load(dllPath, nameSet); 71 | 72 | if (assembly == null) continue; 73 | 74 | yield return assembly; 75 | } 76 | } 77 | 78 | private static Assembly LoadPlugin(AssemblyLoadContext context, string assemblyPath) => 79 | context.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(assemblyPath))); 80 | 81 | private static IEnumerable CreatePlugins(PluginLoadContext context, Assembly assembly, 82 | ISet? nameSet = null 83 | ) { 84 | var loadedPlugins = 0; 85 | 86 | foreach (var type in assembly.GetTypes()) { 87 | if (!typeof(Plugin).IsAssignableFrom(type)) continue; 88 | 89 | if (Activator.CreateInstance(type) is not Plugin plugin) continue; 90 | 91 | L.Info("Loading plugin \"{0}\"...", plugin.Name); 92 | 93 | if (nameSet != null) 94 | if (!nameSet.Add(plugin.Name)) 95 | throw new ApplicationException( 96 | $"Name conflict! {assembly.GetName().Name} ({assembly.Location}) has '{plugin.Name}' plugin name that is already used by another plugin" 97 | ); 98 | 99 | loadedPlugins++; 100 | var wrapped = new WrappedPlugin(plugin, context, loadedPlugins == 1); 101 | 102 | L.Info("Loaded plugin \"{0}\" ({1})", plugin.Name, assembly.Location); 103 | 104 | yield return wrapped; 105 | } 106 | 107 | if (loadedPlugins != 0) yield break; 108 | 109 | var types = string.Join(",", assembly.GetTypes().Select(t => t.FullName)); 110 | throw new ApplicationException( 111 | $"{assembly} ({assembly.Location}) doesn't have any types that implement Plugin class\n" + 112 | $"Available types: {types}" 113 | ); 114 | } 115 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/Methods/ConfigurableReflectedAction.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using Streamduck.Actions; 8 | using Streamduck.Interfaces; 9 | 10 | namespace Streamduck.Plugins.Methods; 11 | 12 | public class ConfigurableReflectedAction( 13 | string name, 14 | Func actionToCall, 15 | string? description = null 16 | ) 17 | : PluginAction, IConfigurable where T : class, new() where C : class, new() { 18 | public override string Name { get; } = name; 19 | public override string? Description { get; } = description; 20 | public C Config { get; set; } = new(); 21 | 22 | public override Task Invoke(T data) => actionToCall.Invoke(data, Config); 23 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/Methods/ReflectedAction.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using Streamduck.Actions; 8 | 9 | namespace Streamduck.Plugins.Methods; 10 | 11 | public class ReflectedAction( 12 | string name, 13 | Func actionToCall, 14 | string? description = null 15 | ) 16 | : PluginAction where T : class, new() { 17 | public override string Name { get; } = name; 18 | public override string? Description { get; } = description; 19 | 20 | public override Task Invoke(T data) => actionToCall.Invoke(data); 21 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/PluginAssembly.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Streamduck.Plugins.Loaders; 7 | 8 | namespace Streamduck.Plugins; 9 | 10 | /** 11 | * Collection of all types inside of a plugin 12 | */ 13 | public class PluginAssembly(PluginLoadContext context, WrappedPlugin[] plugins) { 14 | internal readonly PluginLoadContext Context = context; 15 | 16 | public IEnumerable Plugins => plugins; 17 | 18 | public void Unload() { 19 | Context.Unload(); 20 | } 21 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/PluginCollection.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using Streamduck.Actions; 11 | using Streamduck.Configuration; 12 | using Streamduck.Data; 13 | using Streamduck.Plugins.Extensions; 14 | using Streamduck.Plugins.Loaders; 15 | using Streamduck.Rendering; 16 | using Streamduck.Socket; 17 | using Streamduck.Triggers; 18 | using Streamduck.Utils; 19 | 20 | namespace Streamduck.Plugins; 21 | 22 | public class PluginCollection : IPluginQuery { 23 | private readonly ConcurrentDictionary>> _actionMap; 24 | private readonly Config _config; 25 | private readonly ConcurrentDictionary>> _driverMap; 26 | 27 | private readonly ConcurrentDictionary> _pluginMap; 28 | private readonly ConcurrentDictionary>> _rendererMap; 29 | private readonly ConcurrentDictionary>> _socketRequestMap; 30 | private readonly ConcurrentDictionary>> _triggerMap; 31 | public readonly List Plugins; 32 | 33 | public PluginCollection(IEnumerable plugins, Config config) { 34 | _config = config; 35 | Plugins = plugins.ToList(); 36 | _pluginMap = new ConcurrentDictionary>( 37 | Plugins 38 | .SelectMany(a => a.Plugins) 39 | .ToDictionary(p => p.Name, p => new WeakReference(p)) 40 | ); 41 | _driverMap = BuildMap(p => p.Drivers); 42 | _actionMap = BuildMap(p => p.Actions); 43 | _rendererMap = BuildMap(p => p.Renderers); 44 | _triggerMap = BuildMap(p => p.Triggers); 45 | _socketRequestMap = BuildMap(p => p.SocketRequests); 46 | } 47 | 48 | public PluginCollection(IEnumerable plugins, Config config) : this(PluginsToAssembly(plugins), config) { } 49 | 50 | public IEnumerable AllPlugins() => 51 | from weakPlugin in _pluginMap 52 | let plugin = weakPlugin.Value.WeakToNullable() 53 | where plugin != null 54 | select plugin.Instance; 55 | 56 | public Plugin? SpecificPlugin(string name) => _pluginMap.GetValueOrDefault(name)?.WeakToNullable()?.Instance; 57 | 58 | public IEnumerable PluginsAssignableTo() where T : class => 59 | from weakPlugin in _pluginMap 60 | let plugin = weakPlugin.Value.WeakToNullable() 61 | where plugin != null 62 | let castedPlugin = plugin.Instance as T 63 | where castedPlugin != null 64 | select castedPlugin; 65 | 66 | public IEnumerable> AllDrivers() => GetAll(_driverMap); 67 | 68 | public IEnumerable> DriversByPlugin(string pluginName) => GetByPlugin(_driverMap, pluginName); 69 | 70 | public Namespaced? SpecificDriver(NamespacedName name) => GetSpecific(_driverMap, name); 71 | 72 | public NamespacedName DriverName(Driver driver) => FindNameFor(_driverMap, driver); 73 | 74 | public IEnumerable> AllActions() => GetAll(_actionMap); 75 | 76 | public IEnumerable> ActionsByPlugin(string pluginName) => 77 | GetByPlugin(_actionMap, pluginName); 78 | 79 | public Namespaced? SpecificAction(NamespacedName name) => GetSpecific(_actionMap, name); 80 | 81 | public NamespacedName ActionName(PluginAction action) => FindNameFor(_actionMap, action); 82 | 83 | public IEnumerable> AllRenderers() => GetAll(_rendererMap); 84 | 85 | public IEnumerable> RenderersByPlugin(string pluginName) => 86 | GetByPlugin(_rendererMap, pluginName); 87 | 88 | public Namespaced? SpecificRenderer(NamespacedName name) => GetSpecific(_rendererMap, name); 89 | 90 | public NamespacedName RendererName(Renderer renderer) => FindNameFor(_rendererMap, renderer); 91 | 92 | public Namespaced? DefaultRenderer() => 93 | GetSpecific(_rendererMap, _config.DefaultRenderer) 94 | ?? GetSpecific(_rendererMap, Config.DefaultRendererName); 95 | 96 | public IEnumerable> AllTriggers() => GetAll(_triggerMap); 97 | 98 | public IEnumerable> TriggersByPlugin(string pluginName) => GetByPlugin(_triggerMap, pluginName); 99 | 100 | public Namespaced? SpecificTrigger(NamespacedName name) => GetSpecific(_triggerMap, name); 101 | 102 | public NamespacedName TriggerName(Trigger trigger) => FindNameFor(_triggerMap, trigger); 103 | 104 | public IEnumerable> AllSocketRequests() => GetAll(_socketRequestMap); 105 | 106 | public IEnumerable> SocketRequestsByPlugin(string pluginName) => 107 | GetByPlugin(_socketRequestMap, pluginName); 108 | 109 | public Namespaced? SpecificSocketRequest(NamespacedName name) => 110 | GetSpecific(_socketRequestMap, name); 111 | 112 | public NamespacedName SocketRequestName(SocketRequest socketRequest) => 113 | FindNameFor(_socketRequestMap, socketRequest); 114 | 115 | private static PluginAssembly[] PluginsToAssembly(IEnumerable plugins) { 116 | var context = new PluginLoadContext(""); 117 | var pluginAssembly = new PluginAssembly(context, plugins.Select(p => new WrappedPlugin(p, context)).ToArray()); 118 | return new[] { pluginAssembly }; 119 | } 120 | 121 | private ConcurrentDictionary>> BuildMap( 122 | Func>> accessor 123 | ) where T : class { 124 | return new ConcurrentDictionary>>( 125 | Plugins 126 | .SelectMany(a => a.Plugins) 127 | .SelectMany(accessor) 128 | .ToDictionary(x => x.NamespacedName, x => new WeakReference>(x)) 129 | ); 130 | } 131 | 132 | private static void AddNamespacedToDict(ConcurrentDictionary>> dict, 133 | IEnumerable> enumerable 134 | ) where T : class { 135 | foreach (var x in enumerable) dict.TryAdd(x.NamespacedName, new WeakReference>(x)); 136 | } 137 | 138 | public Task AddPlugin(IStreamduck streamduck, PluginAssembly assembly) { 139 | Plugins.Add(assembly); 140 | 141 | foreach (var plugin in assembly.Plugins) { 142 | AddNamespacedToDict(_driverMap, plugin.Drivers); 143 | AddNamespacedToDict(_actionMap, plugin.Actions); 144 | } 145 | 146 | return streamduck.InvokeNewPluginsLoaded(assembly.Plugins.Select(w => w.Instance).ToArray()); 147 | } 148 | 149 | private static IEnumerable GetAll( 150 | ConcurrentDictionary> dict 151 | ) where T : class => 152 | from weak in dict 153 | let strong = weak.Value.WeakToNullable() 154 | where strong != null 155 | select strong; 156 | 157 | private static IEnumerable GetByPlugin( 158 | ConcurrentDictionary> dict, 159 | string pluginName 160 | ) where T : class => 161 | from weak in dict 162 | where weak.Key.PluginName == pluginName 163 | let strong = weak.Value.WeakToNullable() 164 | where strong != null 165 | select strong; 166 | 167 | private static T? GetSpecific( 168 | ConcurrentDictionary> dict, 169 | NamespacedName name 170 | ) where T : class => 171 | dict.GetValueOrDefault(name)?.WeakToNullable(); 172 | 173 | public IEnumerable AllWrappedPlugins() => 174 | from weakPlugin in _pluginMap 175 | let plugin = weakPlugin.Value.WeakToNullable() 176 | where plugin != null 177 | select plugin; 178 | 179 | public NamespacedName FindNameFor(ConcurrentDictionary>> dict, 180 | T instance 181 | ) 182 | where T : class => 183 | ( 184 | from weak in dict 185 | let strongValue = weak.Value.WeakToNullable() 186 | where strongValue != null 187 | where strongValue.Instance == instance 188 | select weak.Key 189 | ).First(); 190 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/PluginReflector.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | using Streamduck.Actions; 11 | using Streamduck.Attributes; 12 | using Streamduck.Plugins.Methods; 13 | using Streamduck.Utils; 14 | 15 | namespace Streamduck.Plugins; 16 | 17 | /** 18 | * Class that analyzes plugins for things to run 19 | */ 20 | public static class PluginReflector { 21 | public const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; 22 | 23 | private const BindingFlags ActionFlags = BindingFlags.Static | BindingFlags.NonPublic; 24 | 25 | private const BindingFlags ConfigurableActionFlags = BindingFlags.Static | BindingFlags.NonPublic; 26 | 27 | private static readonly MethodInfo ActionMethod = 28 | typeof(PluginReflector).GetMethod(nameof(GetAction), ActionFlags)!; 29 | 30 | private static readonly MethodInfo ConfigurableActionMethod = 31 | typeof(PluginReflector).GetMethod(nameof(GetConfigurableAction), ConfigurableActionFlags)!; 32 | 33 | public static IEnumerable GetMethods(object obj) => obj.GetType().GetMethods(Flags); 34 | 35 | // TODO: Validate plugins for incorrect usages of PluginMethod, report them as warnings during plugin load 36 | 37 | public static IEnumerable AnalyzeActions(IEnumerable methods, object obj) { 38 | return from method in methods 39 | where method.GetCustomAttribute() != null 40 | where method.ReturnType == typeof(Task) 41 | let parameters = method.GetParameters() 42 | let paramType = parameters.Length is 1 or 2 ? parameters[0]?.ParameterType : null 43 | let configType = parameters.Length == 2 ? parameters[1]?.ParameterType : null 44 | let isParameterful = paramType is not null && paramType.IsClass && !paramType.IsAbstract && 45 | paramType.GetConstructor(Type.EmptyTypes) is not null 46 | let isConfigurable = configType is not null && configType.IsClass && !configType.IsAbstract && 47 | configType.GetConstructor(Type.EmptyTypes) is not null 48 | let isParameterless = parameters.Length <= 0 49 | where isConfigurable || isParameterless || isParameterful 50 | select isConfigurable && isParameterful ? GetGenericConfigurableAction(paramType, configType, method, obj) : 51 | isParameterful ? GetGenericAction(paramType, method, obj) : 52 | new ReflectedAction( 53 | method.GetCustomAttribute()?.Name ?? method.Name.FormatAsWords(), 54 | _ => { 55 | var task = (Task?)method.Invoke(obj, Flags, null, null, null); 56 | return task ?? Task.CompletedTask; 57 | }, 58 | method.GetCustomAttribute()?.Description 59 | ); 60 | } 61 | 62 | private static PluginAction GetGenericAction(Type paramType, MethodInfo method, object obj) { 63 | return (PluginAction)ActionMethod.MakeGenericMethod(paramType) 64 | .Invoke( 65 | null, ActionFlags, null, new[] { method, obj }, 66 | null 67 | )!; 68 | } 69 | 70 | private static ReflectedAction GetAction(MethodBase method, object obj) where T : class, new() { 71 | return new ReflectedAction( 72 | method.GetCustomAttribute()?.Name ?? method.Name.FormatAsWords(), 73 | options => { 74 | var task = (Task?)method.Invoke(obj, Flags, null, new object?[] { options }, null); 75 | return task ?? Task.CompletedTask; 76 | }, 77 | method.GetCustomAttribute()?.Description 78 | ); 79 | } 80 | 81 | private static PluginAction GetGenericConfigurableAction(Type paramType, Type configType, MethodInfo method, 82 | object obj 83 | ) { 84 | return (PluginAction)ConfigurableActionMethod.MakeGenericMethod(paramType, configType) 85 | .Invoke( 86 | null, ConfigurableActionFlags, null, new[] { method, obj }, 87 | null 88 | )!; 89 | } 90 | 91 | private static ConfigurableReflectedAction GetConfigurableAction(MethodBase method, object obj) 92 | where T : class, new() where C : class, new() { 93 | return new ConfigurableReflectedAction( 94 | method.GetCustomAttribute()?.Name ?? method.Name.FormatAsWords(), 95 | (options, config) => { 96 | var task = (Task?)method.Invoke(obj, Flags, null, new object?[] { options, config }, null); 97 | return task ?? Task.CompletedTask; 98 | }, 99 | method.GetCustomAttribute()?.Description 100 | ); 101 | } 102 | 103 | public static IEnumerable GetPluginTypes(Type pluginType, bool useEmptyOnes) where T : class { 104 | foreach (var type in pluginType.Assembly.GetTypes()) { 105 | if (!type.IsAssignableTo(typeof(T))) continue; 106 | 107 | if (type.GetCustomAttribute() is not { } attribute) continue; 108 | if ((attribute.PluginClass is not null || !useEmptyOnes) && attribute.PluginClass != pluginType) continue; 109 | 110 | if (type.GetConstructor(Type.EmptyTypes) is not { } constructor) continue; 111 | yield return (T)constructor.Invoke([]); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /Streamduck/Plugins/WrappedPlugin.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Streamduck.Actions; 8 | using Streamduck.Data; 9 | using Streamduck.Interfaces; 10 | using Streamduck.Plugins.Loaders; 11 | using Streamduck.Rendering; 12 | using Streamduck.Socket; 13 | using Streamduck.Triggers; 14 | 15 | namespace Streamduck.Plugins; 16 | 17 | /** 18 | * Plugin wrapper that includes namespaced versions of all plugin types 19 | */ 20 | public sealed class WrappedPlugin { 21 | private readonly PluginLoadContext _originatedFrom; 22 | 23 | public WrappedPlugin(Plugin instance, PluginLoadContext originatedFrom, bool isFirst = false) { 24 | Instance = instance; 25 | _originatedFrom = originatedFrom; 26 | 27 | Name = Instance.Name; 28 | Drivers = Instance.Drivers 29 | .Concat(PluginReflector.GetPluginTypes(instance.GetType(), isFirst)) 30 | .Select(Namespace) 31 | .ToArray(); 32 | var methods = PluginReflector.GetMethods(instance).ToArray(); 33 | Actions = Instance.Actions 34 | .Concat(PluginReflector.AnalyzeActions(methods, instance)) 35 | .Concat(PluginReflector.GetPluginTypes(instance.GetType(), isFirst)) 36 | .Select(Namespace) 37 | .ToArray(); 38 | Renderers = Instance.Renderers 39 | .Concat(PluginReflector.GetPluginTypes(instance.GetType(), isFirst)) 40 | .Select(Namespace) 41 | .ToArray(); 42 | Triggers = Instance.Triggers 43 | .Concat(PluginReflector.GetPluginTypes(instance.GetType(), isFirst)) 44 | .Select(Namespace) 45 | .ToArray(); 46 | SocketRequests = Instance.SocketRequests 47 | .Concat(PluginReflector.GetPluginTypes(instance.GetType(), isFirst)) 48 | .Select(Namespace) 49 | .ToArray(); 50 | } 51 | 52 | public Plugin Instance { get; } 53 | 54 | public string Name { get; } 55 | 56 | public IEnumerable> Drivers { get; } 57 | public IEnumerable> Actions { get; } 58 | public IEnumerable> Renderers { get; } 59 | public IEnumerable> Triggers { get; } 60 | public IEnumerable> SocketRequests { get; } 61 | 62 | public Namespaced Namespace(T instance) where T : class, INamed => 63 | new(new NamespacedName(Name, instance.Name), instance); 64 | 65 | public bool BelongsTo(PluginAssembly assembly) => assembly.Context.Equals(_originatedFrom); 66 | } -------------------------------------------------------------------------------- /Streamduck/Program.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Diagnostics; 7 | using System.Net; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using NLog; 11 | using NLog.Config; 12 | using NLog.Layouts; 13 | using NLog.Targets; 14 | using Streamduck.Configuration; 15 | using Streamduck.Socket; 16 | 17 | // Setting up logger 18 | 19 | namespace Streamduck; 20 | 21 | internal class Program { 22 | public static async Task Main(string[] args) { 23 | var cts = new CancellationTokenSource(); 24 | 25 | var logConfig = new LoggingConfiguration(); 26 | 27 | logConfig.AddRule( 28 | LogLevel.Debug, LogLevel.Fatal, new ColoredConsoleTarget { 29 | Layout = Layout.FromString( 30 | "${longdate} ${level:uppercase=true} (${logger}): ${message}${onexception:inner=\\: ${exception}}" 31 | ) 32 | } 33 | ); 34 | Trace.Listeners.Add(new NLogTraceListener { Name = "SysTrace" }); 35 | 36 | LogManager.Configuration = logConfig; 37 | 38 | var L = LogManager.GetCurrentClassLogger(); 39 | 40 | // Initializing Streamduck 41 | var streamduck = new App(); 42 | await streamduck.Init(); 43 | 44 | // Starting Streamduck 45 | _ = Task.Run( 46 | async () => { 47 | try { 48 | await streamduck.Run(cts); 49 | } catch (TaskCanceledException) { 50 | // Ignored 51 | } catch (Exception e) { 52 | L!.Error(e, "Critical Error!"); 53 | } finally { 54 | var config = await Config.Get(); 55 | await config.SaveConfig(); 56 | } 57 | }, cts.Token 58 | ); 59 | 60 | // Starting API 61 | var config = await Config.Get(); 62 | 63 | var server = new Server( 64 | config.OpenToInternet 65 | ? IPAddress.Any 66 | : IPAddress.Loopback, config.WebSocketPort 67 | ) { 68 | AppInstance = streamduck 69 | }; 70 | 71 | server.Start(); 72 | 73 | await Task.Delay(-1, cts.Token); 74 | } 75 | } -------------------------------------------------------------------------------- /Streamduck/Socket/Server.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net; 8 | using System.Text; 9 | using System.Text.Json; 10 | using System.Text.Json.Serialization; 11 | using System.Threading.Tasks; 12 | using NetCoreServer; 13 | using NLog; 14 | using Streamduck.Plugins; 15 | 16 | namespace Streamduck.Socket; 17 | 18 | public class Session : WsSession { 19 | private static readonly Logger _l = LogManager.GetCurrentClassLogger(); 20 | 21 | private readonly Server _server; 22 | // private readonly StringBuilder _requestBuffer = new(); 23 | 24 | private readonly Dictionary> _listeners = []; 25 | 26 | public Session(Server server) : base(server) { 27 | _server = server; 28 | _l.Info($"Client {Id} connection started"); 29 | 30 | foreach (var plugin in _server.AppInstance.Plugins.AllPlugins()) { 31 | Action action = (name, data) => SendEventAsync(plugin, name, data); 32 | _listeners[plugin] = action; 33 | plugin.EventEmitted += action; 34 | } 35 | } 36 | 37 | public override void OnWsConnected(HttpResponse response) { 38 | _l.Info($"Client {Id} connected"); 39 | } 40 | 41 | public override void OnWsDisconnected() { 42 | _l.Info($"Client {Id} disconnected"); 43 | foreach (var (plugin, action) in _listeners) plugin.EventEmitted -= action; 44 | } 45 | 46 | public override void OnWsReceived(byte[] buffer, long offset, long size) { 47 | var message = Encoding.UTF8.GetString(buffer, (int)offset, (int)size); 48 | ReceivePacket(message); 49 | 50 | // while (true) { 51 | // var nullIndex = message.IndexOf('\u0004'); 52 | // 53 | // if (nullIndex >= 0) { 54 | // var left = message[..nullIndex]; 55 | // ReceivePacket(_requestBuffer + left); 56 | // _requestBuffer.Clear(); 57 | // message = message[(nullIndex + 1)..]; 58 | // } else { 59 | // _requestBuffer.Append(message); 60 | // break; 61 | // } 62 | // } 63 | } 64 | 65 | public void ReceivePacket(string packet) { 66 | Console.WriteLine(packet); 67 | try { 68 | var message = JsonSerializer.Deserialize(packet); 69 | 70 | if (message is null) { 71 | SendErrorAsync(new SocketError("Failed to parse")); 72 | return; 73 | } 74 | 75 | if (_server.AppInstance.PluginCollection!.SpecificSocketRequest(message.Name) is not { } request) { 76 | SendErrorAsync(new SocketError($"Request with name '{message.Name}' not found")); 77 | return; 78 | } 79 | 80 | Task.Run( 81 | async () => { 82 | await request.Instance.Received(new SessionRequester(this, message)).ConfigureAwait(false); 83 | } 84 | ); 85 | } catch (JsonException e) { 86 | SendErrorAsync(new SocketError(e.ToString())); 87 | } 88 | } 89 | 90 | internal void SendErrorAsync(SocketError error) { 91 | SendTextAsync(JsonSerializer.Serialize(error)); 92 | } 93 | 94 | internal void SendEventAsync(Plugin plugin, string name, object data) { 95 | SendTextAsync( 96 | JsonSerializer.Serialize( 97 | new Event { 98 | PluginName = plugin.Name, 99 | EventName = name, 100 | Data = data 101 | } 102 | ) 103 | ); 104 | } 105 | } 106 | 107 | public class Event { 108 | public string PluginName { get; set; } = "unknown"; 109 | public string EventName { get; set; } = "unknown"; 110 | public object? Data { get; set; } 111 | } 112 | 113 | internal readonly struct SocketError(string Error) { 114 | [JsonInclude] public string Error { get; } = Error; 115 | 116 | public override int GetHashCode() { 117 | unchecked { 118 | var hash = 17; 119 | hash = hash * 23 + Error.GetHashCode(); 120 | return hash; 121 | } 122 | } 123 | } 124 | 125 | public class Server : WsServer { 126 | private static readonly Logger _l = LogManager.GetCurrentClassLogger(); 127 | 128 | public Server(IPAddress address, int port) : base(address, port) { } 129 | public Server(string address, int port) : base(address, port) { } 130 | public Server(DnsEndPoint endpoint) : base(endpoint) { } 131 | public Server(IPEndPoint endpoint) : base(endpoint) { } 132 | 133 | public required App AppInstance { get; init; } 134 | 135 | protected override void OnStarted() { 136 | _l.Info($"Listening for websocket connections at {Address}:{Port}"); 137 | } 138 | 139 | protected override TcpSession CreateSession() => new Session(this); 140 | 141 | protected override void OnError(System.Net.Sockets.SocketError error) { 142 | _l.Error($"WebSocket error: {error}"); 143 | } 144 | } -------------------------------------------------------------------------------- /Streamduck/Socket/SessionRequester.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Text.Json; 7 | using NetCoreServer; 8 | using Streamduck.Plugins; 9 | 10 | namespace Streamduck.Socket; 11 | 12 | public class SessionRequester(WsSession Session, SocketMessage Message) : SocketRequester { 13 | public override SocketMessage Message { get; } = Message; 14 | 15 | public override void SendBack(object? data) { 16 | var response = JsonSerializer.Serialize( 17 | new Response { 18 | Data = data, 19 | Name = Message.Name, 20 | RequestID = Message.RequestID 21 | } 22 | ); 23 | Session.SendTextAsync(response); 24 | } 25 | 26 | public override void SendBackError(string message) { 27 | SendBack(new SocketError(message)); 28 | } 29 | 30 | public override T? ParseData() where T : class { 31 | if (Message.Data is not { } data) { 32 | SendBack(new SocketError("Data was missing for request that needs data")); 33 | return null; 34 | } 35 | 36 | try { 37 | return data.Deserialize(); 38 | } catch (JsonException e) { 39 | SendBack(new SocketError(e.Message)); 40 | return null; 41 | } 42 | } 43 | } 44 | 45 | internal class Response { 46 | public string? RequestID { get; set; } 47 | public required NamespacedName Name { get; set; } 48 | public required object? Data { get; set; } 49 | } -------------------------------------------------------------------------------- /Streamduck/Streamduck.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /StreamduckGS/Class1.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace StreamduckGS; 6 | 7 | public class Class1 { } -------------------------------------------------------------------------------- /StreamduckGS/Parser/gs.g4: -------------------------------------------------------------------------------- 1 | grammar gs; 2 | 3 | program: scriptParameter* useImport* (eventDeclaration | functionDeclaration | constant)* EOF; 4 | 5 | // Declaration 6 | 7 | pluginItem: item=STRING_LITERAL AT plugin=STRING_LITERAL; 8 | useImport: USE pluginItem AS ID; 9 | 10 | parameter: ID; 11 | parameters: LPARENT parameter (COMMA parameter)* RPARENT; 12 | 13 | eventDeclaration: ON ID parameters? LBRACE statement* RBRACE; 14 | functionDeclaration: FN ID parameters? LBRACE statement* RBRACE; 15 | 16 | scriptParameterProperty: ID ASSIGNMENT literal; 17 | scriptParameter: PARAMETER parameterVariable=ID AS parameterFieldType=STRING_LITERAL scriptParameterProperty*; 18 | 19 | constant: CONST name=ID ASSIGNMENT ( 20 | integer 21 | | FLOAT 22 | | STRING_LITERAL 23 | | (TRUE | FALSE) 24 | ); 25 | 26 | // Statement stuff 27 | 28 | argument: expression; 29 | arguments: argument (COMMA argument)*; 30 | 31 | call: anyVariable LPARENT arguments? RPARENT; 32 | objectedCall: anyVariable COLON ID LPARENT arguments? RPARENT; 33 | 34 | returnStatement: RETURN expression # returnSomething 35 | | RETURN # returnNothing; 36 | 37 | globalVariable: GLOBAL ID # globalAccess 38 | | globalVariable DOT localVariable # globalNested 39 | | globalVariable LSQUARE expression RSQUARE # globalArrayAccess; 40 | localVariable: ID # varLocal 41 | | localVariable DOT localVariable # varNested 42 | | localVariable LSQUARE expression RSQUARE # varArrayAccess; 43 | anyVariable: localVariable | globalVariable; 44 | 45 | variableAssignment: anyVariable ASSIGNMENT expression # assign 46 | | anyVariable ADD_AND_ASSIGN expression # addAssign 47 | | anyVariable SUB_AND_ASSIGN expression # subAssign 48 | | anyVariable MUL_AND_ASSIGN expression # mulAssign 49 | | anyVariable DIV_AND_ASSIGN expression # divAssign 50 | | anyVariable POW_AND_ASSIGN expression # powAssign 51 | | anyVariable MOD_AND_ASSIGN expression # modAssign 52 | | anyVariable ASSIGN_IF_NULL expression # assignIfNull; 53 | 54 | ifStatement: IF expression LBRACE statement* RBRACE 55 | ( ELSE IF expression LBRACE statement* RBRACE)* 56 | ( ELSE LBRACE statement* RBRACE )?; 57 | 58 | forLoop: FOR ID ASSIGNMENT expression TO expression (STEP expression)? LBRACE statement* RBRACE; 59 | 60 | whileLoop: WHILE expression LBRACE statement* RBRACE; 61 | 62 | lockStatement: LOCK globalVariable LBRACE statement* RBRACE; 63 | 64 | codeBlock: LBRACE statement* RBRACE; 65 | 66 | statement: ifStatement 67 | | forLoop 68 | | whileLoop 69 | | lockStatement 70 | | codeBlock 71 | | functionDeclaration 72 | | variableAssignment 73 | | objectedCall 74 | | call 75 | | returnStatement; 76 | 77 | // Expression stuff 78 | 79 | objectItem: (STRING_LITERAL | ID) COLON expression; 80 | 81 | integer: INT | BINARY | HEX | OCTAL; 82 | 83 | literal: integer # literalInteger 84 | | FLOAT # literalFloat 85 | | STRING_LITERAL # literalString 86 | | (TRUE | FALSE) # literalBoolean 87 | | NULL # literalNull 88 | | LSQUARE expression? (COMMA expression)* RSQUARE # literalArray 89 | | LBRACE objectItem? (COMMA objectItem)* RBRACE # literalObject; 90 | 91 | expression: expression ADD expression # expressionMathAdd // Math 92 | | expression SUB expression # expressionMathSub 93 | | expression MUL expression # expressionMathMul 94 | | expression DIV expression # expressionMathDiv 95 | | expression POW expression # expressionMathPow 96 | | expression MOD expression # expressionMathMod 97 | 98 | | expression EQUAL expression # expressionEqualTo // Comparison 99 | | expression NOT_EQUAL expression # expressionNotEqualTo 100 | | expression GREATER expression # expressionGreaterThan 101 | | expression GREATER_EQUAL expression # expressionGreaterOrEqualThan 102 | | expression LESSER expression # expressionLesserThan 103 | | expression LESSER_EQUAL expression # expressionLesserOrEqualThan 104 | 105 | | NOT expression # expressionNot // Logic/Bitwise 106 | | expression OR expression # expressionOr 107 | | expression AND expression # expressionAnd 108 | | expression XOR expression # expressionXor 109 | | expression LSHIFT expression # expressionLeftShift 110 | | expression RSHIFT expression # expressionRightShift 111 | 112 | | expression NULL_COALESCE expression # expressionNullCoalesce 113 | 114 | | FN parameters? LBRACE statement* RBRACE # anonymousFunction // Functions 115 | | expression LPARENT arguments? RPARENT # expressionCall 116 | | expression COLON ID LPARENT arguments? RPARENT # expressionObjectedCall 117 | 118 | | literal # expressionLiteral // Literals 119 | 120 | | anyVariable # expressionVarValue // Variables 121 | 122 | | expression DOT localVariable # expressionObjectAccess 123 | | expression LSQUARE expression RSQUARE # expressionArrayAccess 124 | 125 | | IF expression THEN expression ELSE expression # ternary // Ternary 126 | 127 | | LPARENT expression RPARENT # expressionNested; // Nested expression 128 | 129 | // Lexer down here 130 | 131 | // Comment 132 | COMMENT: '//' ~('\r' | '\n')* -> skip; 133 | 134 | // Keywords 135 | USE: 'use'; 136 | AS: 'as'; 137 | AT: 'at'; 138 | ON: 'on'; 139 | FN: 'fn'; 140 | GLOBAL: 'global'; 141 | RETURN: 'return'; 142 | IF: 'if'; 143 | THEN: 'then'; 144 | ELSE: 'else'; 145 | FOR: 'for'; 146 | TO: 'to'; 147 | STEP: 'step'; 148 | WHILE: 'while'; 149 | PARAMETER: 'parameter'; 150 | LOCK: 'lock'; 151 | CONST: 'const'; 152 | 153 | // Null related operators 154 | ASSIGN_IF_NULL: '?='; 155 | NULL_COALESCE: '??'; 156 | 157 | // Logic/Bitwise operators 158 | NOT: 'not'; 159 | OR: 'or'; 160 | AND: 'and'; 161 | XOR: 'xor'; 162 | LSHIFT: 'lshift'; 163 | RSHIFT: 'rshift'; 164 | 165 | // Symbols 166 | LPARENT: '('; 167 | RPARENT: ')'; 168 | LBRACE: '{'; 169 | RBRACE: '}'; 170 | LSQUARE: '['; 171 | RSQUARE: ']'; 172 | COMMA: ','; 173 | DOT: '.'; 174 | COLON: ':'; 175 | 176 | // Comparison operators 177 | EQUAL: '=='; 178 | NOT_EQUAL: '!='; 179 | GREATER: '>'; 180 | LESSER: '<'; 181 | GREATER_EQUAL: '>='; 182 | LESSER_EQUAL: '<='; 183 | 184 | // Math operators 185 | ADD: '+'; 186 | SUB: '-'; 187 | MUL: '*'; 188 | DIV: '/'; 189 | POW: '^'; 190 | MOD: '%'; 191 | 192 | // Assignment operators 193 | ASSIGNMENT: '='; 194 | ADD_AND_ASSIGN: '+='; 195 | SUB_AND_ASSIGN: '-='; 196 | MUL_AND_ASSIGN: '*='; 197 | DIV_AND_ASSIGN: '/='; 198 | POW_AND_ASSIGN: '^='; 199 | MOD_AND_ASSIGN: '%='; 200 | 201 | // Literals 202 | STRING_LITERAL : '"' (~('"' | '\\' ) | '\\' ('"' | '\\'))* '"'; 203 | FLOAT: '-'?[0-9_]+('.' [0-9_]+)?; 204 | BINARY: '0b' [01_]+; 205 | HEX: '0x' [0-9a-fA-F_]+; 206 | OCTAL: '0o' [0-7_]+; 207 | INT: '-'?[0-9_]+; 208 | TRUE: 'true'; 209 | FALSE: 'false'; 210 | NULL: 'null'; 211 | 212 | ID: [a-zA-Z_][a-zA-Z_\-0-9]*; 213 | 214 | WS: [ \t\n]+ -> skip; 215 | ANY: .; -------------------------------------------------------------------------------- /StreamduckGS/StreamduckGS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | false 13 | runtime 14 | 15 | 16 | false 17 | runtime 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /StreamduckShared/Actions/ActionInstance.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | 7 | namespace Streamduck.Actions; 8 | 9 | public class ActionInstance(PluginAction original) { 10 | public PluginAction Original { get; } = original; 11 | public object? Data { get; set; } 12 | 13 | public async Task Invoke() { 14 | Data ??= await Original.DefaultData(); 15 | await Original.Invoke(Data); 16 | } 17 | } -------------------------------------------------------------------------------- /StreamduckShared/Actions/PluginAction.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using Streamduck.Interfaces; 8 | 9 | namespace Streamduck.Actions; 10 | 11 | /** 12 | * Action that can be triggered by Triggers. Data won't be saved since the type is unknown, use typed version for data loading 13 | */ 14 | public abstract class PluginAction : INamed { 15 | public abstract string? Description { get; } 16 | public abstract string Name { get; } 17 | 18 | /** 19 | * If argument was of invalid type 20 | */ 21 | public abstract Task Invoke(object data); 22 | 23 | /** 24 | * Should create default data that can be used with Invoke 25 | */ 26 | public abstract Task DefaultData(); 27 | } 28 | 29 | /** 30 | * Action that can be triggered by Triggers, but also has typed data associated 31 | */ 32 | public abstract class PluginAction : PluginAction where T : class, new() { 33 | public override Task Invoke(object data) { 34 | if (data is not T casted) 35 | throw new ArgumentException($"Data is of type {data.GetType()}, expected {typeof(T)}"); 36 | 37 | return Invoke(casted); 38 | } 39 | 40 | public abstract Task Invoke(T data); 41 | 42 | public override Task DefaultData() => Task.FromResult((object)new T()); 43 | } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/AutoAddAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Autoinjects instance of the class into appropriate collection on the plugin 11 | */ 12 | [AttributeUsage(AttributeTargets.Class, Inherited = false)] 13 | public class AutoAddAttribute(Type? pluginClass = null) : Attribute { 14 | public Type? PluginClass { get; } = pluginClass; 15 | } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/BitmaskAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Will use an Enum as a bitmask 11 | */ 12 | [AttributeUsage(AttributeTargets.Property)] 13 | public class BitmaskAttribute : Attribute; -------------------------------------------------------------------------------- /StreamduckShared/Attributes/DescriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Specifies description for the property, or description for Action/Function parameters or returns 11 | */ 12 | [AttributeUsage( 13 | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method 14 | | AttributeTargets.Parameter | AttributeTargets.ReturnValue 15 | )] 16 | public class DescriptionAttribute(string description) : Attribute { 17 | public string Description { get; } = description; 18 | } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/HeaderAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Display a header before the property 11 | */ 12 | [AttributeUsage(AttributeTargets.Property)] 13 | public class HeaderAttribute(string text) : Attribute { 14 | public string Text { get; } = text; 15 | } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/IgnoreAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Makes property be ignored by UI 11 | */ 12 | [AttributeUsage(AttributeTargets.Property)] 13 | public class IgnoreAttribute : Attribute { } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/IncludeAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Include non-public property in UI 11 | */ 12 | [AttributeUsage(AttributeTargets.Property)] 13 | public class IncludeAttribute(bool write = false) : Attribute { 14 | public bool WriteAllowed { get; } = write; 15 | } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/NameAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Renames property in UI, methods, parameters and return types. If name is empty, title will not be shown 11 | */ 12 | [AttributeUsage( 13 | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method 14 | | AttributeTargets.Parameter | AttributeTargets.ReturnValue 15 | )] 16 | public class NameAttribute(string name) : Attribute { 17 | public string Name { get; } = name; 18 | } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/NumberFieldAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Numerics; 7 | 8 | namespace Streamduck.Attributes; 9 | 10 | /** 11 | * Settings for a number field. Min and Max can be used to limit the value to the bounds. 12 | * Slider can be disabled if number field is preferred. 13 | * Limit enforcement can be enabled if inputting number out of bounds is unacceptable. 14 | * Default bounds are 0 to 1. 15 | */ 16 | [AttributeUsage(AttributeTargets.Property)] 17 | public class NumberFieldAttribute(T? min, T? max, bool slider = true, bool enforceLimit = false) 18 | : Attribute 19 | where T : INumber { 20 | public NumberFieldAttribute(bool slider = true, bool enforceLimit = false) : this( 21 | T.Zero, T.One, slider, 22 | enforceLimit 23 | ) { } 24 | 25 | public T Min { get; } = min ?? T.Zero; 26 | public T Max { get; } = max ?? T.One; 27 | public bool Slider { get; } = slider; 28 | public bool EnforceLimit { get; } = enforceLimit; 29 | } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/PluginMethodAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Method will be automatically wrapped into a plugin action 11 | */ 12 | [AttributeUsage(AttributeTargets.Method)] 13 | public class PluginMethodAttribute : Attribute { } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/ReadOnlyAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Make property read only in UI despite having public setter 11 | */ 12 | [AttributeUsage(AttributeTargets.Property)] 13 | public class ReadOnlyAttribute : Attribute { } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/StaticTextAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Will add static text field before the field 11 | */ 12 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] 13 | public class StaticTextAttribute(string text) : Attribute { 14 | public string Text { get; } = text; 15 | } -------------------------------------------------------------------------------- /StreamduckShared/Attributes/SwitchAttribute.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Attributes; 8 | 9 | /** 10 | * Makes boolean properties get switch style of a toggle 11 | */ 12 | [AttributeUsage(AttributeTargets.Property)] 13 | public class SwitchAttribute : Attribute { } -------------------------------------------------------------------------------- /StreamduckShared/Cores/Core.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using Streamduck.Devices; 8 | using Streamduck.Inputs; 9 | 10 | namespace Streamduck.Cores; 11 | 12 | public abstract class Core(Device associatedDevice) : IDisposable { 13 | protected readonly Device _associatedDevice = associatedDevice; 14 | 15 | public abstract NamespacedDeviceIdentifier DeviceIdentifier { get; } 16 | public IReadOnlyCollection Inputs { get; } = associatedDevice.Inputs; 17 | 18 | /** 19 | * Top screen of the stack 20 | */ 21 | public abstract Screen? CurrentScreen { get; } 22 | 23 | /** 24 | * Contents of the stack 25 | */ 26 | public abstract IEnumerable ScreenStack { get; } 27 | 28 | public void Dispose() { 29 | if (_associatedDevice is IDisposable disposable) disposable.Dispose(); 30 | GC.SuppressFinalize(this); 31 | } 32 | 33 | public bool IsAlive() => _associatedDevice.Alive; 34 | 35 | /** 36 | * Create new screen that can later be pushed into the stack 37 | */ 38 | public abstract Screen NewScreen(string name = "Screen", bool canWrite = true); 39 | 40 | /** 41 | * Push screen into the stack 42 | */ 43 | public abstract void PushScreen(Screen screen); 44 | 45 | /** 46 | * Pops screen from the stack 47 | */ 48 | public abstract Screen? PopScreen(); 49 | 50 | /** 51 | * Replaces current screen with another 52 | */ 53 | public abstract Screen? ReplaceScreen(Screen newScreen); 54 | 55 | /** 56 | * Called on every tick 57 | */ 58 | public abstract event Action? Tick; 59 | } -------------------------------------------------------------------------------- /StreamduckShared/Cores/Screen.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Streamduck.Inputs; 7 | 8 | namespace Streamduck.Cores; 9 | 10 | /** 11 | * Screen that can contain screen items 12 | */ 13 | public abstract class Screen { 14 | public string Name { get; set; } = "Screen"; 15 | public abstract Core AssociatedCore { get; } 16 | public bool CanWrite { get; init; } = true; 17 | public abstract IReadOnlyCollection Items { get; } 18 | public abstract ScreenItem CreateItem(int index); 19 | public abstract ScreenItem DeleteItem(int index); 20 | } -------------------------------------------------------------------------------- /StreamduckShared/Cores/ScreenItem.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Streamduck.Inputs; 7 | using Streamduck.Plugins; 8 | using Streamduck.Triggers; 9 | 10 | namespace Streamduck.Cores; 11 | 12 | /** 13 | * Item of the screen that has optional renderer settings and can contain actions 14 | */ 15 | public abstract class ScreenItem { 16 | public abstract IReadOnlyCollection Triggers { get; } 17 | 18 | public abstract void AddTrigger(TriggerInstance trigger, bool attachToInput = true); 19 | 20 | public abstract bool RemoveTrigger(TriggerInstance trigger); 21 | 22 | public abstract void Attach(Input input); 23 | 24 | public abstract void Detach(); 25 | 26 | public interface IRenderable { 27 | NamespacedName? RendererName { get; set; } 28 | object? RendererSettings { get; set; } 29 | } 30 | } -------------------------------------------------------------------------------- /StreamduckShared/Data/Double2.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Data; 6 | 7 | public struct Double2(double x, double y) { 8 | public double X { get; set; } = x; 9 | public double Y { get; set; } = y; 10 | 11 | public override string ToString() => $"{{ X: {X}, Y: {Y} }}"; 12 | 13 | public override int GetHashCode() { 14 | unchecked { 15 | var hash = 17; 16 | hash = hash * 23 + X.GetHashCode(); 17 | hash = hash * 23 + Y.GetHashCode(); 18 | return hash; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /StreamduckShared/Data/Int2.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Data; 6 | 7 | public struct Int2(int x, int y) { 8 | public int X { get; set; } = x; 9 | public int Y { get; set; } = y; 10 | 11 | public override string ToString() => $"{{ X: {X}, Y: {Y} }}"; 12 | 13 | public override int GetHashCode() { 14 | unchecked { 15 | var hash = 17; 16 | hash = hash * 23 + X.GetHashCode(); 17 | hash = hash * 23 + Y.GetHashCode(); 18 | return hash; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /StreamduckShared/Data/Namespaced.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using Streamduck.Plugins; 6 | 7 | namespace Streamduck.Data; 8 | 9 | /** 10 | * Type that belongs to a plugin 11 | */ 12 | public class Namespaced(NamespacedName namespacedName, T instance) 13 | where T : class { 14 | public NamespacedName NamespacedName { get; } = namespacedName; 15 | public T Instance { get; } = instance; 16 | 17 | public string Name => NamespacedName.Name; 18 | public string PluginName => NamespacedName.PluginName; 19 | } -------------------------------------------------------------------------------- /StreamduckShared/Data/UInt2.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Data; 6 | 7 | public struct UInt2(uint x, uint y) { 8 | public uint X { get; set; } = x; 9 | public uint Y { get; set; } = y; 10 | 11 | public override string ToString() => $"{{ X: {X}, Y: {Y} }}"; 12 | 13 | public override int GetHashCode() { 14 | unchecked { 15 | var hash = 17; 16 | hash = hash * 23 + X.GetHashCode(); 17 | hash = hash * 23 + Y.GetHashCode(); 18 | return hash; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /StreamduckShared/Devices/Device.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using Streamduck.Inputs; 7 | 8 | namespace Streamduck.Devices; 9 | 10 | public abstract class Device { 11 | protected Device(DeviceIdentifier identifier) { 12 | Identifier = identifier; 13 | Alive = true; 14 | Died += () => Alive = false; 15 | } 16 | 17 | public bool Alive { get; private set; } 18 | 19 | public bool Busy { get; set; } 20 | public DeviceIdentifier Identifier { get; } 21 | 22 | public abstract Input[] Inputs { get; } 23 | 24 | public event Action Died; 25 | 26 | protected void Die() { 27 | if (Alive) Died.Invoke(); 28 | } 29 | 30 | public void ThrowDisconnectedIfDead() { 31 | if (!Alive) throw new DeviceDisconnectedException(); 32 | } 33 | } -------------------------------------------------------------------------------- /StreamduckShared/Devices/DeviceDisconnectedException.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Devices; 8 | 9 | public class DeviceDisconnectedException : Exception { 10 | public override string Message => "Device got disconnected"; 11 | } -------------------------------------------------------------------------------- /StreamduckShared/Devices/DeviceIdentifier.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Text.Json.Serialization; 6 | 7 | namespace Streamduck.Devices; 8 | 9 | [method: JsonConstructor] 10 | public readonly struct DeviceIdentifier(string Identifier, string Description) { 11 | public string Identifier { get; } = Identifier; 12 | 13 | public string Description { get; } = Description; 14 | 15 | public override string ToString() => $"{Identifier} ({Description})"; 16 | 17 | public override int GetHashCode() { 18 | unchecked { 19 | var hash = 17; 20 | hash = hash * 23 + Identifier.GetHashCode(); 21 | hash = hash * 23 + Description.GetHashCode(); 22 | return hash; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /StreamduckShared/Devices/DeviceMetadata.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using Streamduck.Inputs; 6 | 7 | namespace Streamduck.Devices; 8 | 9 | public struct DeviceMetadata { 10 | public Input[] Inputs { get; init; } 11 | } -------------------------------------------------------------------------------- /StreamduckShared/Devices/IDevicePoolable.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | 7 | namespace Streamduck.Devices; 8 | 9 | /** 10 | * Device should implement this interface if it needs to be pooled to receive events from it. 11 | * You don't have to implement this if your device is event based or it can't generate any events. 12 | */ 13 | public interface IDevicePoolable { 14 | Task Poll(); 15 | } -------------------------------------------------------------------------------- /StreamduckShared/Devices/NamespacedDeviceIdentifier.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Text.Json.Serialization; 6 | using Streamduck.Plugins; 7 | 8 | namespace Streamduck.Devices; 9 | 10 | [method: JsonConstructor] 11 | public readonly struct NamespacedDeviceIdentifier(NamespacedName NamespacedName, DeviceIdentifier DeviceIdentifier) { 12 | [JsonIgnore] public string PluginName => NamespacedName.PluginName; 13 | 14 | [JsonIgnore] public string DriverName => NamespacedName.Name; 15 | 16 | [JsonIgnore] public string Identifier => DeviceIdentifier.Identifier; 17 | 18 | [JsonIgnore] public string Description => DeviceIdentifier.Description; 19 | 20 | public NamespacedName NamespacedName { get; } = NamespacedName; 21 | 22 | public DeviceIdentifier DeviceIdentifier { get; } = DeviceIdentifier; 23 | 24 | public override string ToString() => $"{DeviceIdentifier} from {NamespacedName}"; 25 | 26 | public override int GetHashCode() { 27 | unchecked { 28 | var hash = 17; 29 | hash = hash * 23 + NamespacedName.GetHashCode(); 30 | hash = hash * 23 + DeviceIdentifier.GetHashCode(); 31 | return hash; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /StreamduckShared/Fields/Field.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Numerics; 7 | 8 | namespace Streamduck.Fields; 9 | 10 | public abstract class Field(string title) { 11 | public string Title { get; } = title; 12 | public string? Description { get; init; } 13 | 14 | /** 15 | * Displays title text in a large text 16 | */ 17 | public class Header(string title) : Field(title); 18 | 19 | /** 20 | * Displays description text in a normal text font with optional title 21 | */ 22 | public class StaticText(string title) : Field(title); 23 | 24 | /** 25 | * Displays text from text accessor 26 | */ 27 | public class Label(string title, Func textGetter) : Field(title) { 28 | public string Text => textGetter.Invoke(); 29 | } 30 | 31 | /** 32 | * Checkbox 33 | */ 34 | public class Checkbox : Field { 35 | private readonly Func _getter; 36 | private readonly Action? _setter; 37 | 38 | public Checkbox(string title, Func getter, Action? setter) : base(title) { 39 | _getter = getter; 40 | _setter = setter; 41 | Disabled = _setter == null; 42 | } 43 | 44 | public bool Disabled { get; } 45 | public bool SwitchStyle { get; init; } 46 | 47 | public bool Value { 48 | get => _getter.Invoke(); 49 | set => _setter?.Invoke(value); 50 | } 51 | } 52 | 53 | /** 54 | * Input field that changes the bound value, can contain any UTF-8 character 55 | */ 56 | public class StringInput : Field { 57 | private readonly Func _getter; 58 | private readonly Action? _setter; 59 | 60 | public StringInput(string title, Func getter, Action? setter = null) : base(title) { 61 | _getter = getter; 62 | _setter = setter; 63 | Disabled = _setter == null; 64 | } 65 | 66 | public bool Disabled { get; } 67 | 68 | public string Value { 69 | get => _getter.Invoke(); 70 | set => _setter?.Invoke(value); 71 | } 72 | } 73 | 74 | /** 75 | * Input field that changes the bound value, can contain any number 76 | */ 77 | public class NumberInput : Field where T : INumber { 78 | private readonly Func _getter; 79 | private readonly Action? _setter; 80 | 81 | public NumberInput(string title, Func getter, Action? setter = null) : base(title) { 82 | _getter = getter; 83 | _setter = setter; 84 | Disabled = _setter == null; 85 | } 86 | 87 | public bool Disabled { get; } 88 | public bool Slider { get; init; } 89 | public bool EnforceLimit { get; init; } 90 | 91 | public T? Min { get; init; } 92 | public T? Max { get; init; } 93 | 94 | public T Value { 95 | get => _getter.Invoke(); 96 | set => _setter?.Invoke(value); 97 | } 98 | } 99 | 100 | public class Choice : Field { 101 | private readonly Func _getter; 102 | private readonly Action? _setter; 103 | 104 | public Choice(string title, Func getter, Action? setter, (string, string?)[] variants) : 105 | base(title) { 106 | _getter = getter; 107 | _setter = setter; 108 | Variants = variants; 109 | Disabled = _setter == null; 110 | } 111 | 112 | public bool Disabled { get; } 113 | public (string, string?)[] Variants { get; } 114 | 115 | public string Value { 116 | get => _getter.Invoke(); 117 | set => _setter?.Invoke(value); 118 | } 119 | } 120 | 121 | public class MultiChoice : Field { 122 | private readonly Func _getter; 123 | private readonly Action? _setter; 124 | 125 | public MultiChoice(string title, Func getter, Action? setter, 126 | (string, string?)[] variants 127 | ) : base(title) { 128 | _getter = getter; 129 | _setter = setter; 130 | Variants = variants; 131 | Disabled = _setter == null; 132 | } 133 | 134 | public bool Disabled { get; } 135 | public (string, string?)[] Variants { get; } 136 | 137 | public bool? this[string name] { 138 | get { 139 | var index = FindVariantIndex(name); 140 | if (index < 0 || index >= Variants.Length) return null; 141 | return _getter.Invoke()[index]; 142 | } 143 | 144 | set { 145 | if (_setter == null) return; 146 | if (value == null) return; 147 | 148 | var index = FindVariantIndex(name); 149 | if (index == -1) return; 150 | _setter.Invoke(name, value.Value); 151 | } 152 | } 153 | 154 | public bool[] Values => _getter.Invoke(); 155 | 156 | private int FindVariantIndex(string name) { 157 | for (var i = 0; i < Variants.Length; i++) 158 | if (Variants[i].Item1.Equals(name)) 159 | return i; 160 | 161 | return -1; 162 | } 163 | } 164 | 165 | public class Array(string title, Field[][] values) : Field(title) { 166 | public Field[][] Values { get; init; } = values; 167 | 168 | /** 169 | * Serializable object that will be created when UI is trying to add an element to the array. 170 | * If null, UI will not be able to create any elements in this array 171 | */ 172 | public object? NewElementTemplate { get; init; } 173 | 174 | public bool AllowRemoving { get; init; } 175 | 176 | public bool AllowReorder { get; init; } 177 | } 178 | 179 | public class NestedFields(string title) : Field(title) { 180 | public Field[] Schema { get; init; } = System.Array.Empty(); 181 | } 182 | } -------------------------------------------------------------------------------- /StreamduckShared/Fields/FieldReflector.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Numerics; 9 | using System.Reflection; 10 | using Streamduck.Attributes; 11 | using Streamduck.Utils; 12 | 13 | namespace Streamduck.Fields; 14 | 15 | /** 16 | * Class that analyzes objects for fields 17 | */ 18 | public static class FieldReflector { 19 | private const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; 20 | 21 | public static IEnumerable AnalyzeObject(object? obj) { 22 | if (obj == null) { 23 | yield return new Field.StaticText("Object was null"); 24 | yield break; 25 | } 26 | 27 | foreach (var property in obj.GetType().GetProperties(Flags)) { 28 | // Handle Header first 29 | if (property.GetCustomAttribute() is { } headerAttribute) 30 | yield return new Field.Header(headerAttribute.Text); 31 | 32 | // Handle static text 33 | foreach (var attribute in property.GetCustomAttributes()) 34 | yield return new Field.StaticText(attribute.Text); 35 | 36 | // Property must have a getter to be considered for UI 37 | var getMethod = property.GetGetMethod(true); 38 | if (getMethod is null) continue; 39 | 40 | // Continue if not public and doesn't have Include attribute 41 | if ((!getMethod.IsPublic && property.GetCustomAttribute() == null) 42 | || property.GetCustomAttribute() != null) continue; 43 | 44 | var title = GetMemberName(property); 45 | var description = GetMemberDescription(property); 46 | 47 | var type = property.PropertyType; 48 | 49 | if (type == typeof(string)) { 50 | // Check if there's public setter 51 | if (HasSetter(property)) { 52 | // Do text field instead 53 | string Getter() => (string)property.GetValue(obj)!; 54 | 55 | Action? setter = IsReadWrite(property) 56 | ? val => property.SetValue(obj, val) 57 | : null; 58 | 59 | yield return new Field.StringInput(title, Getter, setter) { 60 | Description = description 61 | }; 62 | } else { 63 | // Do label 64 | yield return new Field.Label(title, () => (string)property.GetValue(obj)!) { 65 | Description = description 66 | }; 67 | } 68 | } else if (type.IsAssignableTo(typeof(INumber<>))) { 69 | yield return (Field)typeof(FieldReflector).GetMethod( 70 | nameof(MakeGenericNumberField), 71 | BindingFlags.NonPublic | BindingFlags.Static 72 | )!.MakeGenericMethod(type) 73 | .Invoke(null, new[] { property, obj, title, description! })!; 74 | } else if (type == typeof(bool)) { 75 | bool Getter() => (bool)property.GetValue(obj)!; 76 | 77 | Action? setter = IsReadWrite(property) 78 | ? val => property.SetValue(obj, val) 79 | : null; 80 | 81 | yield return new Field.Checkbox(title, Getter, setter) { 82 | Description = description, 83 | SwitchStyle = property.GetCustomAttribute() != null 84 | }; 85 | } else if (type.IsAssignableTo(typeof(Enum))) { 86 | var variants = type.GetFields(BindingFlags.Public | BindingFlags.Static); 87 | var underlyingType = type.GetEnumUnderlyingType(); 88 | if (property.GetCustomAttribute() != null) { 89 | yield return (Field)typeof(FieldReflector).GetMethod( 90 | nameof(MakeGenericBitmask), 91 | BindingFlags.NonPublic | BindingFlags.Static 92 | )!.MakeGenericMethod(underlyingType) 93 | .Invoke(null, new[] { property, obj, variants, title, description! })!; 94 | } else { 95 | string FindVariant() { 96 | var value = property.GetValue(obj)!; 97 | 98 | foreach (var variant in variants) 99 | if (value.Equals(variant.GetValue(null))) 100 | return GetMemberName(variant); 101 | 102 | return "Unknown"; 103 | } 104 | 105 | void SetVariant(string variant) { 106 | foreach (var variantInfo in variants) { 107 | var name = GetMemberName(variantInfo); 108 | 109 | if (name.Equals(variant)) property.SetValue(obj, variantInfo.GetValue(null)); 110 | } 111 | } 112 | 113 | string Getter() => FindVariant(); 114 | 115 | Action? setter = IsReadWrite(property) 116 | ? SetVariant 117 | : null; 118 | 119 | yield return new Field.Choice(title, Getter, setter, HumanReadableVariants(variants).ToArray()) { 120 | Description = description 121 | }; 122 | } 123 | } else if (type.IsClass && Nullable.GetUnderlyingType(type) == null) { 124 | // Use recursion for anything else 125 | yield return new Field.NestedFields(title) { 126 | Description = description, 127 | Schema = AnalyzeObject(property.GetValue(obj)).ToArray() 128 | }; 129 | } 130 | 131 | // TODO: Split all field creation into private methods and implement collection support 132 | } 133 | } 134 | 135 | private static Field MakeGenericNumberField(PropertyInfo property, object obj, string title, string? description) 136 | where T : INumber { 137 | var attr = property.GetCustomAttribute>() ?? new NumberFieldAttribute(); 138 | Action? setter = IsReadWrite(property) 139 | ? val => property.SetValue(obj, val) 140 | : null; 141 | return new Field.NumberInput(title, Getter, setter) { 142 | Description = description, 143 | Min = attr.Min, 144 | Max = attr.Max, 145 | EnforceLimit = attr.EnforceLimit, 146 | Slider = attr.Slider 147 | }; 148 | 149 | T Getter() => (T)property.GetValue(obj)!; 150 | } 151 | 152 | private static Field MakeGenericBitmask(PropertyInfo property, object obj, FieldInfo[] variants, string title, 153 | string? description 154 | ) 155 | where T : IBinaryInteger { 156 | return new Field.MultiChoice( 157 | title, 158 | () => GetVariantStatuses().ToArray(), 159 | SetVariantStatus, 160 | HumanReadableVariants(variants).ToArray() 161 | ) { 162 | Description = description 163 | }; 164 | 165 | long SetFlagAny(FieldInfo variantInfo, bool state) { 166 | var currentValue = Convert.ToInt64(property.GetValue(obj)); 167 | var variantValue = Convert.ToInt64(variantInfo.GetValue(null)); 168 | 169 | return state 170 | ? currentValue | variantValue 171 | : currentValue & ~variantValue; 172 | } 173 | 174 | ulong SetFlagULong(FieldInfo variantInfo, bool state) { 175 | var currentValue = Convert.ToUInt64(property.GetValue(obj)); 176 | var variantValue = Convert.ToUInt64(variantInfo.GetValue(null)); 177 | 178 | return state 179 | ? currentValue | variantValue 180 | : currentValue & ~variantValue; 181 | } 182 | 183 | void SetVariantStatus(string variant, bool state) { 184 | foreach (var variantInfo in variants) { 185 | var name = GetMemberName(variantInfo); 186 | 187 | if (!name.Equals(variant)) continue; 188 | 189 | property.SetValue( 190 | obj, 191 | typeof(T) == typeof(ulong) 192 | ? Enum.ToObject(property.PropertyType, SetFlagULong(variantInfo, state)) 193 | : Enum.ToObject(property.PropertyType, SetFlagAny(variantInfo, state)) 194 | ); 195 | } 196 | } 197 | 198 | IEnumerable GetVariantStatuses() { 199 | var value = (Enum)property.GetValue(obj)!; 200 | 201 | foreach (var variant in variants) { 202 | var variantValue = (Enum)variant.GetValue(null)!; 203 | var hasFlag = value.HasFlag(variantValue); 204 | 205 | yield return hasFlag; 206 | } 207 | } 208 | } 209 | 210 | private static bool HasSetter(PropertyInfo property) => property.GetSetMethod(true) != null; 211 | 212 | private static bool HasPublicSetter(PropertyInfo property) => property.GetSetMethod() != null; 213 | 214 | private static bool IsReadWrite(PropertyInfo property) => 215 | (HasPublicSetter(property) || (property.GetCustomAttribute()?.WriteAllowed ?? false)) 216 | && property.GetCustomAttribute() == null; 217 | 218 | private static string GetMemberName(MemberInfo member) => 219 | member.GetCustomAttribute()?.Name 220 | ?? member.Name.FormatAsWords(); 221 | 222 | private static string? GetMemberDescription(MemberInfo member) => 223 | member.GetCustomAttribute()?.Description; 224 | 225 | private static IEnumerable<(string, string?)> HumanReadableVariants(IEnumerable variants) => 226 | from variant in variants 227 | let name = GetMemberName(variant) 228 | let variantDescription = GetMemberDescription(variant) 229 | select (name, variantDescription); 230 | } -------------------------------------------------------------------------------- /StreamduckShared/IStreamduck.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Streamduck.Cores; 8 | using Streamduck.Devices; 9 | using Streamduck.Images; 10 | using Streamduck.Plugins; 11 | 12 | namespace Streamduck; 13 | 14 | /** 15 | * Collection of references to systems used around Streamduck project 16 | */ 17 | public interface IStreamduck { 18 | public IPluginQuery Plugins { get; } 19 | public IImageCollection Images { get; } 20 | public IReadOnlyCollection DiscoveredDevices { get; } 21 | public IReadOnlyDictionary ConnectedDevices { get; } 22 | } -------------------------------------------------------------------------------- /StreamduckShared/Images/IImageCollection.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using SixLabors.ImageSharp; 8 | 9 | namespace Streamduck.Images; 10 | 11 | public interface IImageCollection { 12 | public IReadOnlyDictionary Images { get; } 13 | public Guid Add(Image image); 14 | public Image? Delete(Guid id); 15 | } -------------------------------------------------------------------------------- /StreamduckShared/Images/ImageExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using SixLabors.ImageSharp; 6 | 7 | namespace Streamduck.Images; 8 | 9 | public static class ImageExtensions { 10 | public static bool IsAnimated(this Image image) => image.Frames.Count > 1; 11 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputButton.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Inputs; 8 | 9 | public interface IInputButton { 10 | event Action? ButtonPressed; 11 | event Action? ButtonReleased; 12 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputDisplay.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | using SixLabors.ImageSharp; 7 | using Streamduck.Data; 8 | 9 | namespace Streamduck.Inputs; 10 | 11 | public interface IInputDisplay { 12 | UInt2 DisplayResolution { get; } 13 | 14 | /** 15 | * Streamduck will hash its render structures and then call this to append the hash in case the devices requires 16 | * 2 different formats depending on which input is being rendered to 17 | */ 18 | long AppendHashKey(long key); 19 | 20 | /** 21 | * Lets the device process the image into format it needs, 22 | * key is derived from render structure and appended by the input 23 | */ 24 | Task UploadImage(long key, Image image); 25 | 26 | /** 27 | * Should return true if image still exists, false if image was already deleted by the cache 28 | */ 29 | ValueTask ApplyImage(long key); 30 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputEncoder.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Inputs; 8 | 9 | public interface IInputEncoder { 10 | event Action? EncoderTwisted; 11 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputJoystick.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using Streamduck.Data; 7 | 8 | namespace Streamduck.Inputs; 9 | 10 | public interface IInputJoystick { 11 | Double2 JoystickValue { get; } 12 | Double2 MinJoystickValue { get; } 13 | Double2 MaxJoystickValue { get; } 14 | event Action? JoystickValueChanged; 15 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputKnob.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Inputs; 8 | 9 | public interface IInputKnob { 10 | double KnobValue { get; } 11 | double MinKnobValue { get; } 12 | double MaxKnobValue { get; } 13 | event Action? KnobValueChanged; 14 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputPressure.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Inputs; 8 | 9 | /** 10 | * Button press pressure, 3D touch, etc. 11 | */ 12 | public interface IInputPressure { 13 | double Pressure { get; } 14 | double MinPressure { get; } 15 | double MaxPressure { get; } 16 | event Action? PressureChanged; 17 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputSlider.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Inputs; 8 | 9 | public interface IInputSlider { 10 | double SliderValue { get; } 11 | double MinSliderValue { get; } 12 | double MaxSliderValue { get; } 13 | event Action? SliderValueChanged; 14 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputToggle.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace Streamduck.Inputs; 8 | 9 | public interface IInputToggle { 10 | bool ToggleState { get; } 11 | event Action? ToggleStateChanged; 12 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputTouchScreen.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using Streamduck.Data; 7 | 8 | namespace Streamduck.Inputs; 9 | 10 | public interface IInputTouchScreen { 11 | event Action? TouchScreenPressed; 12 | event Action? TouchScreenReleased; 13 | 14 | public interface Drag { 15 | event Action? TouchScreenDragStart; 16 | event Action? TouchScreenDragEnd; 17 | } 18 | 19 | public interface Dragging { 20 | event Action? TouchScreenDragging; 21 | } 22 | 23 | public interface Hover { 24 | event Action? TouchScreenHoverStart; 25 | event Action? TouchScreenHovering; 26 | event Action? TouchScreenHoverEnd; 27 | } 28 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/IInputTrackpad.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using Streamduck.Data; 7 | 8 | namespace Streamduck.Inputs; 9 | 10 | public interface IInputTrackpad { 11 | event Action? TrackpadDragged; 12 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/Input.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Inputs; 6 | 7 | public abstract class Input(int x, int y, uint w, uint h, InputIcon icon) { 8 | public int X { get; } = x; 9 | public int Y { get; } = y; 10 | public uint W { get; } = w; 11 | public uint H { get; } = h; 12 | public InputIcon Icon { get; } = icon; 13 | } -------------------------------------------------------------------------------- /StreamduckShared/Inputs/InputIcon.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Inputs; 6 | 7 | public enum InputIcon { 8 | Button, 9 | Toggle, 10 | AnalogButton, 11 | Slider, 12 | Knob, 13 | Encoder, 14 | TouchScreen, 15 | Joystick, 16 | Trackball, 17 | Touchpad, 18 | Sensor 19 | } -------------------------------------------------------------------------------- /StreamduckShared/Interfaces/IConfigurable.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Streamduck.Fields; 7 | 8 | namespace Streamduck.Interfaces; 9 | 10 | /* TODO: Put this information somewhere in Field documentation 11 | * Options objects will be queried using reflection for public properties and/or attributed properties. 12 | * - Property that has getter and setter can be edited in UI 13 | * - Property that only has getter will no be able to be edited in UI and will be shown as read-only 14 | * - Non-public property annotated with [Include] will be shown in UI and will be read-only unless write parameter is used in [Include] 15 | * - Non-public properties not annotated with [Include] will not be shown in UI 16 | * 17 | * Property field type will be assumed from it's Type, use any of the [Field] attributes to force field type 18 | */ 19 | 20 | /** 21 | * Object provided as Options needs to support serialization, 22 | * along with having fields that can be represented as Field objects 23 | */ 24 | public interface IConfigurable { 25 | IEnumerable Config { get; } 26 | } 27 | 28 | /** 29 | *

30 | * Allows types to contain options that can be configured from UI, 31 | * saving and loading will be done automatically. 32 | *

33 | *

34 | * Type provided as Options needs to support serialization and deserialization, 35 | * along with having properties that can be represented as Field objects. 36 | *

37 | */ 38 | public interface IConfigurable : IConfigurable where T : class, new() { 39 | new T Config { get; set; } 40 | IEnumerable IConfigurable.Config => FieldReflector.AnalyzeObject(Config); 41 | } -------------------------------------------------------------------------------- /StreamduckShared/Interfaces/INamed.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Interfaces; 6 | 7 | public interface INamed { 8 | string Name { get; } 9 | } -------------------------------------------------------------------------------- /StreamduckShared/Plugins/Driver.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Streamduck.Devices; 8 | using Streamduck.Interfaces; 9 | 10 | namespace Streamduck.Plugins; 11 | 12 | public abstract class Driver : INamed { 13 | public abstract string Name { get; } 14 | public abstract Task> ListDevices(); 15 | public abstract Task ConnectDevice(DeviceIdentifier identifier); 16 | } -------------------------------------------------------------------------------- /StreamduckShared/Plugins/IPluginQuery.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Streamduck.Actions; 7 | using Streamduck.Data; 8 | using Streamduck.Rendering; 9 | using Streamduck.Socket; 10 | using Streamduck.Triggers; 11 | 12 | namespace Streamduck.Plugins; 13 | 14 | /** 15 | * Allows to query all loaded plugins 16 | */ 17 | public interface IPluginQuery { 18 | IEnumerable AllPlugins(); 19 | Plugin? SpecificPlugin(string name); 20 | IEnumerable PluginsAssignableTo() where T : class; 21 | 22 | IEnumerable> AllDrivers(); 23 | IEnumerable> DriversByPlugin(string pluginName); 24 | Namespaced? SpecificDriver(NamespacedName name); 25 | 26 | Namespaced? SpecificDriver(string pluginName, string name) => 27 | SpecificDriver(new NamespacedName(pluginName, name)); 28 | 29 | NamespacedName DriverName(Driver driver); 30 | 31 | IEnumerable> AllActions(); 32 | IEnumerable> ActionsByPlugin(string pluginName); 33 | Namespaced? SpecificAction(NamespacedName name); 34 | 35 | Namespaced? SpecificAction(string pluginName, string name) => 36 | SpecificAction(new NamespacedName(pluginName, name)); 37 | 38 | NamespacedName ActionName(PluginAction action); 39 | 40 | IEnumerable> AllRenderers(); 41 | IEnumerable> RenderersByPlugin(string pluginName); 42 | Namespaced? SpecificRenderer(NamespacedName name); 43 | 44 | Namespaced? SpecificRenderer(string pluginName, string name) => 45 | SpecificRenderer(new NamespacedName(pluginName, name)); 46 | 47 | NamespacedName RendererName(Renderer renderer); 48 | 49 | Namespaced? DefaultRenderer(); 50 | 51 | IEnumerable> AllTriggers(); 52 | IEnumerable> TriggersByPlugin(string pluginName); 53 | Namespaced? SpecificTrigger(NamespacedName name); 54 | 55 | Namespaced? SpecificTrigger(string pluginName, string name) => 56 | SpecificTrigger(new NamespacedName(pluginName, name)); 57 | 58 | NamespacedName TriggerName(Trigger trigger); 59 | 60 | IEnumerable> AllSocketRequests(); 61 | IEnumerable> SocketRequestsByPlugin(string pluginName); 62 | Namespaced? SpecificSocketRequest(NamespacedName name); 63 | 64 | Namespaced? SpecificSocketRequest(string pluginName, string name) => 65 | SpecificSocketRequest(new NamespacedName(pluginName, name)); 66 | 67 | NamespacedName SocketRequestName(SocketRequest socketRequest); 68 | } -------------------------------------------------------------------------------- /StreamduckShared/Plugins/NamespacedName.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Text.Json.Serialization; 6 | 7 | namespace Streamduck.Plugins; 8 | 9 | [method: JsonConstructor] 10 | public readonly struct NamespacedName(string PluginName, string Name) { 11 | [JsonInclude] public string PluginName { get; } = PluginName; 12 | 13 | [JsonInclude] public string Name { get; } = Name; 14 | 15 | public override string ToString() => $"{Name} ({PluginName})"; 16 | 17 | public override int GetHashCode() { 18 | unchecked { 19 | var hash = 17; 20 | hash = hash * 23 + PluginName.GetHashCode(); 21 | hash = hash * 23 + Name.GetHashCode(); 22 | return hash; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /StreamduckShared/Plugins/Plugin.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using Streamduck.Actions; 9 | using Streamduck.Cores; 10 | using Streamduck.Devices; 11 | using Streamduck.Interfaces; 12 | using Streamduck.Rendering; 13 | using Streamduck.Socket; 14 | using Streamduck.Triggers; 15 | 16 | namespace Streamduck.Plugins; 17 | 18 | public abstract class Plugin : INamed { 19 | public virtual IEnumerable Drivers { get; } = Array.Empty(); 20 | public virtual IEnumerable Actions { get; } = Array.Empty(); 21 | public virtual IEnumerable Renderers { get; } = Array.Empty(); 22 | public virtual IEnumerable Triggers { get; } = Array.Empty(); 23 | public virtual IEnumerable SocketRequests { get; } = Array.Empty(); 24 | public abstract string Name { get; } 25 | 26 | public virtual Task OnPluginsLoaded(IStreamduck streamduck) => Task.CompletedTask; 27 | 28 | public virtual Task OnNewPluginsLoaded(IEnumerable newPlugins, IStreamduck streamduck) => 29 | Task.CompletedTask; 30 | 31 | public virtual Task OnDeviceConnected(NamespacedDeviceIdentifier identifier, Core deviceCore) => Task.CompletedTask; 32 | 33 | public virtual Task OnDeviceDisconnected(NamespacedDeviceIdentifier identifier) => Task.CompletedTask; 34 | 35 | public virtual Task OnDeviceAppeared(NamespacedDeviceIdentifier identifier) => Task.CompletedTask; 36 | 37 | public virtual Task OnDeviceDisappeared(NamespacedDeviceIdentifier identifier) => Task.CompletedTask; 38 | 39 | public event Action? EventEmitted; 40 | 41 | protected void SendEventToSocket(string name, object data) { 42 | EventEmitted?.Invoke(name, data); 43 | } 44 | } -------------------------------------------------------------------------------- /StreamduckShared/Rendering/Renderer.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using SixLabors.ImageSharp; 7 | using Streamduck.Cores; 8 | using Streamduck.Interfaces; 9 | 10 | namespace Streamduck.Rendering; 11 | 12 | /** 13 | * Renderer that will render images for input's screens 14 | */ 15 | public abstract class Renderer : INamed { 16 | public abstract object DefaultRendererConfig { get; } 17 | public abstract string Name { get; } 18 | public abstract long Hash(ScreenItem input, object? renderConfig); 19 | public abstract Image Render(ScreenItem input, object? renderConfig, bool forPreview); 20 | } 21 | 22 | public abstract class Renderer : Renderer where T : class, new() { 23 | public override object DefaultRendererConfig => new T(); 24 | 25 | public override long Hash(ScreenItem input, object? renderConfig) { 26 | if (renderConfig is not T castedConfig) 27 | throw new ArgumentException("Render config was of incorrect type"); 28 | return Hash(input, castedConfig); 29 | } 30 | 31 | public override Image Render(ScreenItem input, object? renderConfig, bool forPreview) { 32 | if (renderConfig is not T castedConfig) 33 | throw new ArgumentException("Render config was of incorrect type"); 34 | return Render(input, castedConfig, forPreview); 35 | } 36 | 37 | public abstract long Hash(ScreenItem input, T renderConfig); 38 | public abstract Image Render(ScreenItem input, T renderConfig, bool forPreview); 39 | } -------------------------------------------------------------------------------- /StreamduckShared/Socket/SocketMessage.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Text.Json; 6 | using Streamduck.Plugins; 7 | 8 | namespace Streamduck.Socket; 9 | 10 | public class SocketMessage { 11 | public NamespacedName Name { get; set; } 12 | public JsonElement? Data { get; set; } 13 | public string? RequestID { get; set; } 14 | } -------------------------------------------------------------------------------- /StreamduckShared/Socket/SocketRequest.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | using Streamduck.Interfaces; 7 | 8 | namespace Streamduck.Socket; 9 | 10 | /** 11 | * Socket requests that can be defined by plugins for custom socket behaviors 12 | */ 13 | public abstract class SocketRequest : INamed { 14 | public abstract string Name { get; } 15 | 16 | public abstract Task Received(SocketRequester request); 17 | } 18 | 19 | /** 20 | * Socket requests that can be defined by plugins for custom socket behaviors, with automatic data parsing 21 | */ 22 | public abstract class SocketRequest : SocketRequest where T : class { 23 | public override Task Received(SocketRequester request) { 24 | if (request.ParseData() is { } data) return Received(request, data); 25 | return Task.CompletedTask; 26 | } 27 | 28 | public abstract Task Received(SocketRequester request, T data); 29 | } -------------------------------------------------------------------------------- /StreamduckShared/Socket/SocketRequester.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Socket; 6 | 7 | /** 8 | * Represents the client that sent the request, contains message and helper methods to send back or serialize data 9 | */ 10 | public abstract class SocketRequester { 11 | public abstract SocketMessage Message { get; } 12 | public abstract void SendBack(object? data); 13 | public abstract void SendBackError(string message); 14 | public abstract T? ParseData() where T : class; 15 | } -------------------------------------------------------------------------------- /StreamduckShared/StreamduckShared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | disable 6 | enable 7 | Streamduck 8 | 9 | 10 | 11 | 12 | 13 | ..\..\..\.nuget\packages\messagepack.annotations\2.5.124\lib\netstandard2.0\MessagePack.Annotations.dll 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /StreamduckShared/Triggers/Trigger.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | using Streamduck.Inputs; 7 | using Streamduck.Interfaces; 8 | 9 | namespace Streamduck.Triggers; 10 | 11 | public abstract class Trigger : INamed { 12 | public abstract string? Description { get; } 13 | public abstract string Name { get; } 14 | public abstract bool IsApplicableTo(Input input); 15 | public abstract Task CreateInstance(); 16 | } -------------------------------------------------------------------------------- /StreamduckShared/Triggers/TriggerInstance.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using Streamduck.Actions; 9 | using Streamduck.Inputs; 10 | 11 | namespace Streamduck.Triggers; 12 | 13 | /** 14 | * Instance of Trigger that goes onto the input 15 | */ 16 | public abstract class TriggerInstance(Trigger original) { 17 | protected readonly List _actions = []; 18 | 19 | public Trigger Original { get; } = original; 20 | public IReadOnlyCollection Actions => _actions; 21 | 22 | public void AddAction(ActionInstance action) { 23 | _actions.Add(action); 24 | } 25 | 26 | public void RemoveAction(int index) { 27 | _actions.RemoveAt(index); 28 | } 29 | 30 | public void AddActions(IEnumerable actions) { 31 | _actions.AddRange(actions); 32 | } 33 | 34 | public abstract void Attach(Input input); 35 | public abstract void Detach(Input input); 36 | 37 | protected void InvokeActions() { 38 | Task.Run(async () => { await Task.WhenAll(_actions.Select(a => a.Invoke())); }); 39 | } 40 | } 41 | 42 | /** 43 | * Instance of Trigger that goes onto the input, but with options 44 | */ 45 | public abstract class TriggerInstance(Trigger original) : TriggerInstance(original) where T : class, new() { 46 | public T Options { get; set; } = new(); 47 | } -------------------------------------------------------------------------------- /StreamduckShared/Utils/ScreenExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace Streamduck.Utils; 6 | 7 | public static class ScreenExtensions { } -------------------------------------------------------------------------------- /StreamduckShared/Utils/UtilMethods.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Linq; 7 | 8 | namespace Streamduck.Utils; 9 | 10 | public static class UtilMethods { 11 | public static string FormatAsWords(this string name) { 12 | return string.Concat( 13 | name 14 | .Select((x, i) => i == 0 ? $"{char.ToUpper(x)}" : char.IsUpper(x) ? $" {x}" : $"{x}") 15 | ).TrimStart(' '); 16 | } 17 | 18 | public static T? WeakToNullable(this WeakReference weakReference) where T : class { 19 | weakReference.TryGetTarget(out var result); 20 | return result; 21 | } 22 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/Inputs/StreamDeckButton.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using ElgatoStreamDeck; 8 | using Microsoft.Extensions.Caching.Memory; 9 | using SixLabors.ImageSharp; 10 | using Streamduck.Data; 11 | using Streamduck.Inputs; 12 | using Input = Streamduck.Inputs.Input; 13 | 14 | namespace StreamduckStreamDeck.Inputs; 15 | 16 | public class StreamDeckButton(StreamDeckDevice device, int x, int y, UInt2 displayResolution, byte keyIndex) 17 | : Input(x, y, 1, 1, InputIcon.Button), IInputButton, IInputDisplay { 18 | public event Action? ButtonPressed; 19 | public event Action? ButtonReleased; 20 | 21 | public UInt2 DisplayResolution { get; } = displayResolution; 22 | 23 | public long AppendHashKey(long key) => $"{key}button".GetHashCode(); 24 | 25 | public async Task UploadImage(long key, Image image) { 26 | device.ThrowDisconnectedIfDead(); 27 | var data = await ImageUtils.EncodeImageForButtonAsync(image, device._device.Kind()); 28 | device.SetCache(key, data); 29 | } 30 | 31 | public ValueTask ApplyImage(long key) { 32 | device.ThrowDisconnectedIfDead(); 33 | if (!device._imageCache.TryGetValue(key, out byte[]? data)) 34 | return ValueTask.FromResult(false); 35 | 36 | device._device.WriteImage(keyIndex, data); 37 | 38 | return ValueTask.FromResult(true); 39 | } 40 | 41 | internal void CallPressed() { 42 | ButtonPressed?.Invoke(); 43 | } 44 | 45 | internal void CallReleased() { 46 | ButtonReleased?.Invoke(); 47 | } 48 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/Inputs/StreamDeckButtonWithoutDisplay.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using Streamduck.Inputs; 7 | 8 | namespace StreamduckStreamDeck.Inputs; 9 | 10 | public class StreamDeckButtonWithoutDisplay(int x, int y) : Input(x, y, 1, 1, InputIcon.Button), IInputButton { 11 | public event Action? ButtonPressed; 12 | public event Action? ButtonReleased; 13 | 14 | internal void CallPressed() { 15 | ButtonPressed?.Invoke(); 16 | } 17 | 18 | internal void CallReleased() { 19 | ButtonReleased?.Invoke(); 20 | } 21 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/Inputs/StreamDeckEncoder.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using Streamduck.Inputs; 7 | 8 | namespace StreamduckStreamDeck.Inputs; 9 | 10 | public class StreamDeckEncoder(int x, int y) : Input(x, y, 1, 1, InputIcon.Encoder), IInputButton, IInputEncoder { 11 | public event Action? ButtonPressed; 12 | public event Action? ButtonReleased; 13 | public event Action? EncoderTwisted; 14 | 15 | internal void CallPressed() { 16 | ButtonPressed?.Invoke(); 17 | } 18 | 19 | internal void CallReleased() { 20 | ButtonReleased?.Invoke(); 21 | } 22 | 23 | internal void CallTwist(int value) { 24 | EncoderTwisted?.Invoke(value); 25 | } 26 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/Inputs/StreamDeckLCDSegment.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using ElgatoStreamDeck; 8 | using Microsoft.Extensions.Caching.Memory; 9 | using SixLabors.ImageSharp; 10 | using Streamduck.Data; 11 | using Streamduck.Inputs; 12 | using Input = Streamduck.Inputs.Input; 13 | 14 | namespace StreamduckStreamDeck.Inputs; 15 | 16 | public class StreamDeckLCDSegment(StreamDeckDevice device, int x, int y, uint w, UInt2 displayResolution) 17 | : Input(x, y, w, 1, InputIcon.TouchScreen), IInputTouchScreen, IInputTouchScreen.Drag, IInputDisplay { 18 | public event Action? TouchScreenDragStart; 19 | public event Action? TouchScreenDragEnd; 20 | 21 | public UInt2 DisplayResolution { get; } = displayResolution; 22 | 23 | public long AppendHashKey(long key) => $"{key}lcd".GetHashCode(); 24 | 25 | public async Task UploadImage(long key, Image image) { 26 | device.ThrowDisconnectedIfDead(); 27 | var data = await ImageUtils.EncodeImageForLcdAsync(image, image.Width, image.Height); 28 | device.SetCache(key, data); 29 | } 30 | 31 | public ValueTask ApplyImage(long key) { 32 | device.ThrowDisconnectedIfDead(); 33 | device._imageCache.TryGetValue(key, out byte[]? data); 34 | 35 | if (data == null) return ValueTask.FromResult(false); 36 | 37 | var resolution = device._device.Kind().KeyImageMode().Resolution; 38 | device._device.WriteLcd(0, 0, (ushort)resolution.Item1, (ushort)resolution.Item2, data); 39 | 40 | return ValueTask.FromResult(true); 41 | } 42 | 43 | public event Action? TouchScreenPressed; 44 | public event Action? TouchScreenReleased; 45 | 46 | internal void CallPressed(Int2 position) { 47 | TouchScreenPressed?.Invoke(position); 48 | } 49 | 50 | internal void CallReleased(Int2 position) { 51 | TouchScreenReleased?.Invoke(position); 52 | } 53 | 54 | internal void CallDrag(Int2 start, Int2 end) { 55 | TouchScreenDragStart?.Invoke(start); 56 | TouchScreenDragEnd?.Invoke(end); 57 | } 58 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/StreamDeckDevice.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using ElgatoStreamDeck; 9 | using HidApi; 10 | using Microsoft.Extensions.Caching.Memory; 11 | using Streamduck.Data; 12 | using Streamduck.Devices; 13 | using Streamduck.Interfaces; 14 | using StreamduckStreamDeck.Inputs; 15 | using Device = Streamduck.Devices.Device; 16 | using ElgatoDevice = ElgatoStreamDeck.IDevice; 17 | using Input = Streamduck.Inputs.Input; 18 | 19 | namespace StreamduckStreamDeck; 20 | 21 | public class StreamDeckDevice : Device, IDisposable, IConfigurable { 22 | internal readonly ElgatoDevice _device; 23 | private readonly DeviceReader _deviceReader; 24 | internal readonly IMemoryCache _imageCache = new MemoryCache(new MemoryCacheOptions()); 25 | 26 | internal readonly MemoryCacheEntryOptions _imageCacheEntryOptions = new() { 27 | SlidingExpiration = TimeSpan.FromMinutes(5) 28 | }; 29 | 30 | private readonly Thread _readingThread; 31 | 32 | public StreamDeckDevice(ElgatoDevice device, DeviceIdentifier identifier) : base(identifier) { 33 | _device = device; 34 | _deviceReader = new DeviceReader(_device); 35 | 36 | // Creating input objects 37 | var kind = StreamDeckDriver.DescriptionToKind(identifier.Description); 38 | 39 | var inputs = new Input[kind.KeyCount() + kind.EncoderCount() + (kind.LcdStripSize() != null ? 1 : 0)]; 40 | 41 | // Setting buttons 42 | var resolution = kind.KeyImageMode().Resolution; 43 | var hasScreen = resolution.Item1 > 0; 44 | 45 | for (var x = 0; x < kind.ColumnCount(); x++) 46 | for (var y = 0; y < kind.RowCount(); y++) { 47 | var i = x + y * kind.ColumnCount(); 48 | inputs[i] = hasScreen 49 | ? new StreamDeckButton( 50 | this, 51 | x, 52 | y, 53 | new UInt2(resolution.Item1, resolution.Item2), 54 | (byte)i 55 | ) 56 | : new StreamDeckButtonWithoutDisplay(x, y); 57 | } 58 | 59 | // Setting screen 60 | if (kind.LcdStripSize() is { } strip) 61 | inputs[kind.KeyCount()] = new StreamDeckLCDSegment( 62 | this, 63 | 0, 64 | kind.RowCount(), 65 | 4, 66 | new UInt2(strip.Item1, strip.Item2) 67 | ); 68 | 69 | // Setting encoders 70 | for (var i = 0; i < kind.EncoderCount(); i++) 71 | inputs[kind.KeyCount() + 1 + i] = new StreamDeckEncoder(i, kind.RowCount() + 1); 72 | 73 | Inputs = inputs; 74 | 75 | // Reading thread 76 | _readingThread = new Thread(ReaderThread); 77 | _readingThread.Start(); 78 | } 79 | 80 | public override Input[] Inputs { get; } 81 | public StreamDeckDeviceOptions Config { get; set; } = new(); 82 | 83 | public void Dispose() { 84 | Die(); 85 | _readingThread.Interrupt(); 86 | _device.Dispose(); 87 | GC.SuppressFinalize(this); 88 | } 89 | 90 | private void ReaderThread() { 91 | var lcdIndex = _device.Kind().KeyCount(); 92 | var encoderOffset = _device.Kind().KeyCount() + 1; 93 | 94 | while (Alive) 95 | try { 96 | foreach (var input in _deviceReader.Read()) 97 | switch (input) { 98 | case DeviceReader.Input.ButtonPressed buttonPressed: { 99 | if (Inputs[buttonPressed.key] is StreamDeckButton button) button.CallPressed(); 100 | 101 | break; 102 | } 103 | 104 | case DeviceReader.Input.ButtonReleased buttonReleased: { 105 | if (Inputs[buttonReleased.key] is StreamDeckButton button) button.CallReleased(); 106 | 107 | break; 108 | } 109 | 110 | case DeviceReader.Input.EncoderPressed encoderPressed: { 111 | if (Inputs[encoderOffset + encoderPressed.encoder] is StreamDeckEncoder encoder) 112 | encoder.CallPressed(); 113 | 114 | break; 115 | } 116 | 117 | case DeviceReader.Input.EncoderReleased encoderReleased: { 118 | if (Inputs[encoderOffset + encoderReleased.encoder] is StreamDeckEncoder encoder) 119 | encoder.CallReleased(); 120 | 121 | break; 122 | } 123 | 124 | case DeviceReader.Input.EncoderTwist encoderTwist: { 125 | if (Inputs[encoderOffset + encoderTwist.encoder] is StreamDeckEncoder encoder) 126 | encoder.CallTwist(encoderTwist.value); 127 | 128 | break; 129 | } 130 | 131 | case DeviceReader.Input.TouchScreenPress touchScreenPress: { 132 | if (Inputs[lcdIndex] is StreamDeckLCDSegment segment) { 133 | segment.CallPressed(new Int2(touchScreenPress.X, touchScreenPress.Y)); 134 | segment.CallReleased(new Int2(touchScreenPress.X, touchScreenPress.Y)); 135 | } 136 | 137 | break; 138 | } 139 | 140 | case DeviceReader.Input.TouchScreenLongPress touchScreenPress: { 141 | if (Inputs[lcdIndex] is StreamDeckLCDSegment segment) 142 | Task.Run( 143 | async () => { 144 | segment.CallPressed(new Int2(touchScreenPress.X, touchScreenPress.Y)); 145 | await Task.Delay(TimeSpan.FromSeconds(1)); 146 | segment.CallReleased(new Int2(touchScreenPress.X, touchScreenPress.Y)); 147 | } 148 | ); 149 | 150 | break; 151 | } 152 | 153 | case DeviceReader.Input.TouchScreenSwipe touchScreenSwipe: { 154 | if (Inputs[lcdIndex] is StreamDeckLCDSegment segment) 155 | segment.CallDrag( 156 | new Int2(touchScreenSwipe.StartX, touchScreenSwipe.StartY), 157 | new Int2(touchScreenSwipe.EndX, touchScreenSwipe.EndY) 158 | ); 159 | 160 | break; 161 | } 162 | } 163 | } catch (HidException e) { 164 | if (e.Message.Contains("Input/output error")) 165 | Die(); 166 | else 167 | throw; 168 | } 169 | } 170 | 171 | internal void SetCache(long key, byte[] data) { 172 | _imageCache.Set(key, data, _imageCacheEntryOptions); 173 | } 174 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/StreamDeckDeviceOptions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using Streamduck.Attributes; 6 | 7 | namespace StreamduckStreamDeck; 8 | 9 | public class StreamDeckDeviceOptions { 10 | [Header("Screen Controls")] 11 | [Description("Adjusts screen brightness of the device")] 12 | public int ScreenBrightness { get; set; } = 100; 13 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/StreamDeckDriver.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using ElgatoStreamDeck; 9 | using Streamduck.Devices; 10 | using Streamduck.Interfaces; 11 | using Streamduck.Plugins; 12 | using Device = Streamduck.Devices.Device; 13 | 14 | namespace StreamduckStreamDeck; 15 | 16 | public class StreamDeckDriver(DeviceManager manager) : Driver, IConfigurable { 17 | private const string StreamDeckOriginalDesc = "Stream Deck Original"; 18 | private const string StreamDeckOriginalV2Desc = "Stream Deck Original V2"; 19 | private const string StreamDeckMiniDesc = "Stream Deck Mini"; 20 | private const string StreamDeckXlDesc = "Stream Deck XL"; 21 | private const string StreamDeckXlV2Desc = "Stream Deck XL V2"; 22 | private const string StreamDeckMk2Desc = "Stream Deck Mk2"; 23 | private const string StreamDeckMiniMk2Desc = "Stream Deck Mini Mk2"; 24 | private const string StreamDeckPedalDesc = "Stream Deck Pedal"; 25 | private const string StreamDeckPlusDesc = "Stream Deck Plus"; 26 | private const string StreamDeckUnknownDesc = "Unknown"; 27 | 28 | public override string Name => "Stream Deck Driver"; 29 | 30 | public StreamDeckDeviceOptions Config { get; set; } = new(); 31 | 32 | public override Task> ListDevices() { 33 | return Task.FromResult( 34 | manager.ListDevices() 35 | .Where(t => IsValid(t.Item1)) 36 | .Select(t => new DeviceIdentifier(t.Item2, KindToDescription(t.Item1))) 37 | ); 38 | } 39 | 40 | public override Task ConnectDevice(DeviceIdentifier identifier) { 41 | var device = manager.ConnectDeviceConcurrent(DescriptionToKind(identifier.Description), identifier.Identifier); 42 | return Task.FromResult(new StreamDeckDevice(device, identifier) as Device); 43 | } 44 | 45 | private static bool IsValid(Kind kind) => kind != Kind.Unknown; 46 | 47 | internal static string KindToDescription(Kind kind) { 48 | return kind switch { 49 | Kind.Original => StreamDeckOriginalDesc, 50 | Kind.OriginalV2 => StreamDeckOriginalV2Desc, 51 | Kind.Mini => StreamDeckMiniDesc, 52 | Kind.Xl => StreamDeckXlDesc, 53 | Kind.XlV2 => StreamDeckXlV2Desc, 54 | Kind.Mk2 => StreamDeckMk2Desc, 55 | Kind.MiniMk2 => StreamDeckMiniMk2Desc, 56 | Kind.Pedal => StreamDeckPedalDesc, 57 | Kind.Plus => StreamDeckPlusDesc, 58 | _ => StreamDeckUnknownDesc 59 | }; 60 | } 61 | 62 | internal static Kind DescriptionToKind(string desc) { 63 | return desc switch { 64 | StreamDeckOriginalDesc => Kind.Original, 65 | StreamDeckOriginalV2Desc => Kind.OriginalV2, 66 | StreamDeckMiniDesc => Kind.Mini, 67 | StreamDeckXlDesc => Kind.Xl, 68 | StreamDeckXlV2Desc => Kind.XlV2, 69 | StreamDeckMk2Desc => Kind.Mk2, 70 | StreamDeckMiniMk2Desc => Kind.MiniMk2, 71 | StreamDeckPedalDesc => Kind.Pedal, 72 | StreamDeckPlusDesc => Kind.Plus, 73 | _ => Kind.Unknown 74 | }; 75 | } 76 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/StreamDeckPlugin.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using ElgatoStreamDeck; 9 | using Streamduck; 10 | using Streamduck.Attributes; 11 | using Streamduck.Plugins; 12 | 13 | namespace StreamduckStreamDeck; 14 | 15 | public class StreamDeckPlugin : Plugin, IDisposable { 16 | private readonly DeviceManager _manager = DeviceManager.Get(); 17 | 18 | private readonly StreamDeckDriver driver; 19 | 20 | public StreamDeckPlugin() => driver = new StreamDeckDriver(_manager); 21 | 22 | public override string Name => "Stream Deck Plugin"; 23 | 24 | public override IEnumerable Drivers => 25 | new Driver[] { 26 | driver 27 | }; 28 | 29 | public void Dispose() { 30 | _manager.Dispose(); 31 | GC.SuppressFinalize(this); 32 | } 33 | 34 | [PluginMethod] 35 | public void MyAction() { } 36 | 37 | public override Task OnPluginsLoaded(IStreamduck pluginQuery) { 38 | Console.WriteLine($"Driver has {driver.Config.ScreenBrightness} for brightness"); 39 | 40 | return Task.CompletedTask; 41 | } 42 | } -------------------------------------------------------------------------------- /StreamduckStreamDeck/StreamduckStreamDeck.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | disable 7 | enable 8 | 9 | 10 | 11 | 12 | false 13 | runtime 14 | 15 | 16 | 17 | false 18 | runtime 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /StreamduckTest/PluginReflectorTest.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Threading.Tasks; 9 | using Streamduck.Actions; 10 | using Streamduck.Attributes; 11 | using Streamduck.Plugins; 12 | using Streamduck.Plugins.Methods; 13 | 14 | namespace StreamduckTest; 15 | 16 | [TestFixture] 17 | [SuppressMessage("Assertion", "NUnit2045:Use Assert.Multiple")] 18 | public class PluginReflectorTest { 19 | private class TestPlugin : Plugin { 20 | public override string Name => "Test Plugin"; 21 | 22 | public bool ActionWasCalled { get; private set; } 23 | public int Counter { get; private set; } 24 | 25 | [PluginMethod] 26 | public async Task TestAction() { 27 | await Task.Delay(3); 28 | ActionWasCalled = true; 29 | } 30 | 31 | [PluginMethod] 32 | public async Task TestOptionAction(TestOptions options) { 33 | await Task.Delay(1); 34 | options.Count++; 35 | Counter = options.Count; 36 | } 37 | 38 | [PluginMethod] 39 | public async Task TestConfigurableAction(TestOptions options, TestConfig config) { 40 | await Task.Delay(1); 41 | options.Count++; 42 | config.IncrementsPerformed++; 43 | Counter = options.Count; 44 | } 45 | 46 | public class TestOptions { 47 | public int Count { get; set; } 48 | } 49 | 50 | public class TestConfig { 51 | public int IncrementsPerformed { get; set; } 52 | } 53 | } 54 | 55 | [Test] 56 | public async Task TestActions() { 57 | var plugin = new TestPlugin(); 58 | 59 | var methods = PluginReflector.GetMethods(plugin); 60 | using var actions = PluginReflector.AnalyzeActions(methods, plugin).GetEnumerator(); 61 | 62 | { 63 | // Test Action 64 | var action = AnalyzeActionInfo(actions, "Test Action"); 65 | 66 | await action.Invoke(await action.DefaultData()); 67 | Assert.That(plugin.ActionWasCalled, Is.True, "Action was not properly called"); 68 | } 69 | 70 | { 71 | // Test Action with option parameter 72 | var action = AnalyzeActionInfo(actions, "Test Option Action"); 73 | 74 | var data = await action.DefaultData(); 75 | Assert.That( 76 | action, Is.InstanceOf>(), 77 | "Action wasn't recognized as correct type" 78 | ); 79 | Assert.That(data, Is.InstanceOf(), "Default data wasn't the correct type"); 80 | 81 | await action.Invoke(data); 82 | Assert.That(plugin.Counter, Is.EqualTo(1), "Action was not properly called or options didn't increment"); 83 | 84 | await action.Invoke(data); 85 | Assert.That(plugin.Counter, Is.EqualTo(2), "Action was not properly called or options didn't increment"); 86 | 87 | var castedData = (TestPlugin.TestOptions)data; 88 | Assert.That(castedData, Has.Count.EqualTo(2), "Action options weren't updated properly"); 89 | } 90 | 91 | { 92 | // Test Configurable Action 93 | var action = AnalyzeActionInfo(actions, "Test Configurable Action"); 94 | 95 | var data = await action.DefaultData(); 96 | Assert.That( 97 | action, 98 | Is.InstanceOf>(), 99 | "Action wasn't recognized as correct type" 100 | ); 101 | Assert.That(data, Is.InstanceOf(), "Default data wasn't the correct type"); 102 | 103 | await action.Invoke(data); 104 | Assert.That(plugin.Counter, Is.EqualTo(1), "Action was not properly called or options didn't increment"); 105 | 106 | await action.Invoke(data); 107 | Assert.That(plugin.Counter, Is.EqualTo(2), "Action was not properly called or options didn't increment"); 108 | 109 | var castedData = (TestPlugin.TestOptions)data; 110 | Assert.That(castedData, Has.Count.EqualTo(2), "Action options weren't updated properly"); 111 | 112 | var configurable = (ConfigurableReflectedAction)action; 113 | Assert.That( 114 | configurable.Config.IncrementsPerformed, Is.EqualTo(2), 115 | "Action config weren't maintained properly" 116 | ); 117 | } 118 | } 119 | 120 | private static PluginAction AnalyzeActionInfo(IEnumerator enumerator, string name, 121 | string? description = null 122 | ) { 123 | Console.WriteLine($"Testing action '{name}'"); 124 | 125 | Assert.That(enumerator.MoveNext(), Is.True, $"Action '{name}' wasn't returned by reflector"); 126 | var action = enumerator.Current; 127 | 128 | AssertInfo(action.Name, action.Description, name, description, "Action"); 129 | 130 | return action; 131 | } 132 | 133 | private static void AssertInfo(string actualName, string? actualDescription, string name, string? description, 134 | string logPrefix 135 | ) { 136 | Assert.That(actualName, Is.EqualTo(name), $"{logPrefix} '{name}' had invalid title"); 137 | 138 | if (description != null) 139 | Assert.That(actualDescription, Is.EqualTo(description), $"{logPrefix} '{name}' had invalid description"); 140 | } 141 | } -------------------------------------------------------------------------------- /StreamduckTest/StreamduckTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /StreamduckTest/Usings.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | global using NUnit.Framework; -------------------------------------------------------------------------------- /streamduck-sharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streamduck", "Streamduck\Streamduck.csproj", "{CDAC1480-E220-4130-869D-90CA7949F504}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamduckShared", "StreamduckShared\StreamduckShared.csproj", "{EC48B16B-1049-43E7-A968-692800075797}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamduckStreamDeck", "StreamduckStreamDeck\StreamduckStreamDeck.csproj", "{624D1E4C-B4E0-40BF-A898-377C765B06F1}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElgatoStreamDeck", "ElgatoStreamDeck\ElgatoStreamDeck.csproj", "{C7AF0436-A9EE-4E4B-B209-2241F5FC7CB3}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamduckTest", "StreamduckTest\StreamduckTest.csproj", "{C15E56D7-E41B-48F5-BE18-BC061B2F7C46}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamduckGS", "StreamduckGS\StreamduckGS.csproj", "{FE4EAE30-5D1E-42E7-84F5-0EA2E2837041}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {CDAC1480-E220-4130-869D-90CA7949F504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {CDAC1480-E220-4130-869D-90CA7949F504}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {CDAC1480-E220-4130-869D-90CA7949F504}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {CDAC1480-E220-4130-869D-90CA7949F504}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {EC48B16B-1049-43E7-A968-692800075797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {EC48B16B-1049-43E7-A968-692800075797}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {EC48B16B-1049-43E7-A968-692800075797}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {EC48B16B-1049-43E7-A968-692800075797}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {624D1E4C-B4E0-40BF-A898-377C765B06F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {624D1E4C-B4E0-40BF-A898-377C765B06F1}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {624D1E4C-B4E0-40BF-A898-377C765B06F1}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {624D1E4C-B4E0-40BF-A898-377C765B06F1}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {C7AF0436-A9EE-4E4B-B209-2241F5FC7CB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {C7AF0436-A9EE-4E4B-B209-2241F5FC7CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {C7AF0436-A9EE-4E4B-B209-2241F5FC7CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {C7AF0436-A9EE-4E4B-B209-2241F5FC7CB3}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {C15E56D7-E41B-48F5-BE18-BC061B2F7C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {C15E56D7-E41B-48F5-BE18-BC061B2F7C46}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {C15E56D7-E41B-48F5-BE18-BC061B2F7C46}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {C15E56D7-E41B-48F5-BE18-BC061B2F7C46}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {FE4EAE30-5D1E-42E7-84F5-0EA2E2837041}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {FE4EAE30-5D1E-42E7-84F5-0EA2E2837041}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {FE4EAE30-5D1E-42E7-84F5-0EA2E2837041}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {FE4EAE30-5D1E-42E7-84F5-0EA2E2837041}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /why-dotnet.md: -------------------------------------------------------------------------------- 1 | # Why I switched from Rust to C# 2 | 3 | ### Some behind the scenes first 4 | A lot of things happened from my last commit on the Streamduck repo master, I've decided to rewrite the software 5 | for several reasons: 6 | 7 | I didn't like how the current available version of Streamduck could only work with Elgato 8 | Stream Decks, I wanted to broaden the software into supporting any device that a plugin could bring. 9 | But with the current code base it basically meant I have to rewrite it, since the code was intertwined with the 10 | Stream Deck library... 11 | 12 | So I started the first rewrite of the software where the only change was a more generic interface through which 13 | plugins could implement drivers for any kind of device. But then the next problem happened... 14 | 15 | Rust does not have a stable ABI, the plugin implementation I've done is incredibly naive and it would had failed 16 | in many spectacular ways, since Rust compiler doesn't even guarantee that it's gonna do the same binary between 17 | compilations on the same compiler version. 18 | 19 | So second rewrite was focused to trying to make plugin support using C ABI, but using FFI like this brings a lot 20 | of other problems, like the entire interface between main application and plugins being made up out of unsafe code. 21 | I'd have to express my entire API in C ABI and make sure to not corrupt my memory at the same time, since memory 22 | allocated on plugin should be deallocated by the plugin... 23 | 24 | For last half a year I was trying to figure out how to even get there with the software design, and even if I did 25 | manage to get there, I'd have to make a lot of macros to make plugin development easy enough and every plugin would 26 | have to compile to multiple platforms, and distribute those binaries separately.. 27 | 28 | It's basically just a huge mess I didn't want to handle. So it brings us back to the topic: 29 | 30 | ### So why did I chose C#? 31 | 32 | Dotnet pretty natively supports running applications with plugins, making designing a plugin interface very trivial 33 | (they even have an [official guide](https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support) on this!). 34 | 35 | Binaries that dotnet compiles are cross platform, the .dll file of a plugin can be used on Windows, Linux and OSX without 36 | additional compilations or complications (unless your plugin uses native libraries, you'd have to compile those to platforms 37 | you'd want to support). 38 | 39 | Dotnet 7 is also very fast, it's incredibly optimized for being a semi-interpreted language. I've rewrite my Stream Deck 40 | library to C# and C# version managed to squeeze some extra frames out of my Stream Deck Plus while flipping between 2 41 | images with the library just writing already encoded JPEGs to the HID Device. 42 | 43 | Dotnet being faster might have been due to rust version allocating and deallocating some memory on each write, while C# 44 | version uses a buffer that it allocated once, and then some several small allocations of byte[] on every write. But it 45 | still proved to me that C# can be as fast as Rust (at least in my use case). 46 | 47 | I'm also still more familiar with OOP and how to design software around it, new features of C# make it a breeze as well. 48 | Like all the null-coalescing operators, and the Nullable feature where every type won't be null unless you suffix it with "?". 49 | Event dispatchers are also amazing, I don't have to make my own solution for that. 50 | 51 | Plugin development experience is going to be much simpler on C# though, since you won't have to deal with Rust's quirks 52 | like the borrow checker... On C#, all you have to do is define a class that implements Plugin class and it will 53 | automatically be loaded by Streamduck along with anything you specify in the Plugin properties, see StreamduckStreamDeck 54 | plugin for an example. 55 | 56 | ### Not everything is perfect however... 57 | C# does have some pitfalls, like the difference between reference types and value types and C# always copying data unless 58 | the data is readonly, or you're using a Span to pass it instead. 59 | 60 | Exceptions are also not explicitly listed like they are in Java, making error handling complicated, since you need to 61 | check source of the method you're calling and see if it throws anything in there, thankfully at least for Standard library, 62 | all exceptions that you should be concerned about are included into the doc comments. 63 | 64 | Resource usage of dotnet applications is also bigger most of the time compared to Rust, but to be honest, I'll take that 65 | instead of having to deal with Rust's FFI... 66 | 67 | ### Conclusion 68 | Despite some of the cons that dotnet has, I'll be able to actually finish this project in near future, instead of being 69 | stuck on all the difficulties that Rust has doing this kind of extensible project... 70 | 71 | The idea of this project is to be the foundation and infrastructure for using all kinds of macro devices to execute any 72 | kind of function, all implemented using plugins. 73 | 74 | I personally will be developing plugins for OBS Studio, support for upcoming [Framework 16 macro pad](https://frame.work/products/laptop16-diy-amd-7040?tab=modules), 75 | keyboard emulation plugin, NodeGraph scripting system, and also some default plugins that Streamduck will ship with to 76 | make it be able to do some things like being able to run commands and such. 77 | 78 | With C#, hopefully I'll have working software by the end of this year, with all the plugins I wanted to make --------------------------------------------------------------------------------