From a09931b7a817c3f8ee790d937fbf34417a26dd26 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Tue, 27 Feb 2024 08:32:38 +1300 Subject: [PATCH 01/95] OSCQuery auto discover send port --- ShockOsc/Config.cs | 2 - ShockOsc/OscClient.cs | 15 ++++-- ShockOsc/OscQueryLibrary/OscQueryModels.cs | 18 +++++++ ShockOsc/OscQueryLibrary/OscQueryServer.cs | 61 ++++++++++++++++++---- ShockOsc/ShockOsc.cs | 37 ++++++++++--- 5 files changed, 109 insertions(+), 24 deletions(-) diff --git a/ShockOsc/Config.cs b/ShockOsc/Config.cs index 3cac544..9178091 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/Config.cs @@ -80,7 +80,6 @@ public static void Save() { Chatbox = true, Hoscy = false, - SendPort = 9000, HoscySendPort = 9001 }, Chatbox = new Conf.ChatboxConf @@ -197,7 +196,6 @@ public class OscConf { public required bool Chatbox { get; init; } public required bool Hoscy { get; init; } - public required ushort SendPort { get; init; } public ushort HoscySendPort { get; init; } = 9001; } diff --git a/ShockOsc/OscClient.cs b/ShockOsc/OscClient.cs index 4f0d43a..afa0c52 100644 --- a/ShockOsc/OscClient.cs +++ b/ShockOsc/OscClient.cs @@ -7,7 +7,7 @@ namespace OpenShock.ShockOsc; public static class OscClient { - private static OscDuplex _gameConnection; + private static OscDuplex? _gameConnection; private static readonly OscSender HoscySenderClient = new(new IPEndPoint(IPAddress.Loopback, Config.ConfigInstance.Osc.HoscySendPort)); private static readonly ILogger Logger = Log.ForContext(typeof(OscClient)); @@ -17,9 +17,12 @@ static OscClient() Task.Run(HoscySenderLoop); } - public static void CreateGameConnection(ushort receivePort) + public static void CreateGameConnection(ushort receivePort, ushort sendPort) { - _gameConnection = new(new IPEndPoint(IPAddress.Loopback, receivePort), new IPEndPoint(IPAddress.Loopback, Config.ConfigInstance.Osc.SendPort)); + _gameConnection?.Dispose(); + _gameConnection = null; + Logger.Debug("Creating game connection with receive port {ReceivePort} and send port {SendPort}", receivePort, sendPort); + _gameConnection = new(new IPEndPoint(IPAddress.Loopback, receivePort), new IPEndPoint(IPAddress.Loopback, sendPort)); } private static readonly Channel GameSenderChannel = Channel.CreateUnbounded(new UnboundedChannelOptions() @@ -50,6 +53,7 @@ private static async Task GameSenderLoop() Logger.Debug("Starting game sender loop"); await foreach (var oscMessage in GameSenderChannel.Reader.ReadAllAsync()) { + if (_gameConnection == null) continue; try { await _gameConnection.SendAsync(oscMessage); @@ -77,5 +81,8 @@ private static async Task HoscySenderLoop() } } - public static Task ReceiveGameMessage() => _gameConnection.ReceiveMessageAsync(); + public static Task? ReceiveGameMessage() + { + return _gameConnection?.ReceiveMessageAsync(); + } } \ No newline at end of file diff --git a/ShockOsc/OscQueryLibrary/OscQueryModels.cs b/ShockOsc/OscQueryLibrary/OscQueryModels.cs index 3ff7476..299a26b 100644 --- a/ShockOsc/OscQueryLibrary/OscQueryModels.cs +++ b/ShockOsc/OscQueryLibrary/OscQueryModels.cs @@ -52,4 +52,22 @@ public class Node public string? TYPE { get; set; } public List? VALUE { get; set; } } + + public class HostInfo + { + public string NAME { get; set; } + public string OSC_IP { get; set; } + public int OSC_PORT { get; set; } + public string OSC_TRANSPORT { get; set; } + public Extensions EXTENSIONS { get; set; } + } + + public class Extensions + { + public bool ACCESS { get; set; } + public bool CLIPMODE { get; set; } + public bool RANGE { get; set; } + public bool TYPE { get; set; } + public bool VALUE { get; set; } + } } \ No newline at end of file diff --git a/ShockOsc/OscQueryLibrary/OscQueryServer.cs b/ShockOsc/OscQueryLibrary/OscQueryServer.cs index ec4a54e..52765dc 100644 --- a/ShockOsc/OscQueryLibrary/OscQueryServer.cs +++ b/ShockOsc/OscQueryLibrary/OscQueryServer.cs @@ -14,28 +14,32 @@ public class OscQueryServer : IDisposable private readonly ushort _httpPort; // TODO: remove when switching httpServer library for proper random port support private readonly string _ipAddress; - public static ushort OscPort; + public static ushort OscReceivePort; + public static ushort OscSendPort; private const string OscHttpServiceName = "_oscjson._tcp"; private const string OscUdpServiceName = "_osc._udp"; private readonly HttpListener _httpListener; private readonly MulticastService _multicastService; private readonly ServiceDiscovery _serviceDiscovery; private readonly string _serviceName; - private object? _hostInfo; + private OscQueryModels.HostInfo? _hostInfo; private object? _queryData; private static readonly HashSet FoundServices = new(); private static IPEndPoint? _lastVrcHttpServer; + private static event Action? FoundVrcClient; private static event Action>? ParameterUpdate; private static readonly Dictionary ParameterList = new(); public OscQueryServer(string serviceName, string ipAddress, + Action? foundVrcClient = null, Action>? parameterUpdate = null) { _serviceName = serviceName; _ipAddress = ipAddress; - OscPort = FindAvailableUdpPort(); + OscReceivePort = FindAvailableUdpPort(); _httpPort = FindAvailableTcpPort(); + FoundVrcClient = foundVrcClient; ParameterUpdate = parameterUpdate; SetupJsonObjects(); // ignore our own service @@ -67,7 +71,7 @@ private void AdvertiseOscQueryServer() new ServiceProfile(_serviceName, OscHttpServiceName, _httpPort, new[] { IPAddress.Parse(_ipAddress) }); var oscProfile = - new ServiceProfile(_serviceName, OscUdpServiceName, OscPort, + new ServiceProfile(_serviceName, OscUdpServiceName, OscReceivePort, new[] { IPAddress.Parse(_ipAddress) }); _serviceDiscovery.Advertise(httpProfile); _serviceDiscovery.Advertise(oscProfile); @@ -85,7 +89,7 @@ private void ListenForServices() _multicastService.AnswerReceived += OnAnswerReceived; } - private static void OnAnswerReceived(object? sender, MessageEventArgs args) + private void OnAnswerReceived(object? sender, MessageEventArgs args) { var response = args.Message; try @@ -117,8 +121,7 @@ private static void OnAnswerReceived(object? sender, MessageEventArgs args) if (instanceName.StartsWith("VRChat-Client-") && ipAddress != null) { - _lastVrcHttpServer = new IPEndPoint(ipAddress, record.Port); - FetchJsonFromVrc(ipAddress, record.Port).GetAwaiter(); + FoundNewVrcClient(ipAddress, record.Port).GetAwaiter(); } } } @@ -127,6 +130,42 @@ private static void OnAnswerReceived(object? sender, MessageEventArgs args) Logger.Debug("Failed to parse from {ArgsRemoteEndPoint}: {ExMessage}", args.RemoteEndPoint, ex.Message); } } + + private async Task FoundNewVrcClient(IPAddress ipAddress, int port) + { + _lastVrcHttpServer = new IPEndPoint(ipAddress, port); + await FetchOscSendPortFromVrc(ipAddress, port); + FoundVrcClient?.Invoke(); + await FetchJsonFromVrc(ipAddress, port); + } + + private async Task FetchOscSendPortFromVrc(IPAddress ipAddress, int port) + { + var url = $"http://{ipAddress}:{port}?HOST_INFO"; + Logger.Debug("OSCQueryHttpClient: Fetching OSC send port from {Url}", url); + var response = string.Empty; + var client = new HttpClient(); + try + { + response = await client.GetStringAsync(url); + var rootNode = JsonSerializer.Deserialize(response); + if (rootNode?.OSC_PORT == null) + { + Logger.Error("OSCQueryHttpClient: Error no OSC port found"); + return; + } + + OscSendPort = (ushort)rootNode.OSC_PORT; + } + catch (HttpRequestException ex) + { + Logger.Error("OSCQueryHttpClient: Error {ExMessage}", ex.Message); + } + catch (Exception ex) + { + Logger.Error("OSCQueryHttpClient: Error {ExMessage}\\n{Response}", ex.Message, response); + } + } private static bool _fetchInProgress; @@ -165,7 +204,7 @@ private static async Task FetchJsonFromVrc(IPAddress ipAddress, int port) } catch (Exception ex) { - Logger.Error("OSCQueryHttp: Error {ExMessage}\\n{Response}", ex.Message, response); + Logger.Error("OSCQueryHttpClient: Error {ExMessage}\\n{Response}", ex.Message, response); } finally { @@ -261,13 +300,13 @@ private void SetupJsonObjects() } }; - _hostInfo = new + _hostInfo = new OscQueryModels.HostInfo { NAME = _serviceName, - OSC_PORT = (int)OscPort, + OSC_PORT = OscReceivePort, OSC_IP = _ipAddress, OSC_TRANSPORT = "UDP", - EXTENSIONS = new + EXTENSIONS = new OscQueryModels.Extensions { ACCESS = true, CLIPMODE = true, diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 22cf213..a2d9085 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Globalization; using System.Reflection; +using LucHeart.CoreOSC; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.OscChangeTracker; using OpenShock.ShockOsc.OscQueryLibrary; @@ -16,6 +17,7 @@ namespace OpenShock.ShockOsc; public static class ShockOsc { private static ILogger _logger = null!; + private static bool _oscServerActive; private static bool _isAfk; private static bool _isMuted; private static readonly Random Random = new(); @@ -86,25 +88,36 @@ private static async Task Main(string[] args) _ = new OscQueryServer( "ShockOsc", // service name "127.0.0.1", // ip address for udp and http server + FoundVrcClient, // optional callback on vrc discovery OnAvatarChange // optional parameter list callback on vrc discovery ); - OscClient.CreateGameConnection(OscQueryServer.OscPort); + await Task.Delay(Timeout.Infinite).ConfigureAwait(false); + } + + private static void FoundVrcClient() + { + _logger.Information("Found VRC client"); + // stop tasks + _oscServerActive = false; + Task.Delay(1000).Wait(); // wait for tasks to stop + + OscClient.CreateGameConnection(OscQueryServer.OscReceivePort, OscQueryServer.OscSendPort); _logger.Information("Connecting UDP Clients..."); // Start tasks + _oscServerActive = true; OsTask.Run(ReceiverLoopAsync); OsTask.Run(SenderLoopAsync); OsTask.Run(CheckLoop); + Shockers.Clear(); Shockers.TryAdd("_All", new Shocker(Guid.Empty, "_All")); foreach (var (shockerName, shockerId) in Config.ConfigInstance.ShockLink.Shockers) Shockers.TryAdd(shockerName, new Shocker(shockerId, shockerName)); _logger.Information("Ready"); - OsTask.Run(UnderscoreConfig.SendUpdateForAll); - await Task.Delay(Timeout.Infinite).ConfigureAwait(false); } private static void OnAvatarChange(Dictionary? parameters) @@ -159,7 +172,7 @@ private static void OnAvatarChange(Dictionary? parameters) private static async Task ReceiverLoopAsync() { - while (true) + while (_oscServerActive) { try { @@ -175,7 +188,17 @@ private static async Task ReceiverLoopAsync() private static async Task ReceiveLogic() { - var received = await OscClient.ReceiveGameMessage(); + OscMessage received; + try + { + received = await OscClient.ReceiveGameMessage()!; + } + catch (Exception e) + { + _logger.Verbose(e, "Error receiving message"); + return; + } + var addr = received.Address; _logger.Verbose("Received message: {Addr}", addr); @@ -314,7 +337,7 @@ private static ValueTask LogIgnoredAfk() private static async Task SenderLoopAsync() { - while (true) + while (_oscServerActive) { await SendParams(); await Task.Delay(300); @@ -404,7 +427,7 @@ private static async Task SendParams() private static async Task CheckLoop() { - while (true) + while (_oscServerActive) { try { From 49db53bd9a1ed477a305ceed7db2bc4018753639 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Mon, 4 Mar 2024 10:33:00 +1300 Subject: [PATCH 02/95] OSCQuery Quest standalone support --- ShockOsc/Config.cs | 4 +- ShockOsc/OscClient.cs | 4 +- ShockOsc/OscQueryLibrary/OscQueryServer.cs | 62 ++++++++-------------- ShockOsc/ShockOsc.cs | 22 +++++++- ShockOsc/ShockOsc.csproj | 1 + 5 files changed, 48 insertions(+), 45 deletions(-) diff --git a/ShockOsc/Config.cs b/ShockOsc/Config.cs index 9178091..db2f38c 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/Config.cs @@ -80,7 +80,8 @@ public static void Save() { Chatbox = true, Hoscy = false, - HoscySendPort = 9001 + HoscySendPort = 9001, + QuestSupport = false }, Chatbox = new Conf.ChatboxConf { @@ -197,6 +198,7 @@ public class OscConf public required bool Chatbox { get; init; } public required bool Hoscy { get; init; } public ushort HoscySendPort { get; init; } = 9001; + public required bool QuestSupport { get; init; } } public class BehaviourConf diff --git a/ShockOsc/OscClient.cs b/ShockOsc/OscClient.cs index afa0c52..fec1ae4 100644 --- a/ShockOsc/OscClient.cs +++ b/ShockOsc/OscClient.cs @@ -17,12 +17,12 @@ static OscClient() Task.Run(HoscySenderLoop); } - public static void CreateGameConnection(ushort receivePort, ushort sendPort) + public static void CreateGameConnection(IPAddress ipAddress, ushort receivePort, ushort sendPort) { _gameConnection?.Dispose(); _gameConnection = null; Logger.Debug("Creating game connection with receive port {ReceivePort} and send port {SendPort}", receivePort, sendPort); - _gameConnection = new(new IPEndPoint(IPAddress.Loopback, receivePort), new IPEndPoint(IPAddress.Loopback, sendPort)); + _gameConnection = new(new IPEndPoint(ipAddress, receivePort), new IPEndPoint(ipAddress, sendPort)); } private static readonly Channel GameSenderChannel = Channel.CreateUnbounded(new UnboundedChannelOptions() diff --git a/ShockOsc/OscQueryLibrary/OscQueryServer.cs b/ShockOsc/OscQueryLibrary/OscQueryServer.cs index 52765dc..d349875 100644 --- a/ShockOsc/OscQueryLibrary/OscQueryServer.cs +++ b/ShockOsc/OscQueryLibrary/OscQueryServer.cs @@ -5,20 +5,22 @@ using MeaMod.DNS.Model; using MeaMod.DNS.Multicast; using Serilog; +using EmbedIO; +using EmbedIO.Actions; namespace OpenShock.ShockOsc.OscQueryLibrary; public class OscQueryServer : IDisposable { private static readonly ILogger Logger = Log.ForContext(typeof(OscQueryServer)); - - private readonly ushort _httpPort; // TODO: remove when switching httpServer library for proper random port support + + private readonly ushort _httpPort; private readonly string _ipAddress; + public static string OscIpAddress; public static ushort OscReceivePort; public static ushort OscSendPort; private const string OscHttpServiceName = "_oscjson._tcp"; private const string OscUdpServiceName = "_osc._udp"; - private readonly HttpListener _httpListener; private readonly MulticastService _multicastService; private readonly ServiceDiscovery _serviceDiscovery; private readonly string _serviceName; @@ -35,6 +37,8 @@ public OscQueryServer(string serviceName, string ipAddress, Action? foundVrcClient = null, Action>? parameterUpdate = null) { + Swan.Logging.Logger.NoLogging(); + _serviceName = serviceName; _ipAddress = ipAddress; OscReceivePort = FindAvailableUdpPort(); @@ -46,12 +50,18 @@ public OscQueryServer(string serviceName, string ipAddress, FoundServices.Add($"{_serviceName.ToLower()}.{OscHttpServiceName}.local:{_httpPort}"); // HTTP Server - _httpListener = new HttpListener(); - var prefix = $"http://{_ipAddress}:{_httpPort}/"; - _httpListener.Prefixes.Add(prefix); - _httpListener.Start(); - _httpListener.BeginGetContext(OnHttpRequest, null); - Logger.Debug("OSCQueryHttpServer: Listening at {Prefix}", prefix); + var url = $"http://{_ipAddress}:{_httpPort}/"; + var server = new WebServer(o => o + .WithUrlPrefix(url) + .WithMode(HttpListenerMode.EmbedIO)) + .WithModule(new ActionModule("/", HttpVerbs.Get, + ctx => ctx.SendStringAsync( + ctx.Request.RawUrl.Contains("HOST_INFO") + ? JsonSerializer.Serialize(_hostInfo) + : JsonSerializer.Serialize(_queryData), "application/json", Encoding.UTF8))); + + server.RunAsync(); + Logger.Debug("OSCQueryHttpServer: Listening at {Prefix}", url); // mDNS _multicastService = new MulticastService @@ -156,6 +166,7 @@ private async Task FetchOscSendPortFromVrc(IPAddress ipAddress, int port) } OscSendPort = (ushort)rootNode.OSC_PORT; + OscIpAddress = rootNode.OSC_IP; } catch (HttpRequestException ex) { @@ -183,7 +194,7 @@ private static async Task FetchJsonFromVrc(IPAddress ipAddress, int port) var rootNode = JsonSerializer.Deserialize(response); if (rootNode?.CONTENTS?.avatar?.CONTENTS?.parameters?.CONTENTS == null) { - Logger.Error("OSCQueryHttpClient: Error no parameters found"); + Logger.Debug("OSCQueryHttpClient: Error no parameters found"); return; } @@ -226,35 +237,6 @@ private static void RecursiveParameterLookup(OscQueryModels.Node node) } } - private async void OnHttpRequest(IAsyncResult result) - { - var context = _httpListener.EndGetContext(result); - _httpListener.BeginGetContext(OnHttpRequest, null); - var request = context.Request; - var response = context.Response; - var path = request.Url?.AbsolutePath; - if (path == null || request.RawUrl == null) - return; - - if (!request.RawUrl.Contains("HOST_INFO") && path != "/") - { - response.StatusCode = 404; - response.StatusDescription = "Not Found"; - response.Close(); - return; - } - - Logger.Debug("OSCQueryHttp request: {Path}", path); - - var json = JsonSerializer.Serialize(request.RawUrl.Contains("HOST_INFO") ? _hostInfo : _queryData); - response.Headers.Add("pragma:no-cache"); - response.ContentType = "application/json"; - var buffer = Encoding.UTF8.GetBytes(json); - response.ContentLength64 = buffer.Length; - await response.OutputStream.WriteAsync(buffer); - response.OutputStream.Close(); - } - public static async Task GetParameters() { if (_lastVrcHttpServer == null) @@ -322,8 +304,6 @@ public void Dispose() GC.SuppressFinalize(this); _multicastService.Dispose(); _serviceDiscovery.Dispose(); - _httpListener.Stop(); - _httpListener.Close(); } ~OscQueryServer() diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index a2d9085..0f1b2e1 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Globalization; +using System.Net; using System.Reflection; using LucHeart.CoreOSC; using OpenShock.ShockOsc.Models; @@ -91,6 +92,25 @@ private static async Task Main(string[] args) FoundVrcClient, // optional callback on vrc discovery OnAvatarChange // optional parameter list callback on vrc discovery ); + + // listen for VRC on every network interface + if (Config.ConfigInstance.Osc.QuestSupport) + { + var host = await Dns.GetHostEntryAsync(Dns.GetHostName()); + foreach (var ip in host.AddressList) + { + if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + continue; + + var ipAddress = ip.ToString(); + _ = new OscQueryServer( + "ShockOsc", // service name + ipAddress, // ip address for udp and http server + FoundVrcClient, // optional callback on vrc discovery + OnAvatarChange // parameter list callback on vrc discovery + ); + } + } await Task.Delay(Timeout.Infinite).ConfigureAwait(false); } @@ -102,7 +122,7 @@ private static void FoundVrcClient() _oscServerActive = false; Task.Delay(1000).Wait(); // wait for tasks to stop - OscClient.CreateGameConnection(OscQueryServer.OscReceivePort, OscQueryServer.OscSendPort); + OscClient.CreateGameConnection(IPAddress.Parse(OscQueryServer.OscIpAddress), OscQueryServer.OscReceivePort, OscQueryServer.OscSendPort); _logger.Information("Connecting UDP Clients..."); // Start tasks diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 8b7b4ce..7abfe49 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -18,6 +18,7 @@ + From fc1c27d42513d3cb1f6c70f0cd8632bf84c02728 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Wed, 6 Mar 2024 13:22:23 +1300 Subject: [PATCH 03/95] Ui init --- ShockOsc.sln | 9 +- ShockOsc/Config.cs | 41 +++-- ShockOsc/LoggerSink.cs | 75 +++++++++ ShockOsc/MauiProgram.cs | 25 +++ ShockOsc/Models/JsonRange.cs | 2 +- .../Platforms/Android/AndroidManifest.xml | 6 + ShockOsc/Platforms/Android/MainActivity.cs | 12 ++ ShockOsc/Platforms/Android/MainApplication.cs | 15 ++ .../Android/Resources/values/colors.xml | 6 + ShockOsc/Platforms/MacCatalyst/AppDelegate.cs | 9 ++ ShockOsc/Platforms/MacCatalyst/Info.plist | 30 ++++ ShockOsc/Platforms/MacCatalyst/Program.cs | 15 ++ ShockOsc/Platforms/Tizen/Main.cs | 16 ++ ShockOsc/Platforms/Tizen/tizen-manifest.xml | 15 ++ ShockOsc/Platforms/Windows/App.xaml | 8 + ShockOsc/Platforms/Windows/App.xaml.cs | 23 +++ .../Platforms/Windows/Package.appxmanifest | 52 ++++++ ShockOsc/Platforms/Windows/app.manifest | 15 ++ ShockOsc/Platforms/iOS/AppDelegate.cs | 9 ++ ShockOsc/Platforms/iOS/Info.plist | 32 ++++ ShockOsc/Platforms/iOS/Program.cs | 15 ++ ShockOsc/Properties/launchSettings.json | 8 + ShockOsc/Resources/Icon512.png | Bin 0 -> 25450 bytes ShockOsc/Resources/Logo512.png | Bin 0 -> 121558 bytes ShockOsc/ShockOsc.cs | 54 ++++--- ShockOsc/ShockOsc.csproj | 49 +++++- ShockOsc/Ui/App.xaml | 26 +++ ShockOsc/Ui/App.xaml.cs | 13 ++ ShockOsc/Ui/Components/Layout/Login.razor | 12 ++ ShockOsc/Ui/Components/Layout/Logo.razor | 8 + ShockOsc/Ui/Components/MainLayout.razor | 152 ++++++++++++++++++ ShockOsc/Ui/Main.razor | 12 ++ ShockOsc/Ui/MainPage.xaml | 15 ++ ShockOsc/Ui/MainPage.xaml.cs | 9 ++ ShockOsc/Ui/_Imports.razor | 8 + ShockOsc/wwwroot/app.css | 35 ++++ .../poppins-latin-400-normal.405055dd..woff2 | Bin 0 -> 7884 bytes .../poppins-latin-400-normal.cda9c93f..woff | Bin 0 -> 10528 bytes ShockOsc/wwwroot/images/Icon.svg | 23 +++ ShockOsc/wwwroot/images/Icon512.png | Bin 0 -> 25450 bytes ShockOsc/wwwroot/images/Icon64.png | Bin 0 -> 2772 bytes ShockOsc/wwwroot/images/IconLoadingSpin.svg | 21 +++ ShockOsc/wwwroot/images/Logo512.png | Bin 0 -> 121558 bytes ShockOsc/wwwroot/index.html | 30 ++++ 44 files changed, 860 insertions(+), 45 deletions(-) create mode 100644 ShockOsc/LoggerSink.cs create mode 100644 ShockOsc/MauiProgram.cs create mode 100644 ShockOsc/Platforms/Android/AndroidManifest.xml create mode 100644 ShockOsc/Platforms/Android/MainActivity.cs create mode 100644 ShockOsc/Platforms/Android/MainApplication.cs create mode 100644 ShockOsc/Platforms/Android/Resources/values/colors.xml create mode 100644 ShockOsc/Platforms/MacCatalyst/AppDelegate.cs create mode 100644 ShockOsc/Platforms/MacCatalyst/Info.plist create mode 100644 ShockOsc/Platforms/MacCatalyst/Program.cs create mode 100644 ShockOsc/Platforms/Tizen/Main.cs create mode 100644 ShockOsc/Platforms/Tizen/tizen-manifest.xml create mode 100644 ShockOsc/Platforms/Windows/App.xaml create mode 100644 ShockOsc/Platforms/Windows/App.xaml.cs create mode 100644 ShockOsc/Platforms/Windows/Package.appxmanifest create mode 100644 ShockOsc/Platforms/Windows/app.manifest create mode 100644 ShockOsc/Platforms/iOS/AppDelegate.cs create mode 100644 ShockOsc/Platforms/iOS/Info.plist create mode 100644 ShockOsc/Platforms/iOS/Program.cs create mode 100644 ShockOsc/Properties/launchSettings.json create mode 100644 ShockOsc/Resources/Icon512.png create mode 100644 ShockOsc/Resources/Logo512.png create mode 100644 ShockOsc/Ui/App.xaml create mode 100644 ShockOsc/Ui/App.xaml.cs create mode 100644 ShockOsc/Ui/Components/Layout/Login.razor create mode 100644 ShockOsc/Ui/Components/Layout/Logo.razor create mode 100644 ShockOsc/Ui/Components/MainLayout.razor create mode 100644 ShockOsc/Ui/Main.razor create mode 100644 ShockOsc/Ui/MainPage.xaml create mode 100644 ShockOsc/Ui/MainPage.xaml.cs create mode 100644 ShockOsc/Ui/_Imports.razor create mode 100644 ShockOsc/wwwroot/app.css create mode 100644 ShockOsc/wwwroot/fonts/poppins-latin-400-normal.405055dd..woff2 create mode 100644 ShockOsc/wwwroot/fonts/poppins-latin-400-normal.cda9c93f..woff create mode 100644 ShockOsc/wwwroot/images/Icon.svg create mode 100644 ShockOsc/wwwroot/images/Icon512.png create mode 100644 ShockOsc/wwwroot/images/Icon64.png create mode 100644 ShockOsc/wwwroot/images/IconLoadingSpin.svg create mode 100644 ShockOsc/wwwroot/images/Logo512.png create mode 100644 ShockOsc/wwwroot/index.html diff --git a/ShockOsc.sln b/ShockOsc.sln index 33a9847..de7fa67 100644 --- a/ShockOsc.sln +++ b/ShockOsc.sln @@ -1,6 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShockOsc", "ShockOsc\ShockOsc.csproj", "{D3C13FCF-0FF6-45E1-AA57-0EDF1AB8C48B}" +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShockOsc", "ShockOsc\ShockOsc.csproj", "{D3C13FCF-0FF6-45E1-AA57-0EDF1AB8C48B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -10,7 +13,11 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {D3C13FCF-0FF6-45E1-AA57-0EDF1AB8C48B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D3C13FCF-0FF6-45E1-AA57-0EDF1AB8C48B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3C13FCF-0FF6-45E1-AA57-0EDF1AB8C48B}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {D3C13FCF-0FF6-45E1-AA57-0EDF1AB8C48B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3C13FCF-0FF6-45E1-AA57-0EDF1AB8C48B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal diff --git a/ShockOsc/Config.cs b/ShockOsc/Config.cs index db2f38c..d51aee1 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/Config.cs @@ -9,7 +9,7 @@ public static class Config { private static readonly ILogger Logger = Log.ForContext(typeof(Config)); private static Conf? _internalConfig; - private static readonly string Path = Directory.GetCurrentDirectory() + "/config.json"; + private static readonly string Path = AppDomain.CurrentDomain.BaseDirectory + "config.json"; public static Conf ConfigInstance => _internalConfig!; @@ -50,7 +50,6 @@ private static void TryLoad() _internalConfig = JsonSerializer.Deserialize(jsonNew, Options); Logger.Information("New configuration file generated! Please configure it!"); Logger.Information("Press any key to exit..."); - Console.ReadKey(); Environment.Exit(10); } @@ -195,26 +194,26 @@ public enum HoscyMessageType public class OscConf { - public required bool Chatbox { get; init; } - public required bool Hoscy { get; init; } - public ushort HoscySendPort { get; init; } = 9001; - public required bool QuestSupport { get; init; } + public required bool Chatbox { get; set; } + public required bool Hoscy { get; set; } + public ushort HoscySendPort { get; set; } = 9001; + public required bool QuestSupport { get; set; } } public class BehaviourConf { - public required bool RandomIntensity { get; init; } - public required bool RandomDuration { get; init; } - public required uint RandomDurationStep { get; init; } = 1000; - public required JsonRange DurationRange { get; init; } - public required JsonRange IntensityRange { get; init; } - public required byte FixedIntensity { get; init; } - public required uint FixedDuration { get; init; } - public required uint HoldTime { get; init; } - public required uint CooldownTime { get; init; } - public BoneHeldAction WhileBoneHeld { get; init; } = BoneHeldAction.Vibrate; - public bool DisableWhileAfk { get; init; } = true; - public bool ForceUnmute { get; init; } = false; + public required bool RandomIntensity { get; set; } + public required bool RandomDuration { get; set; } + public required uint RandomDurationStep { get; set; } = 1000; + public required JsonRange DurationRange { get; set; } + public required JsonRange IntensityRange { get; set; } + public required byte FixedIntensity { get; set; } + public required uint FixedDuration { get; set; } + public required uint HoldTime { get; set; } + public required uint CooldownTime { get; set; } + public BoneHeldAction WhileBoneHeld { get; set; } = BoneHeldAction.Vibrate; + public bool DisableWhileAfk { get; set; } = true; + public bool ForceUnmute { get; set; } = false; public enum BoneHeldAction { @@ -226,9 +225,9 @@ public enum BoneHeldAction public class OpenShockConf { - public Uri UserHub { get; init; } = new("https://api.shocklink.net/1/hubs/user"); - public required string ApiToken { get; init; } - public required IReadOnlyDictionary Shockers { get; init; } + public Uri UserHub { get; set; } = new("https://api.shocklink.net/1/hubs/user"); + public required string ApiToken { get; set; } + public required IReadOnlyDictionary Shockers { get; set; } } } } \ No newline at end of file diff --git a/ShockOsc/LoggerSink.cs b/ShockOsc/LoggerSink.cs new file mode 100644 index 0000000..520ebeb --- /dev/null +++ b/ShockOsc/LoggerSink.cs @@ -0,0 +1,75 @@ +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting.Display; +using Serilog.Formatting; +using System.Diagnostics; + +namespace Serilog; + +public class LogStore +{ + public static List Logs = new(); + + public static void AddLog(string log) + { + if (Logs.Count == 0) + { + Logs.Add(new LogEntry { Time = DateTime.Now, Message = log }); + return; + } + // add to start of list + Logs.Insert(0, new LogEntry { Time = DateTime.Now, Message = log }); + } + + public class LogEntry + { + public DateTime Time { get; set; } + public string Message { get; set; } + } +} + +public class MySink : ILogEventSink +{ + private TextWriter _textWriter; + private readonly ITextFormatter _formatProvider; + // public Action? EmitAction { get; set; } + + public MySink(ITextFormatter formatProvider) + { + SinkExtensions.Instance = this; + _formatProvider = formatProvider; + } + + public void Emit(LogEvent logEvent) + { + _textWriter = new StringWriter(); + _formatProvider.Format(logEvent, _textWriter); + // var logMessage = logEvent.RenderMessage(_formatProvider); + Debug.WriteLine(_textWriter); + // EmitAction?.Invoke(_textWriter.ToString()); + LogStore.AddLog(_textWriter.ToString()); + _textWriter.Flush(); + } +} + +public static class SinkExtensions +{ + public static MySink? Instance; + + const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"; + + public static LoggerConfiguration MySink( + this LoggerSinkConfiguration sinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string outputTemplate = DefaultOutputTemplate, + IFormatProvider formatProvider = null, + LoggingLevelSwitch levelSwitch = null) + { + if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); + + var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); + var sink = new MySink(formatter); + return sinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); + } +} \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs new file mode 100644 index 0000000..31fc757 --- /dev/null +++ b/ShockOsc/MauiProgram.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Logging; +using MudBlazor.Services; + +namespace ShockOsc; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); + + builder.Services.AddMudServices(); + builder.Services.AddMauiBlazorWebView(); + +#if DEBUG + builder.Services.AddBlazorWebViewDeveloperTools(); + builder.Logging.AddDebug(); +#endif + + return builder.Build(); + } +} \ No newline at end of file diff --git a/ShockOsc/Models/JsonRange.cs b/ShockOsc/Models/JsonRange.cs index 2006781..67b1dec 100644 --- a/ShockOsc/Models/JsonRange.cs +++ b/ShockOsc/Models/JsonRange.cs @@ -1,7 +1,7 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global namespace OpenShock.ShockOsc.Models; -public struct JsonRange +public class JsonRange { public required uint Min { get; set; } public required uint Max { get; set; } diff --git a/ShockOsc/Platforms/Android/AndroidManifest.xml b/ShockOsc/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..dbf9e7e --- /dev/null +++ b/ShockOsc/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ShockOsc/Platforms/Android/MainActivity.cs b/ShockOsc/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..552f15e --- /dev/null +++ b/ShockOsc/Platforms/Android/MainActivity.cs @@ -0,0 +1,12 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace MauiApp1; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, + ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | + ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ +} \ No newline at end of file diff --git a/ShockOsc/Platforms/Android/MainApplication.cs b/ShockOsc/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..347b31d --- /dev/null +++ b/ShockOsc/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace ShockOsc; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/ShockOsc/Platforms/Android/Resources/values/colors.xml b/ShockOsc/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/ShockOsc/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/ShockOsc/Platforms/MacCatalyst/AppDelegate.cs b/ShockOsc/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 0000000..3fca2bd --- /dev/null +++ b/ShockOsc/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace ShockOsc; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/ShockOsc/Platforms/MacCatalyst/Info.plist b/ShockOsc/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..403ce9c --- /dev/null +++ b/ShockOsc/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,30 @@ + + + + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/ShockOsc/Platforms/MacCatalyst/Program.cs b/ShockOsc/Platforms/MacCatalyst/Program.cs new file mode 100644 index 0000000..efe47bf --- /dev/null +++ b/ShockOsc/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace ShockOsc; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/ShockOsc/Platforms/Tizen/Main.cs b/ShockOsc/Platforms/Tizen/Main.cs new file mode 100644 index 0000000..07507af --- /dev/null +++ b/ShockOsc/Platforms/Tizen/Main.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Maui; +using Microsoft.Maui.Hosting; + +namespace MauiApp1; + +class Program : MauiApplication +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + + static void Main(string[] args) + { + var app = new Program(); + app.Run(args); + } +} diff --git a/ShockOsc/Platforms/Tizen/tizen-manifest.xml b/ShockOsc/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 0000000..8e1d506 --- /dev/null +++ b/ShockOsc/Platforms/Tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + maui-appicon-placeholder + + + + + http://tizen.org/privilege/internet + + + + \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/App.xaml b/ShockOsc/Platforms/Windows/App.xaml new file mode 100644 index 0000000..b10c7df --- /dev/null +++ b/ShockOsc/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/ShockOsc/Platforms/Windows/App.xaml.cs b/ShockOsc/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..3ab885b --- /dev/null +++ b/ShockOsc/Platforms/Windows/App.xaml.cs @@ -0,0 +1,23 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace ShockOsc.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/Package.appxmanifest b/ShockOsc/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..4867aec --- /dev/null +++ b/ShockOsc/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,52 @@ + + + + + + + + + $placeholder$ + User Name + Resources\Icon512.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ShockOsc/Platforms/Windows/app.manifest b/ShockOsc/Platforms/Windows/app.manifest new file mode 100644 index 0000000..3ca1f8a --- /dev/null +++ b/ShockOsc/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/ShockOsc/Platforms/iOS/AppDelegate.cs b/ShockOsc/Platforms/iOS/AppDelegate.cs new file mode 100644 index 0000000..3fca2bd --- /dev/null +++ b/ShockOsc/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace ShockOsc; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/ShockOsc/Platforms/iOS/Info.plist b/ShockOsc/Platforms/iOS/Info.plist new file mode 100644 index 0000000..ecb7f71 --- /dev/null +++ b/ShockOsc/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/ShockOsc/Platforms/iOS/Program.cs b/ShockOsc/Platforms/iOS/Program.cs new file mode 100644 index 0000000..efe47bf --- /dev/null +++ b/ShockOsc/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace ShockOsc; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/ShockOsc/Properties/launchSettings.json b/ShockOsc/Properties/launchSettings.json new file mode 100644 index 0000000..edf8aad --- /dev/null +++ b/ShockOsc/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/ShockOsc/Resources/Icon512.png b/ShockOsc/Resources/Icon512.png new file mode 100644 index 0000000000000000000000000000000000000000..46a9e7c80722bdd59be65db01e4139f1f8e78ac1 GIT binary patch literal 25450 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 A82|tP literal 0 HcmV?d00001 diff --git a/ShockOsc/Resources/Logo512.png b/ShockOsc/Resources/Logo512.png new file mode 100644 index 0000000000000000000000000000000000000000..b310d8e5967d803379ade9e559bed2869e3f62e2 GIT binary patch literal 121558 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 yDxwKKe|Oz_S$zm&HF_8dul4Ky|I ParamsInUse = new(); + + public static Action? OnParamsChange; + + public static async Task StartMain(string[] args) { Log.Logger = new LoggerConfiguration() .Filter.ByExcluding(ev => ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")) - .WriteTo.Console(LogEventLevel.Information, - "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") + .WriteTo.MySink(LogEventLevel.Information, + "[{SourceContext}] {Message:lj} {NewLine}{Exception}") .CreateLogger(); // ReSharper disable once RedundantAssignment @@ -57,8 +61,8 @@ private static async Task Main(string[] args) .MinimumLevel.Debug() .Filter.ByExcluding(ev => ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")) - .WriteTo.Console(LogEventLevel.Debug, - "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") + .WriteTo.MySink(LogEventLevel.Debug, + "[{SourceContext}] {Message:lj} {NewLine}{Exception}") .CreateLogger(); } @@ -83,7 +87,7 @@ private static async Task Main(string[] args) _logger.Information("Found shockers: {Shockers}", Config.ConfigInstance.ShockLink.Shockers.Select(x => x.Key)); _logger.Information("Init user hub..."); - await UserHubClient.InitializeAsync(); + // await UserHubClient.InitializeAsync(); _logger.Information("Creating OSC Query Server..."); _ = new OscQueryServer( @@ -92,7 +96,7 @@ private static async Task Main(string[] args) FoundVrcClient, // optional callback on vrc discovery OnAvatarChange // optional parameter list callback on vrc discovery ); - + // listen for VRC on every network interface if (Config.ConfigInstance.Osc.QuestSupport) { @@ -114,7 +118,7 @@ private static async Task Main(string[] args) await Task.Delay(Timeout.Infinite).ConfigureAwait(false); } - + private static void FoundVrcClient() { _logger.Information("Found VRC client"); @@ -157,6 +161,8 @@ private static void OnAvatarChange(Dictionary? parameters) return; } + ParamsInUse.Clear(); + foreach (var param in parameters.Keys) { if (!param.StartsWith("/avatar/parameters/ShockOsc/")) @@ -179,7 +185,11 @@ private static void OnAvatarChange(Dictionary? parameters) continue; } - if (ShockerParams.Contains(action)) parameterCount++; + if (ShockerParams.Contains(action)) + { + parameterCount++; + ParamsInUse.TryAdd(paramName, parameters[param]); + } } _logger.Information("Loaded avatar config with {ParamCount} parameters", parameterCount); @@ -270,6 +280,14 @@ private static async Task ReceiveLogic() _logger.Debug("Param: {Param}", pos); return; } + + if (ParamsInUse.ContainsKey(pos)) + { + ParamsInUse[pos] = received.Arguments[0]; + OnParamsChange(); + } + else + ParamsInUse.TryAdd(pos, received.Arguments[0]); var shocker = Shockers[shockerName]; @@ -313,7 +331,7 @@ private static async Task ReceiveLogic() shocker.LastActive = DateTime.UtcNow; } else if (Config.ConfigInstance.Behaviour.WhileBoneHeld != - Config.Conf.BehaviourConf.BoneHeldAction.None) + Config.Conf.BehaviourConf.BoneHeldAction.None) { await CancelAction(shocker); } @@ -620,14 +638,14 @@ public static async Task RemoteActivateShocker(ControlLogSender sender, ControlL switch (log.Type) { case ControlType.Shock: - { - pain.LastIntensity = log.Intensity; - pain.LastDuration = log.Duration; - pain.LastExecuted = log.ExecutedAt; + { + pain.LastIntensity = log.Intensity; + pain.LastDuration = log.Duration; + pain.LastExecuted = log.ExecutedAt; - oneShock = true; - break; - } + oneShock = true; + break; + } case ControlType.Vibrate: pain.LastVibration = log.ExecutedAt; break; @@ -669,4 +687,4 @@ private static Task CancelAction(Shocker shocker) private static float LerpFloat(float min, float max, float t) => min + (max - min) * t; private static float ClampFloat(float value) => value < 0 ? 0 : value > 1 ? 1 : value; -} \ No newline at end of file +} diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 7abfe49..9cbc63c 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -1,10 +1,19 @@ - + Exe - net8.0 + + $(TargetFrameworks);net8.0-windows10.0.19041.0 + true + true enable + false enable + ShockOsc + com.companyname.shockosc + 2C147618-324E-4C37-B4B6-C50C8A9BD5ED + 1.0 + 1 OpenShock.ShockOsc OpenShock.ShockOsc OpenShock @@ -12,12 +21,34 @@ 1.8.3 Resources\openshock-icon.ico true - true ShockOsc - 12 + + 14.2 + 14.0 + 24.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + @@ -25,7 +56,17 @@ + + + + + PreserveNewest + + + PreserveNewest + + diff --git a/ShockOsc/Ui/App.xaml b/ShockOsc/Ui/App.xaml new file mode 100644 index 0000000..411f4f6 --- /dev/null +++ b/ShockOsc/Ui/App.xaml @@ -0,0 +1,26 @@ + + + + + + #512bdf + White + + + + + + + + \ No newline at end of file diff --git a/ShockOsc/Ui/App.xaml.cs b/ShockOsc/Ui/App.xaml.cs new file mode 100644 index 0000000..d870c3f --- /dev/null +++ b/ShockOsc/Ui/App.xaml.cs @@ -0,0 +1,13 @@ +namespace ShockOsc; + +public partial class App : Application +{ + public App() + { + _ = OpenShock.ShockOsc.ShockOsc.StartMain([ "--debug" ]); + + InitializeComponent(); + + MainPage = new MainPage(); + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Layout/Login.razor b/ShockOsc/Ui/Components/Layout/Login.razor new file mode 100644 index 0000000..2d8a39a --- /dev/null +++ b/ShockOsc/Ui/Components/Layout/Login.razor @@ -0,0 +1,12 @@ +@code { + public string? ApiKeyField { get; set; } +} + + + Login +
+ +
+ Continue +
+
\ No newline at end of file diff --git a/ShockOsc/Ui/Components/Layout/Logo.razor b/ShockOsc/Ui/Components/Layout/Logo.razor new file mode 100644 index 0000000..72fd587 --- /dev/null +++ b/ShockOsc/Ui/Components/Layout/Logo.razor @@ -0,0 +1,8 @@ + + + + + ShockOSC + + + \ No newline at end of file diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor new file mode 100644 index 0000000..f1c53ed --- /dev/null +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -0,0 +1,152 @@ +@using System.Text.Json.Serialization +@using System.Diagnostics +@using OpenShock.ShockOsc.Ui.Components.Layout +@using Serilog +@inherits LayoutComponentBase + + + + + +@code { + private static readonly ILogger Logger = Log.ForContext(typeof(MainLayout)); + + public string? ApiKeyField { get; set; } + public int? intValue { get; set; } + + readonly MudTheme _myCustomTheme = new() + { + Palette = new PaletteDark + { + Primary = "#8f38fd", + PrimaryDarken = "#722cca", + Secondary = Colors.Green.Accent4, + AppbarBackground = Colors.Red.Default, + Background = "#2f2f2f", + Surface = "#1f1f1f", + }, + LayoutProperties = new LayoutProperties + { + DrawerWidthLeft = "260px", + DrawerWidthRight = "300px" + }, + Typography = new Typography + { + Default = new Default + { + FontFamily = new string[] { "'Poppins', Roboto, Helvetica, Arial, sans-serif" } + }, + } + }; +} + + + + + + @* *@ + + @* main app *@ + + + + + Chatbox Options + + + + + + + + @foreach (Config.Conf.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(Config.Conf.ChatboxConf.HoscyMessageType))) + { + @hoscyMessageType + } + + + + + + Shocker Options + + + + + + + + + + + + + + + + Other Options + + + + @foreach (Config.Conf.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(Config.Conf.BehaviourConf.BoneHeldAction))) + { + @boneHeldAction + } + + + + + + + + + Shocker name 1 + @* Add *@ + + + List of ShockOSC prefixed parameters and their states + +
+ @foreach (var param in ShockOsc.ParamsInUse) + { + + } +
+ + @foreach (var log in Serilog.LogStore.Logs) + { + @log.Message + } + +
+
+
+
+ +@code { + private int activePageIndex = 0; + + private Task OnSettingsValueChange() + { + Logger.Information("setting changed"); + Debug.WriteLine("changed"); + return Task.CompletedTask; + } + + private Config.Conf _config; + + private void OnParamsChange() + { + // check Debug page is active + if (activePageIndex == 2) + InvokeAsync(StateHasChanged); + } + + protected override async Task OnInitializedAsync() + { + ShockOsc.OnParamsChange = () => OnParamsChange(); + + _config = Config.ConfigInstance; + } + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Main.razor b/ShockOsc/Ui/Main.razor new file mode 100644 index 0000000..90b699a --- /dev/null +++ b/ShockOsc/Ui/Main.razor @@ -0,0 +1,12 @@ +@using OpenShock.ShockOsc.Ui.Components + + + + + + + +

Sorry, there's nothing at this address.

+ + + \ No newline at end of file diff --git a/ShockOsc/Ui/MainPage.xaml b/ShockOsc/Ui/MainPage.xaml new file mode 100644 index 0000000..40ef064 --- /dev/null +++ b/ShockOsc/Ui/MainPage.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/ShockOsc/Ui/MainPage.xaml.cs b/ShockOsc/Ui/MainPage.xaml.cs new file mode 100644 index 0000000..3ad95cd --- /dev/null +++ b/ShockOsc/Ui/MainPage.xaml.cs @@ -0,0 +1,9 @@ +namespace ShockOsc; + +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/_Imports.razor b/ShockOsc/Ui/_Imports.razor new file mode 100644 index 0000000..7d37aa5 --- /dev/null +++ b/ShockOsc/Ui/_Imports.razor @@ -0,0 +1,8 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using OpenShock.ShockOsc +@using MudBlazor \ No newline at end of file diff --git a/ShockOsc/wwwroot/app.css b/ShockOsc/wwwroot/app.css new file mode 100644 index 0000000..b751e9e --- /dev/null +++ b/ShockOsc/wwwroot/app.css @@ -0,0 +1,35 @@ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(fonts/poppins-latin-400-normal.405055dd..woff2) format('woff2'), url(fonts/poppins-latin-400-normal.cda9c93f..woff) format('woff'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} + +html, body { + font-family: 'Poppins', Roboto, Helvetica, Arial, sans-serif; +} + +body { + background-color: #2f2f2f; + padding: 20px; + min-width: 600px; +} + +.mud-tabs-toolbar { + background-color: #272727 !important; +} + +.mud-tabs-panels { + background-color: #2f2f2f !important; +} + +.option-width { + display: inline-flex !important; + padding: 0 10px !important; +} + +.option-checkbox-height { + padding-top: 10px !important; +} diff --git a/ShockOsc/wwwroot/fonts/poppins-latin-400-normal.405055dd..woff2 b/ShockOsc/wwwroot/fonts/poppins-latin-400-normal.405055dd..woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b69e0091c26c46151996a7feec95515567f95ac2 GIT binary patch literal 7884 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 literal 0 HcmV?d00001 diff --git a/ShockOsc/wwwroot/fonts/poppins-latin-400-normal.cda9c93f..woff b/ShockOsc/wwwroot/fonts/poppins-latin-400-normal.cda9c93f..woff new file mode 100644 index 0000000000000000000000000000000000000000..31c978ec57ba1781c72506664ffc2b07b2f1aa80 GIT binary patch literal 10528 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 literal 0 HcmV?d00001 diff --git a/ShockOsc/wwwroot/images/Icon.svg b/ShockOsc/wwwroot/images/Icon.svg new file mode 100644 index 0000000..d40a5e4 --- /dev/null +++ b/ShockOsc/wwwroot/images/Icon.svg @@ -0,0 +1,23 @@ + + + + diff --git a/ShockOsc/wwwroot/images/Icon512.png b/ShockOsc/wwwroot/images/Icon512.png new file mode 100644 index 0000000000000000000000000000000000000000..46a9e7c80722bdd59be65db01e4139f1f8e78ac1 GIT binary patch literal 25450 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 A82|tP literal 0 HcmV?d00001 diff --git a/ShockOsc/wwwroot/images/Icon64.png b/ShockOsc/wwwroot/images/Icon64.png new file mode 100644 index 0000000000000000000000000000000000000000..919cc2f5d4953bcdb16bffb02a769d36db1fc242 GIT binary patch literal 2772 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 atl|G)wFfi_2v}AC0000 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ShockOsc/wwwroot/images/Logo512.png b/ShockOsc/wwwroot/images/Logo512.png new file mode 100644 index 0000000000000000000000000000000000000000..b310d8e5967d803379ade9e559bed2869e3f62e2 GIT binary patch literal 121558 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 yDxwKKe|Oz_S$zm&HF_8dul4Ky|I + + + + + ShockOSC + + + + + + + + + +

+ +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + \ No newline at end of file From 2be9f63d8e595d8e0f182ce54a508fdf010c21f1 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Fri, 8 Mar 2024 15:00:53 +1300 Subject: [PATCH 04/95] Ui init 1 --- ShockOsc/Config.cs | 8 +- ShockOsc/LoggerSink.cs | 16 +- .../Platforms/Windows/Package.appxmanifest | 2 - ShockOsc/Properties/launchSettings.json | 4 +- ShockOsc/ShockOsc.cs | 20 +- ShockOsc/ShockOsc.csproj | 3 + ShockOsc/Ui/App.xaml.cs | 13 +- ShockOsc/Ui/Components/Layout/Login.razor | 26 +- ShockOsc/Ui/Components/MainLayout.razor | 269 ++++++++++++------ ShockOsc/UserHubClient.cs | 49 ++-- ShockOsc/wwwroot/app.css | 3 +- 11 files changed, 282 insertions(+), 131 deletions(-) diff --git a/ShockOsc/Config.cs b/ShockOsc/Config.cs index d51aee1..3258ca9 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/Config.cs @@ -49,8 +49,6 @@ private static void TryLoad() var jsonNew = File.ReadAllText(Path); _internalConfig = JsonSerializer.Deserialize(jsonNew, Options); Logger.Information("New configuration file generated! Please configure it!"); - Logger.Information("Press any key to exit..."); - Environment.Exit(10); } private static readonly JsonSerializerOptions Options = new() @@ -93,8 +91,8 @@ public static void Save() }, Behaviour = new Conf.BehaviourConf { - RandomDuration = true, - RandomIntensity = true, + RandomDuration = false, + RandomIntensity = false, RandomDurationStep = 1000, DurationRange = new JsonRange { Min = 1000, Max = 5000 }, IntensityRange = new JsonRange { Min = 1, Max = 30 }, @@ -110,7 +108,7 @@ public static void Save() { Shockers = new Dictionary(), UserHub = null!, - ApiToken = "SET THIS TO YOUR OPENSHOCK API TOKEN", + ApiToken = "", } }; diff --git a/ShockOsc/LoggerSink.cs b/ShockOsc/LoggerSink.cs index 520ebeb..52a305d 100644 --- a/ShockOsc/LoggerSink.cs +++ b/ShockOsc/LoggerSink.cs @@ -4,6 +4,7 @@ using Serilog.Formatting.Display; using Serilog.Formatting; using System.Diagnostics; +using MudBlazor; namespace Serilog; @@ -33,7 +34,7 @@ public class MySink : ILogEventSink { private TextWriter _textWriter; private readonly ITextFormatter _formatProvider; - // public Action? EmitAction { get; set; } + public static Action? NotificationAction { get; set; } public MySink(ITextFormatter formatProvider) { @@ -46,9 +47,16 @@ public void Emit(LogEvent logEvent) _textWriter = new StringWriter(); _formatProvider.Format(logEvent, _textWriter); // var logMessage = logEvent.RenderMessage(_formatProvider); - Debug.WriteLine(_textWriter); - // EmitAction?.Invoke(_textWriter.ToString()); - LogStore.AddLog(_textWriter.ToString()); + var logMessage = _textWriter.ToString(); + if (logMessage == null) return; + if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) + { + OpenShock.ShockOsc.ShockOsc.SetAuthLoading?.Invoke(false, false); + NotificationAction?.Invoke(logMessage[82..], Severity.Error); + } + + Debug.WriteLine(logMessage); + LogStore.AddLog(logMessage); _textWriter.Flush(); } } diff --git a/ShockOsc/Platforms/Windows/Package.appxmanifest b/ShockOsc/Platforms/Windows/Package.appxmanifest index 4867aec..f94b3de 100644 --- a/ShockOsc/Platforms/Windows/Package.appxmanifest +++ b/ShockOsc/Platforms/Windows/Package.appxmanifest @@ -8,8 +8,6 @@ - - $placeholder$ User Name diff --git a/ShockOsc/Properties/launchSettings.json b/ShockOsc/Properties/launchSettings.json index edf8aad..de9182a 100644 --- a/ShockOsc/Properties/launchSettings.json +++ b/ShockOsc/Properties/launchSettings.json @@ -1,8 +1,8 @@ { "profiles": { "Windows Machine": { - "commandName": "MsixPackage", + "commandName": "Project", "nativeDebugging": false } } -} \ No newline at end of file +} diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 2e3c3c3..8fb7ec3 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -39,8 +39,9 @@ public static class ShockOsc public static Dictionary ParamsInUse = new(); public static Action? OnParamsChange; + public static Action? SetAuthLoading; - public static async Task StartMain(string[] args) + public static async Task StartMain() { Log.Logger = new LoggerConfiguration() .Filter.ByExcluding(ev => @@ -54,7 +55,7 @@ public static async Task StartMain(string[] args) #if DEBUG isDebug = true; #endif - if ((args.Length > 0 && args[0] == "--debug") || isDebug) + if (isDebug) { Log.Information("Debug logging enabled"); Log.Logger = new LoggerConfiguration() @@ -87,7 +88,12 @@ public static async Task StartMain(string[] args) _logger.Information("Found shockers: {Shockers}", Config.ConfigInstance.ShockLink.Shockers.Select(x => x.Key)); _logger.Information("Init user hub..."); - // await UserHubClient.InitializeAsync(); + SetAuthLoading?.Invoke(false, false); + if (!string.IsNullOrEmpty(Config.ConfigInstance.ShockLink.ApiToken)) + { + SetAuthLoading?.Invoke(false, true); + UserHubClient.InitializeAsync(); + } _logger.Information("Creating OSC Query Server..."); _ = new OscQueryServer( @@ -119,6 +125,14 @@ public static async Task StartMain(string[] args) await Task.Delay(Timeout.Infinite).ConfigureAwait(false); } + public static void ClickLogin() + { + Config.Save(); + _logger.Information("Clicking login"); + SetAuthLoading?.Invoke(false, true); + UserHubClient.InitializeAsync(); + } + private static void FoundVrcClient() { _logger.Information("Found VRC client"); diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 9cbc63c..003bb1c 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -29,6 +29,9 @@ 10.0.17763.0 10.0.17763.0 6.5 + + None + Resources\Icon512.png diff --git a/ShockOsc/Ui/App.xaml.cs b/ShockOsc/Ui/App.xaml.cs index d870c3f..957215a 100644 --- a/ShockOsc/Ui/App.xaml.cs +++ b/ShockOsc/Ui/App.xaml.cs @@ -4,10 +4,21 @@ public partial class App : Application { public App() { - _ = OpenShock.ShockOsc.ShockOsc.StartMain([ "--debug" ]); + _ = OpenShock.ShockOsc.ShockOsc.StartMain(); InitializeComponent(); MainPage = new MainPage(); } + + protected override Window CreateWindow(IActivationState activationState) + { + var window = base.CreateWindow(activationState); + if (window != null) + { + window.Title = "ShockOSC"; + } + + return window; + } } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Layout/Login.razor b/ShockOsc/Ui/Components/Layout/Login.razor index 2d8a39a..f2b19c9 100644 --- a/ShockOsc/Ui/Components/Layout/Login.razor +++ b/ShockOsc/Ui/Components/Layout/Login.razor @@ -1,12 +1,22 @@ @code { - public string? ApiKeyField { get; set; } + [Parameter] + public bool Loading { get; set; } } - - Login -
- -
- Continue -
+ + + Login +
+ @if (!Loading) + { + +
+ Continue +
+ } + else + { + + } +
\ No newline at end of file diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index f1c53ed..fe98737 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,7 +1,9 @@ @using System.Text.Json.Serialization @using System.Diagnostics @using OpenShock.ShockOsc.Ui.Components.Layout +@using OpenShock.ShockOsc.Models; @using Serilog +@inject ISnackbar Snackbar @inherits LayoutComponentBase @@ -10,9 +12,6 @@ @code { private static readonly ILogger Logger = Log.ForContext(typeof(MainLayout)); - - public string? ApiKeyField { get; set; } - public int? intValue { get; set; } readonly MudTheme _myCustomTheme = new() { @@ -43,98 +42,166 @@ + +@* + *@ - @* *@ - - @* main app *@ - - - - - Chatbox Options - - - - - - - - @foreach (Config.Conf.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(Config.Conf.ChatboxConf.HoscyMessageType))) - { - @hoscyMessageType - } - + @if (!_authenticated) + { + + } + else + { + + + + + Chatbox Options + + + +
+
+ + + + + @foreach (Config.Conf.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(Config.Conf.ChatboxConf.HoscyMessageType))) + { + @hoscyMessageType + } + + +
+
+ + @(_AdvancedSettingsExpanded ? "Advanced Settings" : "Advanced Settings") + + + + + +
+ + @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) + { + + + + + + + } + +
+
-
- - Shocker Options - - - - - - - - - - - - - - - - Other Options - - - - @foreach (Config.Conf.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(Config.Conf.BehaviourConf.BoneHeldAction))) - { - @boneHeldAction - } - + + Shocker Options + + + + @foreach (Config.Conf.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(Config.Conf.BehaviourConf.BoneHeldAction))) + { + @boneHeldAction + } + + + + +
+ + + + +
+ @if (intensity == "Fixed Intensity") + { + Intensity: @_config.Behaviour.FixedIntensity.ToString()% + } + else + { + Intensity Min: @_config.Behaviour.IntensityRange.Min.ToString()% +
+ Intensity Max: @_config.Behaviour.IntensityRange.Max.ToString()% + } + + + + +
+ @if (duration == "Fixed Duration") + { + + } + else + { + + + + } +
+
+
+ + + Other Options + + + + + - - -
- -
- - Shocker name 1 - @* Add *@ - - - List of ShockOSC prefixed parameters and their states - -
- @foreach (var param in ShockOsc.ParamsInUse) - { - - } -
- - @foreach (var log in Serilog.LogStore.Logs) - { - @log.Message - } - -
-
+ + + + Shocker name 1 + @* Add *@ + + + List of ShockOSC prefixed parameters and their states + +
+ @foreach (var param in ShockOsc.ParamsInUse) + { + + } +
+ + @{int i = 0;} + @foreach (var log in Serilog.LogStore.Logs) + { + if (i > 1000) + break; + i++; + @log.Message + } + + +
+ } @code { private int activePageIndex = 0; + private bool _AdvancedSettingsExpanded = false; + private Config.Conf _config; - private Task OnSettingsValueChange() + private string intensity = "Fixed Intensity"; + private string duration = "Fixed Duration"; + + private bool _loading = false; + private bool _authenticated = false; + + + + private void OnAdvancedSettingsClick() { - Logger.Information("setting changed"); - Debug.WriteLine("changed"); - return Task.CompletedTask; + _AdvancedSettingsExpanded = !_AdvancedSettingsExpanded; } - private Config.Conf _config; - private void OnParamsChange() { // check Debug page is active @@ -142,11 +209,45 @@ InvokeAsync(StateHasChanged); } + private void MsgNoty(string msg, Severity severity) + { + Snackbar.Add(msg, severity); + } + + private Task OnSettingsValueChange() + { + _config.Behaviour.RandomIntensity = intensity == "Random Intensity"; + _config.Behaviour.RandomDuration = duration == "Random Duration"; + ValidateSettings(); + + Logger.Information("settings changed"); + Config.Save(); + return Task.CompletedTask; + } + + private void ValidateSettings() + { + if (_config.Behaviour.IntensityRange.Min > _config.Behaviour.IntensityRange.Max) + { + _config.Behaviour.IntensityRange.Max = _config.Behaviour.IntensityRange.Min; + } + } + + private void SetAuthLoading(bool auth, bool loading) + { + _authenticated = auth; + _loading = loading; + InvokeAsync(StateHasChanged); + } + protected override async Task OnInitializedAsync() { - ShockOsc.OnParamsChange = () => OnParamsChange(); + ShockOsc.SetAuthLoading = SetAuthLoading; + ShockOsc.OnParamsChange = OnParamsChange; + MySink.NotificationAction = MsgNoty; _config = Config.ConfigInstance; + intensity = _config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; + duration = _config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; } - } \ No newline at end of file diff --git a/ShockOsc/UserHubClient.cs b/ShockOsc/UserHubClient.cs index 3a8ee2f..b192bc2 100644 --- a/ShockOsc/UserHubClient.cs +++ b/ShockOsc/UserHubClient.cs @@ -12,39 +12,46 @@ public static class UserHubClient { private static readonly ILogger Logger = Log.ForContext(typeof(UserHubClient)); public static string? ConnectionId { get; set; } - - private static readonly HubConnection Connection = new HubConnectionBuilder() - .WithUrl(Config.ConfigInstance.ShockLink.UserHub, HttpTransportType.WebSockets, - options => { options.Headers.Add("OpenShockToken", Config.ConfigInstance.ShockLink.ApiToken); }) - .WithAutomaticReconnect() - .ConfigureLogging(builder => - { - builder.ClearProviders(); - builder.SetMinimumLevel(LogLevel.Trace); - builder.AddSerilog(); - }) - .AddJsonProtocol(options => - { - options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; - options.PayloadSerializerOptions.Converters.Add(new CustomJsonStringEnumConverter()); - }) - .Build(); - static UserHubClient() + private static HubConnection? Connection; + + public static Task InitializeAsync() { + Connection?.DisposeAsync(); + Connection = new HubConnectionBuilder() + .WithUrl(Config.ConfigInstance.ShockLink.UserHub, HttpTransportType.WebSockets, + options => { options.Headers.Add("OpenShockToken", Config.ConfigInstance.ShockLink.ApiToken); }) + .WithAutomaticReconnect() + .ConfigureLogging(builder => + { + builder.ClearProviders(); + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddSerilog(); + }) + .AddJsonProtocol(options => + { + options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; + options.PayloadSerializerOptions.Converters.Add(new CustomJsonStringEnumConverter()); + }) + .Build(); Connection.On>("Log", LogReceive); Connection.On("Welcome", WelcomeReceive); + Connection.Closed += async exception => + { + ShockOsc.SetAuthLoading?.Invoke(false, true); + }; + Connection.StartAsync(); + return Task.CompletedTask; } - - public static Task InitializeAsync() => Connection.StartAsync(); - public static Task Control(params Control[] data) => Connection.SendAsync("ControlV2", data, "ShockOsc"); + public static Task? Control(params Control[] data) => Connection?.SendAsync("ControlV2", data, "ShockOsc"); #region Handlers private static Task WelcomeReceive(string connectionId) { ConnectionId = connectionId; + ShockOsc.SetAuthLoading?.Invoke(true, false); return Task.CompletedTask; } diff --git a/ShockOsc/wwwroot/app.css b/ShockOsc/wwwroot/app.css index b751e9e..dd7cc8e 100644 --- a/ShockOsc/wwwroot/app.css +++ b/ShockOsc/wwwroot/app.css @@ -9,6 +9,7 @@ html, body { font-family: 'Poppins', Roboto, Helvetica, Arial, sans-serif; + -webkit-user-select: none; } body { @@ -27,7 +28,7 @@ body { .option-width { display: inline-flex !important; - padding: 0 10px !important; + padding: 0 10px 0 0 !important; } .option-checkbox-height { From 4b446288b786980547acd2e81bf08a48be2d825b Mon Sep 17 00:00:00 2001 From: Natsumi Date: Sat, 9 Mar 2024 15:03:48 +1300 Subject: [PATCH 05/95] Create NSIS installer --- .../Plugins/x86-unicode/ApplicationID.dll | Bin 0 -> 203264 bytes Installer/Plugins/x86-unicode/INetC.dll | Bin 0 -> 25600 bytes .../Plugins/x86-unicode/ShellExecAsUser.dll | Bin 0 -> 44032 bytes Installer/Plugins/x86-unicode/nsProcess.dll | Bin 0 -> 4608 bytes Installer/build-installer.bat | 2 + Installer/installer.nsi | 208 ++++++++++++++++++ ShockOsc/Config.cs | 6 +- ShockOsc/LoggerSink.cs | 2 +- ShockOsc/Resources/openshock-icon.ico | Bin 4286 -> 114773 bytes ShockOsc/ShockOsc.csproj | 3 + 10 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 Installer/Plugins/x86-unicode/ApplicationID.dll create mode 100644 Installer/Plugins/x86-unicode/INetC.dll create mode 100644 Installer/Plugins/x86-unicode/ShellExecAsUser.dll create mode 100644 Installer/Plugins/x86-unicode/nsProcess.dll create mode 100644 Installer/build-installer.bat create mode 100644 Installer/installer.nsi diff --git a/Installer/Plugins/x86-unicode/ApplicationID.dll b/Installer/Plugins/x86-unicode/ApplicationID.dll new file mode 100644 index 0000000000000000000000000000000000000000..5fc0480256d0c68bf643ca4cbc8ab95a349c2aab GIT binary patch literal 203264 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 literal 0 HcmV?d00001 diff --git a/Installer/Plugins/x86-unicode/ShellExecAsUser.dll b/Installer/Plugins/x86-unicode/ShellExecAsUser.dll new file mode 100644 index 0000000000000000000000000000000000000000..2bf1d0c3de1ad63222cc20b4f64bf611f5e366d6 GIT binary patch literal 44032 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 B@$mov literal 0 HcmV?d00001 diff --git a/Installer/Plugins/x86-unicode/nsProcess.dll b/Installer/Plugins/x86-unicode/nsProcess.dll new file mode 100644 index 0000000000000000000000000000000000000000..2478624ee0bcceb6e2c0eee3a563e7cea272ba9a GIT binary patch literal 4608 zcmeHKeQaA-6+cd#)a}y7D)rc=LSNh)ERn#_aahBLk)igDAYKML#b?leg+4FOF zA8CW;X_e!F_hxMp6O&NHC`Cf!Ln>Ma5sc1iJ3^&xAR$6WC8(wfxg>uSsUUA;d;IS6 z2i^u#CB*oHUF+O)&*wYm-1DyQJ8essF zO@ps>4JT7FHzj3GO0hH-kBMSN;l_DRQbjH$a{ULxTso8B9c$OF>8`n6`+NCU`+oMR zxjKm1iMc_8-Z&Qm_Fw+y+BSpqCRmMfxd9vsmLhm?pZF{0<#=7VAAtO0Hs zenju6su9QP1ulC^{!1NnSX+)nwXCHWu!N$zEyR&kQ#-VPrKeq8KS}sm^=Y$iI|f1FQM0Es z<{v$N$-?Y%%N^l}rdpyA&-HN9WVVuSvoiZi&)=1Y3+kQ1@ygEQ9ZGL8a3_>pg*QyM z4MV>3ctzP-46Lx{b%4&7l>5(QS6cD1sMh5CJwy;#41wCF1*}W!!(f;V21vD43v7rk z5PP?D8(P>pHhT2xWdw9)If3Sndh}xc*YE0jsF1x~$SxPMcNPqK11xl#zv_6+{4q4@ ztw~I|pWlFXOE{u@vbr>N&bC2!{y5VJV!B9vqU5Wv)c&B`e87c3rDs8zOrFO&q}z5F ztTI@8BieQ^sHnc!v#^9TZ-&CUx-!|OA1-T^ysmr}lx}lE<&2Kkv!t$Ty&j$jea-sViF0htdU;W{{y`e(r6gtlJ)h zx~^IpbT{aHN!{cx1a9vPbf_HLjbdOqq^sE3{;~(T=dM__cK9;4-d=jX5#OziaGd-W z^Jn8QYuB)$J%8++T?4+ho#mXI#T3>TlM2IH1*-&Bh+Dg+FBSsjyxuwcDmE%#>YTm8 z@b%8wmw?e%E&A`YN=}Eu_rWOUzq5k)qYDP!!jk{Zt(+nFB@^hjjc{DN5cmbCrMB6U zvTEpwcHc4euSWj0bb}Gyb|3Wop$O|;-L}Dq<^$ER*Q>Fmxf(D20KUuicZ`D61H!%` z_`Is?I=Yam*;RKXm%UBus`;aA!|b_ihe>@PmtAJ8FEUnVE_=rW;s_&JJf+d2V^=S0 z{oR(xIkOYx?C;)i4ks&HSd;7+zQ}=!oU>pqKU*ylMar9vcsfGm`%(7MyLJ$%Ixt1b zZz8MQ6*ypj+jsB4#vGU?;oxlD0SjvM@>v!%y2YQ{K}t{Vk!~|ZG^f&qJUY;6ZD(0_ zp<>V+(v@!X4(mw6Mq3vtSae%IXjEgVm7PEq%I#+UNZDoBMT-4o2~GNS+W;%|cB4|a z;i$uK*yM}~?S|KabM%s#okwSu*?+sy`&7~OT2C(ru@Sgz{z{MIbBxT~f zEPLET)w@Z94yGg-mz6$=k170cCL<(yVanqUi?Jy=nK7<0`vZrD0)rm6qknLa`5#ig zpz_j8ct%$E^geMSBc)?XDkB;)8m4|4#LrQ~M4zeMmb#HoeVk`;afrW(XWop{6TE*l zK8hOkQJ;{J`GJ_25O~^Z9_FQVN<>*|#lo%69F3)vU`7h3#FGMlaD0-FEA)zKFRuhn zr<6WitQ)$X)`L^LSSKH7J)jCo>Y*7{MG|#iSX?erU+z>8VtdUZ}38X2d9N*FlNrYkKq!tl}|&vFNSpx>qo6dRz-4WTEsX z5)#^g=uYz!@Wg4R5#c2n`xrPKr6*hV3YqbmGs@Sfs=G!%Bm{wIQ%fi#D+k~;c9Msu zr&%?kDKU{bHJn6OC2A$um%!Oe0Z~m?g)&)MPsvkKN>nDIbT}M1gbp)2vi+}rGn_zA zm_V~rg24{Z1Ar00Tc0301(*l0dv*rAEZ_;ivjAq`zZ(GCndyO0Zn>0S~ zblGpL3pf zE;uVrw`-Rx=sMs!>Uz}myz82)?E19(LAS%*>khhy+#~KEx!-XA$^AF?=RBYHWId02 zp7DIcbJ4Tl`K{-=NB3B~_j)&ad%Q=zlirN?%U;d _internalConfig!; @@ -63,6 +63,10 @@ public static void Save() Logger.Information("Saving config"); try { + var directory = System.IO.Path.GetDirectoryName(Path); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); + File.WriteAllText(Path, JsonSerializer.Serialize(_internalConfig, Options)); } catch (Exception e) diff --git a/ShockOsc/LoggerSink.cs b/ShockOsc/LoggerSink.cs index 52a305d..cf3401f 100644 --- a/ShockOsc/LoggerSink.cs +++ b/ShockOsc/LoggerSink.cs @@ -48,7 +48,7 @@ public void Emit(LogEvent logEvent) _formatProvider.Format(logEvent, _textWriter); // var logMessage = logEvent.RenderMessage(_formatProvider); var logMessage = _textWriter.ToString(); - if (logMessage == null) return; + if (string.IsNullOrEmpty(logMessage)) return; if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) { OpenShock.ShockOsc.ShockOsc.SetAuthLoading?.Invoke(false, false); diff --git a/ShockOsc/Resources/openshock-icon.ico b/ShockOsc/Resources/openshock-icon.ico index 1f24c01b602d15ce4c046c37831744cf1f120ed9..95dd208062d46030cb27084be667a78597088b94 100644 GIT binary patch literal 114773 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 TA&y6w00000NkvXXu0mjf=?2b8 literal 4286 zcmbVQYiJx*6rQ!UX?=7>@bRO#RZv@eVQc^RXV+hGXJS=w1%D|N5@}|(=8u1TbU`E_ z#ri;rREc1P63k8?Q0*@xr9K1FlvW{_otM#83M*~urrq7+ckaxr(_v=z#)Ol5=iGD7 zcka3OoO^E+C5PXQ8x{Vm9JonQZdDXz69_D&1Y*AU!PtX2pL~X*5RnwsO0-%ZYLpDs ze#=m;8MMEEE`pXpXLZ&39_>U`wV&NtI(LJtU0J!RYP}8nBuN(`|G`eRal5SjB)Lk3 zRsrwW*r0p!3#G(&O&u-T>e%@Yh1KUk~}*8f9+-#yFQRaX&hi*6+CWeehw4em`_?noOJz`@A2>Kwd=G0laqw zAA6SfOn+SV!b_kh8jGS7hu68E1N%z=sdy zn6Jv}F-8~zu9MXv>uG=Ps2eM(6Q`%v0KXOZaE0KHj1QtdMCRM8tD3!EMn5XUu@7U= zb<8v5POXcMr}eS^`-3@87^?G~SP#uM(&W?l9j89g!^b*wJabQ)Y?O~YGkko3?Qoy# zc~s`A{-4LLC(j&?6Yvg1@zL)fCrel9#`YkOF;+1DvuH(5a^1<&>p148F)T{__RI#& zA6=hiYxD_gOMu@(%jY-oIhLhEzfI+VXC-2C9!99+njdG{io^uie+m4o_=ob|p~ryJ ziD7*nUl;Mcc;q@6AAtQMlauU7;A2lfFuze7$Z`K~>xdiMbTO5nqYw5`Q2HUa+<>d=SMI$Wx+0b-+Ir_^`ltrf{rJo5R_2m~bBUk?Z@;;rk};&F6YP;O;18 z)0^Hv*5Ny)WK-rJ>Y@%k9PxqkVnq5*6tRt$T~61m9lC0tiQctBoUGQ5LvJnk%{3?P zEfV!Xyc5gLQ_%72YUHk!g&7|c`@!FnVP3Ew=fxW<^8UhEzLnplXooRA3F0%AWtm!f z*?Mhw>#WrMFzy3?i`?|d`nPmpjDVz#Y6=wM}_8YLHSmZAWP7p@otfhQ<1)*0$DUk&0@RQ%HuDv Ki2wBf-G2dgftZB= diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 003bb1c..36b1075 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -70,6 +70,9 @@ PreserveNewest + + PreserveNewest +
From b4ade96b667428007308f5a3b61835b83e0d34a5 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Tue, 12 Mar 2024 02:15:47 +1300 Subject: [PATCH 06/95] Auto fetch shockers, debug all avatar parameters --- ShockOsc/Config.cs | 4 +- ShockOsc/LoggerSink.cs | 41 +-- ShockOsc/OscQueryLibrary/OscQueryServer.cs | 10 +- ShockOsc/ShockLinkApi.cs | 55 ++++ ShockOsc/ShockLinkModels.cs | 25 ++ ShockOsc/ShockOsc.cs | 95 +++++-- ShockOsc/Ui/Components/Layout/Login.razor | 2 +- ShockOsc/Ui/Components/MainLayout.razor | 300 ++++++++++++--------- ShockOsc/UserHubClient.cs | 8 +- ShockOsc/WebRequestApi.cs | 73 +++++ ShockOsc/wwwroot/app.css | 8 +- 11 files changed, 432 insertions(+), 189 deletions(-) create mode 100644 ShockOsc/ShockLinkApi.cs create mode 100644 ShockOsc/ShockLinkModels.cs create mode 100644 ShockOsc/WebRequestApi.cs diff --git a/ShockOsc/Config.cs b/ShockOsc/Config.cs index d17c050..9c6687f 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/Config.cs @@ -111,7 +111,7 @@ public static void Save() ShockLink = new Conf.OpenShockConf { Shockers = new Dictionary(), - UserHub = null!, + OpenShockApi = null!, ApiToken = "", } }; @@ -227,7 +227,7 @@ public enum BoneHeldAction public class OpenShockConf { - public Uri UserHub { get; set; } = new("https://api.shocklink.net/1/hubs/user"); + public Uri OpenShockApi { get; set; } = new("https://api.shocklink.net/1"); public required string ApiToken { get; set; } public required IReadOnlyDictionary Shockers { get; set; } } diff --git a/ShockOsc/LoggerSink.cs b/ShockOsc/LoggerSink.cs index cf3401f..e8c8a82 100644 --- a/ShockOsc/LoggerSink.cs +++ b/ShockOsc/LoggerSink.cs @@ -11,16 +11,20 @@ namespace Serilog; public class LogStore { public static List Logs = new(); + public static Action? OnLogAdded; public static void AddLog(string log) { if (Logs.Count == 0) { Logs.Add(new LogEntry { Time = DateTime.Now, Message = log }); - return; } - // add to start of list - Logs.Insert(0, new LogEntry { Time = DateTime.Now, Message = log }); + else + { + // add to start of list + Logs.Insert(0, new LogEntry { Time = DateTime.Now, Message = log }); + } + OnLogAdded?.Invoke(); } public class LogEntry @@ -44,20 +48,27 @@ public MySink(ITextFormatter formatProvider) public void Emit(LogEvent logEvent) { - _textWriter = new StringWriter(); - _formatProvider.Format(logEvent, _textWriter); - // var logMessage = logEvent.RenderMessage(_formatProvider); - var logMessage = _textWriter.ToString(); - if (string.IsNullOrEmpty(logMessage)) return; - if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) + try { - OpenShock.ShockOsc.ShockOsc.SetAuthLoading?.Invoke(false, false); - NotificationAction?.Invoke(logMessage[82..], Severity.Error); - } + _textWriter = new StringWriter(); + _formatProvider.Format(logEvent, _textWriter); + // var logMessage = logEvent.RenderMessage(_formatProvider); + var logMessage = _textWriter.ToString(); + if (string.IsNullOrEmpty(logMessage)) return; + if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) + { + OpenShock.ShockOsc.ShockOsc.SetAuthLoading?.Invoke(false, false); + NotificationAction?.Invoke(logMessage[82..], Severity.Error); + } - Debug.WriteLine(logMessage); - LogStore.AddLog(logMessage); - _textWriter.Flush(); + Debug.WriteLine(logMessage); + LogStore.AddLog(logMessage); + _textWriter.Flush(); + } + catch (Exception) + { + // this kinda sucks + } } } diff --git a/ShockOsc/OscQueryLibrary/OscQueryServer.cs b/ShockOsc/OscQueryLibrary/OscQueryServer.cs index d349875..0211a5a 100644 --- a/ShockOsc/OscQueryLibrary/OscQueryServer.cs +++ b/ShockOsc/OscQueryLibrary/OscQueryServer.cs @@ -30,12 +30,12 @@ public class OscQueryServer : IDisposable private static readonly HashSet FoundServices = new(); private static IPEndPoint? _lastVrcHttpServer; private static event Action? FoundVrcClient; - private static event Action>? ParameterUpdate; + private static event Action, string>? ParameterUpdate; private static readonly Dictionary ParameterList = new(); public OscQueryServer(string serviceName, string ipAddress, Action? foundVrcClient = null, - Action>? parameterUpdate = null) + Action, string>? parameterUpdate = null) { Swan.Logging.Logger.NoLogging(); @@ -187,6 +187,7 @@ private static async Task FetchJsonFromVrc(IPAddress ipAddress, int port) var url = $"http://{ipAddress}:{port}/"; Logger.Debug("OSCQueryHttpClient: Fetching new parameters from {Url}", url); var response = string.Empty; + var avatarId = string.Empty; var client = new HttpClient(); try { @@ -204,13 +205,14 @@ private static async Task FetchJsonFromVrc(IPAddress ipAddress, int port) RecursiveParameterLookup(node); } - ParameterUpdate?.Invoke(ParameterList); + avatarId = rootNode.CONTENTS.avatar.CONTENTS.change.VALUE?[0]?.ToString() ?? string.Empty; + ParameterUpdate?.Invoke(ParameterList, avatarId); } catch (HttpRequestException ex) { _lastVrcHttpServer = null; ParameterList.Clear(); - ParameterUpdate?.Invoke(ParameterList); + ParameterUpdate?.Invoke(ParameterList, avatarId); Logger.Error("OSCQueryHttpClient: Error {ExMessage}", ex.Message); } catch (Exception ex) diff --git a/ShockOsc/ShockLinkApi.cs b/ShockOsc/ShockLinkApi.cs new file mode 100644 index 0000000..66af608 --- /dev/null +++ b/ShockOsc/ShockLinkApi.cs @@ -0,0 +1,55 @@ +using Serilog; +using System.Net; +using System.Text.Json; + +namespace OpenShock.ShockOsc; + +public class ShockLinkApi +{ + private static readonly ILogger Logger = Log.ForContext(typeof(ShockLinkApi)); + + private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }; + + public static List Shockers = new(); + + public static async Task GetShockers() + { + var response = await WebRequestApi.DoRequest(new WebRequestApi.RequestData + { + url = $"{Config.ConfigInstance.ShockLink.OpenShockApi}/shockers/own" + }); + if (response.Item1 == HttpStatusCode.OK) + { + Shockers.Clear(); + var shockers = JsonSerializer.Deserialize(response.Item2, _jsonOptions); + if (shockers == null || shockers.Data.Length == 0) + { + Logger.Error("Failed to deserialize shockers: {response}", response); + return; + } + foreach (var device in shockers.Data) + { + foreach (var shocker in device.Shockers) + { + Shockers.Add(shocker); + } + } + + // populate config + var shockerList = new Dictionary(); + foreach (var shocker in Shockers) + { + shockerList.Add(shocker.Name, shocker.Id); + } + Config.ConfigInstance.ShockLink.Shockers = shockerList; + Config.Save(); + } + else + { + Logger.Error("Failed to fetch shockers: {response}", response); + } + } +} \ No newline at end of file diff --git a/ShockOsc/ShockLinkModels.cs b/ShockOsc/ShockLinkModels.cs new file mode 100644 index 0000000..953064a --- /dev/null +++ b/ShockOsc/ShockLinkModels.cs @@ -0,0 +1,25 @@ +namespace OpenShock.ShockOsc; + +public class OwnShockersResponseResponseData +{ + public string? Message { get; set; } + public required Device[] Data { get; set; } +} + +public class Device +{ + public required Guid Id { get; set; } + public required string Name { get; set; } + public DateTime CreatedOn { get; set; } + public IList Shockers { get; set; } = new List(); + + public class Shocker + { + public required Guid Id { get; set; } + public required string Name { get; set; } + public required bool IsPaused { get; set; } + public required DateTime CreatedOn { get; set; } + public required int RfId { get; set; } + public required string Model { get; set; } + } +} \ No newline at end of file diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 8fb7ec3..fc4ca5a 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -21,6 +21,7 @@ public static class ShockOsc private static bool _oscServerActive; private static bool _isAfk; private static bool _isMuted; + public static string AvatarId = string.Empty; private static readonly Random Random = new(); public static readonly ConcurrentDictionary Shockers = new(); @@ -37,8 +38,9 @@ public static class ShockOsc }; public static Dictionary ParamsInUse = new(); + public static Dictionary AllAvatarParams = new(); - public static Action? OnParamsChange; + public static Action? OnParamsChange; public static Action? SetAuthLoading; public static async Task StartMain() @@ -87,13 +89,7 @@ public static async Task StartMain() _logger.Information("Found shockers: {Shockers}", Config.ConfigInstance.ShockLink.Shockers.Select(x => x.Key)); - _logger.Information("Init user hub..."); - SetAuthLoading?.Invoke(false, false); - if (!string.IsNullOrEmpty(Config.ConfigInstance.ShockLink.ApiToken)) - { - SetAuthLoading?.Invoke(false, true); - UserHubClient.InitializeAsync(); - } + ConnectToHub(); _logger.Information("Creating OSC Query Server..."); _ = new OscQueryServer( @@ -125,12 +121,40 @@ public static async Task StartMain() await Task.Delay(Timeout.Infinite).ConfigureAwait(false); } + private static void ConnectToHub() + { + _logger.Information("Init user hub..."); + SetAuthLoading?.Invoke(false, false); + if (string.IsNullOrEmpty(Config.ConfigInstance.ShockLink.ApiToken)) + return; + + SetAuthLoading?.Invoke(false, true); + UserHubClient.InitializeAsync().ContinueWith(task => + { + if (task.IsFaulted) + SetAuthLoading?.Invoke(false, false); + + if (task.IsCompletedSuccessfully) + { + ShockLinkApi.GetShockers(); + SetAuthLoading?.Invoke(true, false); + } + }); + } + public static void ClickLogin() { Config.Save(); _logger.Information("Clicking login"); - SetAuthLoading?.Invoke(false, true); - UserHubClient.InitializeAsync(); + ConnectToHub(); + } + + private static void OnParamChange(bool shockOscParam) + { + if (OnParamsChange == null) + return; + + OnParamsChange.Invoke(shockOscParam); } private static void FoundVrcClient() @@ -158,8 +182,9 @@ private static void FoundVrcClient() OsTask.Run(UnderscoreConfig.SendUpdateForAll); } - private static void OnAvatarChange(Dictionary? parameters) + private static void OnAvatarChange(Dictionary? parameters, string avatarId) { + AvatarId = avatarId; try { foreach (var obj in Shockers) @@ -176,13 +201,17 @@ private static void OnAvatarChange(Dictionary? parameters) } ParamsInUse.Clear(); + AllAvatarParams.Clear(); foreach (var param in parameters.Keys) { + if (param.StartsWith("/avatar/parameters/")) + AllAvatarParams.TryAdd(param[19..], parameters[param]); + if (!param.StartsWith("/avatar/parameters/ShockOsc/")) continue; - var paramName = param.Substring(28, param.Length - 28); + var paramName = param[28..]; var lastUnderscoreIndex = paramName.LastIndexOf('_') + 1; var action = string.Empty; var shockerName = paramName; @@ -192,18 +221,17 @@ private static void OnAvatarChange(Dictionary? parameters) action = paramName.Substring(lastUnderscoreIndex, paramName.Length - lastUnderscoreIndex); } - if (!Shockers.ContainsKey(shockerName) && !shockerName.StartsWith("_")) - { - _logger.Warning("Unknown shocker on avatar {Shocker}", shockerName); - _logger.Debug("Param: {Param}", param); - continue; - } - if (ShockerParams.Contains(action)) { parameterCount++; ParamsInUse.TryAdd(paramName, parameters[param]); } + + if (!Shockers.ContainsKey(shockerName) && !shockerName.StartsWith("_")) + { + _logger.Warning("Unknown shocker on avatar {Shocker}", shockerName); + _logger.Debug("Param: {Param}", param); + } } _logger.Information("Loaded avatar config with {ParamCount} parameters", parameterCount); @@ -212,6 +240,7 @@ private static void OnAvatarChange(Dictionary? parameters) { _logger.Error(e, "Error on avatar change logic"); } + OnParamChange(true); } private static async Task ReceiverLoopAsync() @@ -246,6 +275,18 @@ private static async Task ReceiveLogic() var addr = received.Address; _logger.Verbose("Received message: {Addr}", addr); + if (addr.StartsWith("/avatar/parameters/")) + { + var fullName = addr[19..]; + if (AllAvatarParams.ContainsKey(fullName)) + { + AllAvatarParams[fullName] = received.Arguments[0]; + OnParamChange(false); + } + else + AllAvatarParams.TryAdd(fullName, received.Arguments[0]); + } + switch (addr) { case "/avatar/change": @@ -285,6 +326,14 @@ private static async Task ReceiveLogic() action = pos.Substring(lastUnderscoreIndex, pos.Length - lastUnderscoreIndex); } + if (ParamsInUse.ContainsKey(pos)) + { + ParamsInUse[pos] = received.Arguments[0]; + OnParamChange(true); + } + else + ParamsInUse.TryAdd(pos, received.Arguments[0]); + if (!ShockerParams.Contains(action)) return; if (!Shockers.ContainsKey(shockerName)) @@ -294,14 +343,6 @@ private static async Task ReceiveLogic() _logger.Debug("Param: {Param}", pos); return; } - - if (ParamsInUse.ContainsKey(pos)) - { - ParamsInUse[pos] = received.Arguments[0]; - OnParamsChange(); - } - else - ParamsInUse.TryAdd(pos, received.Arguments[0]); var shocker = Shockers[shockerName]; diff --git a/ShockOsc/Ui/Components/Layout/Login.razor b/ShockOsc/Ui/Components/Layout/Login.razor index f2b19c9..87bd8a2 100644 --- a/ShockOsc/Ui/Components/Layout/Login.razor +++ b/ShockOsc/Ui/Components/Layout/Login.razor @@ -4,7 +4,7 @@ } - + Login
@if (!Loading) diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index fe98737..304ae0f 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -40,153 +40,173 @@ } - - + -@* - *@ +@* + *@ - @if (!_authenticated) - { - - } - else - { - - - - - Chatbox Options - - - -
-
- - - - - @foreach (Config.Conf.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(Config.Conf.ChatboxConf.HoscyMessageType))) - { - @hoscyMessageType - } - - -
-
- - @(_AdvancedSettingsExpanded ? "Advanced Settings" : "Advanced Settings") - - - - - -
- - @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) - { - - - - - - - } - -
-
+ @if (!_authenticated) + { + + } + else + { + + + + + Chatbox Options + + + +
+
+ + + + + @foreach (Config.Conf.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(Config.Conf.ChatboxConf.HoscyMessageType))) + { + @hoscyMessageType + } + - - - Shocker Options +
+
+ + @(_advancedSettingsExpanded ? "Advanced Settings" : "Advanced Settings") - - - @foreach (Config.Conf.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(Config.Conf.BehaviourConf.BoneHeldAction))) + + + + +
+ + @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) { - @boneHeldAction + + + + + + } -
-
- - -
- - - - -
- @if (intensity == "Fixed Intensity") - { - Intensity: @_config.Behaviour.FixedIntensity.ToString()% - } - else - { - Intensity Min: @_config.Behaviour.IntensityRange.Min.ToString()% -
- Intensity Max: @_config.Behaviour.IntensityRange.Max.ToString()% - } - - - - -
- @if (duration == "Fixed Duration") - { - - } - else - { - - - - } -
-
-
- - - Other Options - - - - - +
+ + +
+ + + Shocker Options + + + + @foreach (Config.Conf.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(Config.Conf.BehaviourConf.BoneHeldAction))) + { + @boneHeldAction + } + + + +
+ + + + +
+ @if (intensity == "Fixed Intensity") + { + Intensity: @_config.Behaviour.FixedIntensity.ToString()% + } + else + { + Intensity Min: @_config.Behaviour.IntensityRange.Min.ToString()% +
+ Intensity Max: @_config.Behaviour.IntensityRange.Max.ToString()% + } + + + + +
+ @if (duration == "Fixed Duration") + { + + } + else + { + + + + } +
+
+
-
- - Shocker name 1 - @* Add *@ - - - List of ShockOSC prefixed parameters and their states + + Other Options -
- @foreach (var param in ShockOsc.ParamsInUse) + + + + +
+ +
+ + Refresh + @foreach (var shocker in _config.ShockLink.Shockers) + { + // side by side flex + + @shocker.Key + @shocker.Value + + } + + + List of ShockOSC prefixed parameters and their states + AvatarId: @ShockOsc.AvatarId +
+ + +
+ @if (_showAllAvatarParams) + { + @foreach (var param in ShockOsc.AllAvatarParams) { } -
- - @{int i = 0;} - @foreach (var log in Serilog.LogStore.Logs) + } + else + { + @foreach (var param in ShockOsc.ParamsInUse) { - if (i > 1000) - break; - i++; - @log.Message + } - -
-
- } -
+ } + + + @{int i = 0;} + @foreach (var log in Serilog.LogStore.Logs) + { + if (i > 1000) + break; + i++; + @log.Message + } + + +
+ } @code { private int activePageIndex = 0; - private bool _AdvancedSettingsExpanded = false; + private bool _advancedSettingsExpanded = false; + private bool _showAllAvatarParams = false; + private Config.Conf _config; private string intensity = "Fixed Intensity"; @@ -199,13 +219,26 @@ private void OnAdvancedSettingsClick() { - _AdvancedSettingsExpanded = !_AdvancedSettingsExpanded; + _advancedSettingsExpanded = !_advancedSettingsExpanded; } - private void OnParamsChange() + private void OnParamsChange(bool shockOscParam) { // check Debug page is active - if (activePageIndex == 2) + if (activePageIndex != 2) + return; + + // only redraw page when needed + if (!_showAllAvatarParams && !shockOscParam) + return; + + InvokeAsync(StateHasChanged); + } + + private void OnLogAdded() + { + // check Log page is active + if (activePageIndex == 3) InvokeAsync(StateHasChanged); } @@ -245,7 +278,8 @@ ShockOsc.SetAuthLoading = SetAuthLoading; ShockOsc.OnParamsChange = OnParamsChange; MySink.NotificationAction = MsgNoty; - + LogStore.OnLogAdded = OnLogAdded; + _config = Config.ConfigInstance; intensity = _config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; duration = _config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; diff --git a/ShockOsc/UserHubClient.cs b/ShockOsc/UserHubClient.cs index b192bc2..b411da0 100644 --- a/ShockOsc/UserHubClient.cs +++ b/ShockOsc/UserHubClient.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenShock.ShockOsc.Models; using Serilog; @@ -15,11 +14,11 @@ public static class UserHubClient private static HubConnection? Connection; - public static Task InitializeAsync() + public static async Task InitializeAsync() { Connection?.DisposeAsync(); Connection = new HubConnectionBuilder() - .WithUrl(Config.ConfigInstance.ShockLink.UserHub, HttpTransportType.WebSockets, + .WithUrl($"{Config.ConfigInstance.ShockLink.OpenShockApi}/hubs/user", HttpTransportType.WebSockets, options => { options.Headers.Add("OpenShockToken", Config.ConfigInstance.ShockLink.ApiToken); }) .WithAutomaticReconnect() .ConfigureLogging(builder => @@ -40,8 +39,7 @@ public static Task InitializeAsync() { ShockOsc.SetAuthLoading?.Invoke(false, true); }; - Connection.StartAsync(); - return Task.CompletedTask; + await Connection.StartAsync(); } public static Task? Control(params Control[] data) => Connection?.SendAsync("ControlV2", data, "ShockOsc"); diff --git a/ShockOsc/WebRequestApi.cs b/ShockOsc/WebRequestApi.cs new file mode 100644 index 0000000..d7f1bd7 --- /dev/null +++ b/ShockOsc/WebRequestApi.cs @@ -0,0 +1,73 @@ +using Serilog; +using System.Net; +using System.Text; + +namespace OpenShock.ShockOsc; + +public static class WebRequestApi +{ + private static readonly ILogger Logger = Log.ForContext(typeof(WebRequestApi)); + private static readonly HttpClient client; + + static WebRequestApi() + { + ServicePointManager.DefaultConnectionLimit = 10; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + + var handler = new HttpClientHandler(); + client = new HttpClient(handler) + { + BaseAddress = Config.ConfigInstance.ShockLink.OpenShockApi + }; + } + + public class RequestData + { + public string url = string.Empty; + public HttpMethod method = HttpMethod.Get; + public string? body; + public Dictionary? headers; + } + + public static async Task<(HttpStatusCode, string)> DoRequest(RequestData requestData) + { + var request = new HttpRequestMessage(requestData.method, requestData.url); + request.Headers.Add("User-Agent", "ShockOSC"); + request.Headers.Add("OpenShockToken", Config.ConfigInstance.ShockLink.ApiToken); + if (requestData.headers != null) + { + foreach (var header in requestData.headers) + { + request.Headers.Add(header.Key, header.Value); + } + } + + if (requestData.headers != null && requestData.body != null && !requestData.headers.ContainsKey("Content-Type")) + { + request.Headers.Add("Content-Type", "application/json; charset=utf-8"); + } + + if (requestData.body != null) + { + request.Content = new StringContent(requestData.body, Encoding.UTF8, "application/json"); + } + + try + { + var response = await client.SendAsync(request); + var responseString = await response.Content.ReadAsStringAsync(); + Logger.Information($"{request.RequestUri} {responseString}"); + return (response.StatusCode, responseString); + } + catch (WebException webException) + { + if (webException.Response is HttpWebResponse response) + { + var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd(); + return (response.StatusCode, responseString); + } + } + + return (HttpStatusCode.InternalServerError, string.Empty); + } +} \ No newline at end of file diff --git a/ShockOsc/wwwroot/app.css b/ShockOsc/wwwroot/app.css index dd7cc8e..53a80f9 100644 --- a/ShockOsc/wwwroot/app.css +++ b/ShockOsc/wwwroot/app.css @@ -18,12 +18,16 @@ body { min-width: 600px; } +#login-background { + background-color: #272727; +} + .mud-tabs-toolbar { - background-color: #272727 !important; + background-color: #1f1f1f !important; } .mud-tabs-panels { - background-color: #2f2f2f !important; + background-color: #272727 !important; } .option-width { From 70a89fe20b3efbb563a5d9ed465aceb9852e596c Mon Sep 17 00:00:00 2001 From: Natsumi Date: Tue, 12 Mar 2024 02:28:47 +1300 Subject: [PATCH 07/95] Cleanup auth state handling --- ShockOsc/LoggerSink.cs | 3 --- ShockOsc/ShockOsc.cs | 26 +++++++++++++++++++------ ShockOsc/Ui/Components/MainLayout.razor | 7 ++++--- ShockOsc/UserHubClient.cs | 8 ++++---- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/ShockOsc/LoggerSink.cs b/ShockOsc/LoggerSink.cs index e8c8a82..ee618d5 100644 --- a/ShockOsc/LoggerSink.cs +++ b/ShockOsc/LoggerSink.cs @@ -56,10 +56,7 @@ public void Emit(LogEvent logEvent) var logMessage = _textWriter.ToString(); if (string.IsNullOrEmpty(logMessage)) return; if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) - { - OpenShock.ShockOsc.ShockOsc.SetAuthLoading?.Invoke(false, false); NotificationAction?.Invoke(logMessage[82..], Severity.Error); - } Debug.WriteLine(logMessage); LogStore.AddLog(logMessage); diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index fc4ca5a..e33310b 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -37,11 +37,19 @@ public static class ShockOsc "IShock" }; + public enum AuthState + { + NotAuthenticated, + Authenticating, + Authenticated + } + public static Dictionary ParamsInUse = new(); public static Dictionary AllAvatarParams = new(); public static Action? OnParamsChange; - public static Action? SetAuthLoading; + public static Action? SetAuthLoading; + public static AuthState CurrentAuthState = AuthState.NotAuthenticated; public static async Task StartMain() { @@ -124,20 +132,20 @@ public static async Task StartMain() private static void ConnectToHub() { _logger.Information("Init user hub..."); - SetAuthLoading?.Invoke(false, false); + SetAuthSate(AuthState.NotAuthenticated); if (string.IsNullOrEmpty(Config.ConfigInstance.ShockLink.ApiToken)) return; - - SetAuthLoading?.Invoke(false, true); + + SetAuthSate(AuthState.Authenticating); UserHubClient.InitializeAsync().ContinueWith(task => { if (task.IsFaulted) - SetAuthLoading?.Invoke(false, false); + SetAuthSate(AuthState.NotAuthenticated); if (task.IsCompletedSuccessfully) { ShockLinkApi.GetShockers(); - SetAuthLoading?.Invoke(true, false); + SetAuthSate(AuthState.Authenticated); } }); } @@ -149,6 +157,12 @@ public static void ClickLogin() ConnectToHub(); } + public static void SetAuthSate(AuthState state) + { + CurrentAuthState = state; + SetAuthLoading?.Invoke(state); + } + private static void OnParamChange(bool shockOscParam) { if (OnParamsChange == null) diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 304ae0f..ffd9d32 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -266,10 +266,10 @@ } } - private void SetAuthLoading(bool auth, bool loading) + private void SetAuthLoading(ShockOsc.AuthState authState) { - _authenticated = auth; - _loading = loading; + _authenticated = authState == ShockOsc.AuthState.Authenticated; + _loading = authState == ShockOsc.AuthState.Authenticating; InvokeAsync(StateHasChanged); } @@ -280,6 +280,7 @@ MySink.NotificationAction = MsgNoty; LogStore.OnLogAdded = OnLogAdded; + SetAuthLoading(ShockOsc.CurrentAuthState); _config = Config.ConfigInstance; intensity = _config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; duration = _config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; diff --git a/ShockOsc/UserHubClient.cs b/ShockOsc/UserHubClient.cs index b411da0..d7d4241 100644 --- a/ShockOsc/UserHubClient.cs +++ b/ShockOsc/UserHubClient.cs @@ -35,9 +35,9 @@ public static async Task InitializeAsync() .Build(); Connection.On>("Log", LogReceive); Connection.On("Welcome", WelcomeReceive); - Connection.Closed += async exception => - { - ShockOsc.SetAuthLoading?.Invoke(false, true); + Connection.Closed += exception => { + ShockOsc.SetAuthSate(ShockOsc.AuthState.NotAuthenticated); + return Task.CompletedTask; }; await Connection.StartAsync(); } @@ -49,7 +49,7 @@ public static async Task InitializeAsync() private static Task WelcomeReceive(string connectionId) { ConnectionId = connectionId; - ShockOsc.SetAuthLoading?.Invoke(true, false); + ShockOsc.SetAuthSate(ShockOsc.AuthState.Authenticated); return Task.CompletedTask; } From 62f8306bba3ac5bb48cb82beea8e3bab755b34e5 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Wed, 13 Mar 2024 22:40:42 +1300 Subject: [PATCH 08/95] Add updater --- ShockOsc/ShockOsc.cs | 21 ++-- ShockOsc/ShockOsc.csproj | 4 +- ShockOsc/Ui/Components/Layout/Logo.razor | 2 +- ShockOsc/Ui/Components/MainLayout.razor | 13 ++- ShockOsc/Ui/Components/UpdateDialog.razor | 45 +++++++++ ShockOsc/Ui/Components/UpdateLogout.razor | 41 ++++++++ ShockOsc/Updater.cs | 117 ++++++++++------------ ShockOsc/UserHubClient.cs | 2 + ShockOsc/WebRequestApi.cs | 11 +- 9 files changed, 169 insertions(+), 87 deletions(-) create mode 100644 ShockOsc/Ui/Components/UpdateDialog.razor create mode 100644 ShockOsc/Ui/Components/UpdateLogout.razor diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index e33310b..cbf5aeb 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -82,19 +82,6 @@ public static async Task StartMain() _logger.Information("Starting ShockOsc version {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "error"); - try - { - if (await Updater.CheckUpdate()) - { - _logger.Information("Terminating due to update"); - return; - } - } - catch (Exception e) - { - _logger.Error(e, "Unknown error in updater"); - } - _logger.Information("Found shockers: {Shockers}", Config.ConfigInstance.ShockLink.Shockers.Select(x => x.Key)); ConnectToHub(); @@ -150,6 +137,14 @@ private static void ConnectToHub() }); } + public static void Logout() + { + Config.ConfigInstance.ShockLink.ApiToken = string.Empty; + Config.Save(); + UserHubClient.Disconnect(); + SetAuthSate(AuthState.NotAuthenticated); + } + public static void ClickLogin() { Config.Save(); diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 36b1075..dc0f266 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -17,8 +17,8 @@ OpenShock.ShockOsc OpenShock.ShockOsc OpenShock - 1.8.3 - 1.8.3 + 2.0.1 + 2.0.1 Resources\openshock-icon.ico true ShockOsc diff --git a/ShockOsc/Ui/Components/Layout/Logo.razor b/ShockOsc/Ui/Components/Layout/Logo.razor index 72fd587..3374757 100644 --- a/ShockOsc/Ui/Components/Layout/Logo.razor +++ b/ShockOsc/Ui/Components/Layout/Logo.razor @@ -5,4 +5,4 @@ ShockOSC - \ No newline at end of file + diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index ffd9d32..c68b349 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -40,6 +40,7 @@ } + @* @@ -47,7 +48,7 @@ @if (!_authenticated) { - + } else { @@ -74,7 +75,14 @@

- @(_advancedSettingsExpanded ? "Advanced Settings" : "Advanced Settings") + @if (_advancedSettingsExpanded) + { + Advanced Settings + } + else + { + Advanced Settings + } @@ -216,7 +224,6 @@ private bool _authenticated = false; - private void OnAdvancedSettingsClick() { _advancedSettingsExpanded = !_advancedSettingsExpanded; diff --git a/ShockOsc/Ui/Components/UpdateDialog.razor b/ShockOsc/Ui/Components/UpdateDialog.razor new file mode 100644 index 0000000..bdaa727 --- /dev/null +++ b/ShockOsc/Ui/Components/UpdateDialog.razor @@ -0,0 +1,45 @@ +@code { + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } + + private bool _isDownloading = false; + + private void Skip() + { + Config.ConfigInstance.LastIgnoredVersion = Updater.LatestVersion; + Config.Save(); + MudDialog.Close(DialogResult.Ok(true)); + } + + private void DownloadUpdate() + { + _isDownloading = true; + Updater.DoUpdate(); + } +} + + + + @if (!_isDownloading) + { + Update Available +
+ A new version of ShockOSC is available. Would you like to update? + } + else + { + Downloading Update +
+ Please wait while the update is downloaded. +
+ + } +
+ + @if (!_isDownloading) + { + Skip + Update + } + +
\ No newline at end of file diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Components/UpdateLogout.razor new file mode 100644 index 0000000..6965851 --- /dev/null +++ b/ShockOsc/Ui/Components/UpdateLogout.razor @@ -0,0 +1,41 @@ +@inject IDialogService Dialog +@inject IDialogService DialogService + +@code { + [Parameter] + public bool Authenticated { get; set; } + + private DialogOptions dialogOptions = new DialogOptions() { NoHeader = true, DisableBackdropClick = true }; + + private void OpenUpdateDialog() + { + DialogService.Show("Update", dialogOptions); + } + + protected override async Task OnInitializedAsync() + { + if (await Updater.CheckUpdate()) + { + OpenUpdateDialog(); + } + } +} + + + @if (Updater.UpdateAvailable) + { + + + + + + } + @if (Authenticated) + { + + + + + + } + \ No newline at end of file diff --git a/ShockOsc/Updater.cs b/ShockOsc/Updater.cs index b109f1c..d91a2bc 100644 --- a/ShockOsc/Updater.cs +++ b/ShockOsc/Updater.cs @@ -11,12 +11,14 @@ public static class Updater private static readonly ILogger Logger = Log.ForContext(typeof(Updater)); private static readonly HttpClient HttpClient = new(); private const string GithubLatest = "https://api.github.com/repos/OpenShock/ShockOsc/releases/latest"; - private const string CurrentFileName = "OpenShock.ShockOsc.exe"; - private const string OldFileName = "OpenShock.ShockOsc.old.exe"; - private static readonly string OldFilePath = Path.Combine(Environment.CurrentDirectory, OldFileName); - private static readonly string CurrentFilePath = Path.Combine(Environment.CurrentDirectory, CurrentFileName); + private const string SetupFileName = "ShockOSC_Setup.exe"; // OpenShock.ShockOsc.exe + private static readonly string SetupFilePath = Path.Combine(Environment.CurrentDirectory, SetupFileName); private static readonly Version CurrentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? throw new Exception("Could not determine ShockOsc version"); + public static bool UpdateAvailable { get; private set; } + public static Version LatestVersion { get; private set; } + public static Uri LatestDownloadUrl { get; private set; } + static Updater() { HttpClient.DefaultRequestHeaders.Add("User-Agent", $"ShockOsc/{CurrentVersion}"); @@ -38,40 +40,44 @@ private static bool TryDeleteFile(string fileName) private static async Task<(Version, GithubReleaseResponse.Asset)?> GetLatestRelease() { - //TryDeleteFile(Path.Combine(Environment.CurrentDirectory, "ShockLink.ShockOsc.exe")); - //TryDeleteFile(Path.Combine(Environment.CurrentDirectory, "ShockLink.ShockOsc.old.exe")); - TryDeleteFile(OldFilePath); - Logger.Information("Checking GitHub for updates..."); - var res = await HttpClient.GetAsync(GithubLatest); - if (!res.IsSuccessStatusCode) - { - Logger.Warning("Failed to get latest version information from GitHub. {StatusCode}", res.StatusCode); - return null; - } - - var json = await JsonSerializer.DeserializeAsync(await res.Content.ReadAsStreamAsync()); - if (json == null) - { - Logger.Warning("Could not deserialize json"); - return null; - } - - if (!Version.TryParse(json.TagName[1..], out var version)) + try { - Logger.Warning("Failed to parse version. Value: {Version}", json.TagName); - return null; + var res = await HttpClient.GetAsync(GithubLatest); + if (!res.IsSuccessStatusCode) + { + Logger.Warning("Failed to get latest version information from GitHub. {StatusCode}", res.StatusCode); + return null; + } + + var json = await JsonSerializer.DeserializeAsync(await res.Content.ReadAsStreamAsync()); + if (json == null) + { + Logger.Warning("Could not deserialize json"); + return null; + } + + if (!Version.TryParse(json.TagName[1..], out var version)) + { + Logger.Warning("Failed to parse version. Value: {Version}", json.TagName); + return null; + } + + var asset = json.Assets.FirstOrDefault(x => x.Name == SetupFileName); + if (asset == null) + { + Logger.Warning("Could not find asset with {@SetupName}. Assets found: {@Assets}", SetupFileName, json.Assets); + return null; + } + + return (version, asset); } - - var asset = json.Assets.FirstOrDefault(x => x.Name == "OpenShock.ShockOsc.exe"); - if (asset == null) + catch (Exception e) { - Logger.Warning("Could not find asset with OpenShock.ShockOsc.exe. Assets found: {@Assets}", json.Assets); + Logger.Warning(e, "Failed to get latest version information from GitHub"); return null; } - - return (version, asset); } public static async Task CheckUpdate() @@ -81,9 +87,13 @@ public static async Task CheckUpdate() if (latestVersion.Value.Item1 <= CurrentVersion) { Logger.Information("ShockOsc is up to date ([{Version}] >= [{LatestVersion}])", CurrentVersion, latestVersion.Value.Item1); + UpdateAvailable = false; return false; } + UpdateAvailable = true; + LatestVersion = latestVersion.Value.Item1; + LatestDownloadUrl = latestVersion.Value.Item2.BrowserDownloadUrl; if (Config.ConfigInstance.LastIgnoredVersion != null && Config.ConfigInstance.LastIgnoredVersion >= latestVersion.Value.Item1) { @@ -92,50 +102,28 @@ public static async Task CheckUpdate() } Logger.Warning( - "ShockOsc is not up to date. Newest version is [{NewVersion}] but you are on [{CurrentVersion}]!\nDo you wish to update it?\n[Y]es, [N]o, [D]ont ask (Yes)", + "ShockOsc is not up to date. Newest version is [{NewVersion}] but you are on [{CurrentVersion}]!", latestVersion.Value.Item1, CurrentVersion); - var input = Console.ReadLine()?.ToLowerInvariant(); - var inputChar = 'y'; - if (input?.Length > 0) inputChar = input[0]; - - switch (inputChar) - { - case 'y': - return await DoUpdate(latestVersion.Value.Item2.BrowserDownloadUrl); - case 'd': - Config.ConfigInstance.LastIgnoredVersion = latestVersion.Value.Item1; - Config.Save(); - Logger.Information("Postponed update and turned off asking until next version"); - break; - case 'n': - Logger.Information("Postponed update"); - break; - } - - return false; + return true; } - private static async Task DoUpdate(Uri downloadUri) + public static async Task DoUpdate() { Logger.Information("Starting update..."); - - - try + if (LatestVersion == null || LatestDownloadUrl == null) { - Logger.Debug("Moving current file to old"); - File.Move(CurrentFilePath, OldFilePath, true); - } - catch (Exception e) - { - Logger.Warning(e, "Failed to move file, probably doesnt exist, proceeding like normal..."); + Logger.Error("LatestVersion or LatestDownloadUrl is null. Cannot update"); + return; } + TryDeleteFile(SetupFilePath); + Logger.Debug("Downloading new release..."); var sp = Stopwatch.StartNew(); - await using (var stream = await HttpClient.GetStreamAsync(downloadUri)) + await using (var stream = await HttpClient.GetStreamAsync(LatestDownloadUrl)) { - await using var fStream = new FileStream(CurrentFilePath, FileMode.OpenOrCreate); + await using var fStream = new FileStream(SetupFilePath, FileMode.OpenOrCreate); await stream.CopyToAsync(fStream); } @@ -144,11 +132,10 @@ private static async Task DoUpdate(Uri downloadUri) await Task.Delay(1000); var startInfo = new ProcessStartInfo { - FileName = CurrentFilePath, + FileName = SetupFilePath, UseShellExecute = true }; Process.Start(startInfo); Environment.Exit(0); - return true; } } \ No newline at end of file diff --git a/ShockOsc/UserHubClient.cs b/ShockOsc/UserHubClient.cs index d7d4241..f5a3ff4 100644 --- a/ShockOsc/UserHubClient.cs +++ b/ShockOsc/UserHubClient.cs @@ -44,6 +44,8 @@ public static async Task InitializeAsync() public static Task? Control(params Control[] data) => Connection?.SendAsync("ControlV2", data, "ShockOsc"); + public static void Disconnect() => Connection?.DisposeAsync(); + #region Handlers private static Task WelcomeReceive(string connectionId) diff --git a/ShockOsc/WebRequestApi.cs b/ShockOsc/WebRequestApi.cs index d7f1bd7..30dcf90 100644 --- a/ShockOsc/WebRequestApi.cs +++ b/ShockOsc/WebRequestApi.cs @@ -1,5 +1,6 @@ using Serilog; using System.Net; +using System.Reflection; using System.Text; namespace OpenShock.ShockOsc; @@ -8,6 +9,7 @@ public static class WebRequestApi { private static readonly ILogger Logger = Log.ForContext(typeof(WebRequestApi)); private static readonly HttpClient client; + private static readonly Version currentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? throw new Exception("Could not determine ShockOsc version"); static WebRequestApi() { @@ -17,7 +19,12 @@ static WebRequestApi() var handler = new HttpClientHandler(); client = new HttpClient(handler) { - BaseAddress = Config.ConfigInstance.ShockLink.OpenShockApi + BaseAddress = Config.ConfigInstance.ShockLink.OpenShockApi, + DefaultRequestHeaders = + { + {"User-Agent", $"ShockOsc/{currentVersion}"}, + {"OpenShockToken", Config.ConfigInstance.ShockLink.ApiToken} + } }; } @@ -32,8 +39,6 @@ public class RequestData public static async Task<(HttpStatusCode, string)> DoRequest(RequestData requestData) { var request = new HttpRequestMessage(requestData.method, requestData.url); - request.Headers.Add("User-Agent", "ShockOSC"); - request.Headers.Add("OpenShockToken", Config.ConfigInstance.ShockLink.ApiToken); if (requestData.headers != null) { foreach (var header in requestData.headers) From fdea2ba0f9a6cc5601849b0653a7371991bc80a3 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Thu, 14 Mar 2024 02:19:44 +1300 Subject: [PATCH 09/95] Update .NET deps --- ShockOsc/ShockOsc.csproj | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index dc0f266..090f201 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -1,4 +1,4 @@ - + Exe @@ -47,20 +47,20 @@
- - - - - + + + + + - - - - + + + + - + From d2d3c0da6c6f897e827612add2c67b7bb60bedac Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 15 Mar 2024 07:51:46 +0100 Subject: [PATCH 10/95] Rework log sink and log page --- ShockOsc/LoggerSink.cs | 91 ------------ ShockOsc/Logging/LogStore.cs | 31 +++++ ShockOsc/Logging/UiLogSink.cs | 73 ++++++++++ ShockOsc/ShockOsc.cs | 11 +- ShockOsc/Ui/Components/MainLayout.razor | 177 ++++++++++++++++-------- ShockOsc/Utils/UiUtils.cs | 24 ++++ 6 files changed, 254 insertions(+), 153 deletions(-) delete mode 100644 ShockOsc/LoggerSink.cs create mode 100644 ShockOsc/Logging/LogStore.cs create mode 100644 ShockOsc/Logging/UiLogSink.cs create mode 100644 ShockOsc/Utils/UiUtils.cs diff --git a/ShockOsc/LoggerSink.cs b/ShockOsc/LoggerSink.cs deleted file mode 100644 index ee618d5..0000000 --- a/ShockOsc/LoggerSink.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Serilog.Configuration; -using Serilog.Core; -using Serilog.Events; -using Serilog.Formatting.Display; -using Serilog.Formatting; -using System.Diagnostics; -using MudBlazor; - -namespace Serilog; - -public class LogStore -{ - public static List Logs = new(); - public static Action? OnLogAdded; - - public static void AddLog(string log) - { - if (Logs.Count == 0) - { - Logs.Add(new LogEntry { Time = DateTime.Now, Message = log }); - } - else - { - // add to start of list - Logs.Insert(0, new LogEntry { Time = DateTime.Now, Message = log }); - } - OnLogAdded?.Invoke(); - } - - public class LogEntry - { - public DateTime Time { get; set; } - public string Message { get; set; } - } -} - -public class MySink : ILogEventSink -{ - private TextWriter _textWriter; - private readonly ITextFormatter _formatProvider; - public static Action? NotificationAction { get; set; } - - public MySink(ITextFormatter formatProvider) - { - SinkExtensions.Instance = this; - _formatProvider = formatProvider; - } - - public void Emit(LogEvent logEvent) - { - try - { - _textWriter = new StringWriter(); - _formatProvider.Format(logEvent, _textWriter); - // var logMessage = logEvent.RenderMessage(_formatProvider); - var logMessage = _textWriter.ToString(); - if (string.IsNullOrEmpty(logMessage)) return; - if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) - NotificationAction?.Invoke(logMessage[82..], Severity.Error); - - Debug.WriteLine(logMessage); - LogStore.AddLog(logMessage); - _textWriter.Flush(); - } - catch (Exception) - { - // this kinda sucks - } - } -} - -public static class SinkExtensions -{ - public static MySink? Instance; - - const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"; - - public static LoggerConfiguration MySink( - this LoggerSinkConfiguration sinkConfiguration, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - string outputTemplate = DefaultOutputTemplate, - IFormatProvider formatProvider = null, - LoggingLevelSwitch levelSwitch = null) - { - if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); - - var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); - var sink = new MySink(formatter); - return sinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); - } -} \ No newline at end of file diff --git a/ShockOsc/Logging/LogStore.cs b/ShockOsc/Logging/LogStore.cs new file mode 100644 index 0000000..b7a642b --- /dev/null +++ b/ShockOsc/Logging/LogStore.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; +using Serilog.Events; + +namespace OpenShock.ShockOsc.Logging; + +public static class LogStore +{ + public static readonly ConcurrentQueue Logs = new(); + public static Action? OnLogAdded; + + public static void AddLog(LogEntry log) + { + Logs.Enqueue(log); + if(Logs.Count > 1000) Logs.TryDequeue(out _); + + OnLogAdded?.Invoke(); + } + + public class LogEntry + { + public required LogEventLevel Level { get; init; } + public required DateTimeOffset Time { get; init; } + public required string Message { get; init; } + public required string SourceContext { get; init; } + public required string SourceContextShort { get; init; } + + // UI Data + + public bool IsExpanded { get; set; } = false; + } +} \ No newline at end of file diff --git a/ShockOsc/Logging/UiLogSink.cs b/ShockOsc/Logging/UiLogSink.cs new file mode 100644 index 0000000..b604ad4 --- /dev/null +++ b/ShockOsc/Logging/UiLogSink.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; +using MudBlazor; +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Formatting.Display; + +namespace OpenShock.ShockOsc.Logging; + +public class UiLogSink : ILogEventSink +{ + private readonly ITextFormatter _messageProvider; + private readonly ITextFormatter _sourceContextProvider; + public static Action? NotificationAction { get; set; } + + public UiLogSink() + { + _messageProvider = new MessageTemplateTextFormatter("{Message:lj} {NewLine}{Exception}"); + _sourceContextProvider = new MessageTemplateTextFormatter("{SourceContext}"); + } + + public void Emit(LogEvent logEvent) + { + try + { + using var textWriter = new StringWriter(); + _messageProvider.Format(logEvent, textWriter); + // var logMessage = logEvent.RenderMessage(_formatProvider); + var logMessage = textWriter.ToString(); + if (string.IsNullOrEmpty(logMessage)) return; + if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) + NotificationAction?.Invoke(logMessage[82..], Severity.Error); + + Debug.WriteLine(logMessage); + + var sourceContextString = string.Empty; + + if (logEvent.Properties.TryGetValue("SourceContext", out var sourceContext) && sourceContext is ScalarValue + { + Value: string scalarString + }) sourceContextString = scalarString; + + var lastIndexSlash = Math.Min(sourceContextString.Length, sourceContextString.LastIndexOf('.') + 1); + + LogStore.AddLog(new LogStore.LogEntry + { + Message = logMessage, + Time = logEvent.Timestamp, + Level = logEvent.Level, + SourceContext = sourceContextString, + SourceContextShort = sourceContextString[lastIndexSlash..] + }); + } + catch (Exception) + { + // this kinda sucks + } + } +} + +public static class UiLogSinkExtensions +{ + + public static LoggerConfiguration UiLogSink( + this LoggerSinkConfiguration sinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + var sink = new UiLogSink(); + return sinkConfiguration.Sink(sink, restrictedToMinimumLevel); + } +} \ No newline at end of file diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index cbf5aeb..1c14bf1 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -3,6 +3,7 @@ using System.Net; using System.Reflection; using LucHeart.CoreOSC; +using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.OscChangeTracker; using OpenShock.ShockOsc.OscQueryLibrary; @@ -56,8 +57,7 @@ public static async Task StartMain() Log.Logger = new LoggerConfiguration() .Filter.ByExcluding(ev => ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")) - .WriteTo.MySink(LogEventLevel.Information, - "[{SourceContext}] {Message:lj} {NewLine}{Exception}") + .WriteTo.UiLogSink(LogEventLevel.Information) .CreateLogger(); // ReSharper disable once RedundantAssignment @@ -72,13 +72,12 @@ public static async Task StartMain() .MinimumLevel.Debug() .Filter.ByExcluding(ev => ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")) - .WriteTo.MySink(LogEventLevel.Debug, - "[{SourceContext}] {Message:lj} {NewLine}{Exception}") + .WriteTo.UiLogSink(LogEventLevel.Debug) .CreateLogger(); } _logger = Log.ForContext(typeof(ShockOsc)); - + _logger.Information("Starting ShockOsc version {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "error"); @@ -356,8 +355,6 @@ private static async Task ReceiveLogic() var shocker = Shockers[shockerName]; var value = received.Arguments.ElementAtOrDefault(0); - _logger.Debug("Received shocker parameter update for [{ShockerName}] state [{State}]", shocker.Name, - value is true); switch (action) { case "IShock": diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index c68b349..a533993 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,18 +1,21 @@ @using System.Text.Json.Serialization @using System.Diagnostics +@using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Layout @using OpenShock.ShockOsc.Models; +@using OpenShock.ShockOsc.Utils @using Serilog +@using Serilog.Events @inject ISnackbar Snackbar @inherits LayoutComponentBase - + @code { private static readonly ILogger Logger = Log.ForContext(typeof(MainLayout)); - + readonly MudTheme _myCustomTheme = new() { Palette = new PaletteDark @@ -37,15 +40,38 @@ }, } }; + } + + -@* + @* *@ - + @if (!_authenticated) { @@ -57,13 +83,13 @@ Chatbox Options - - - + + +
-
- - +
+ + @foreach (Config.Conf.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(Config.Conf.ChatboxConf.HoscyMessageType))) @@ -72,8 +98,8 @@ } -
-
+
+
@if (_advancedSettingsExpanded) { @@ -83,30 +109,30 @@ { Advanced Settings } - + - - - -
+ + + +
@foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) { - - - - + + + + }
- + Shocker Options - + @foreach (Config.Conf.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(Config.Conf.BehaviourConf.BoneHeldAction))) @@ -115,14 +141,14 @@ } - - + +
- - + + -
+
@if (intensity == "Fixed Intensity") { Intensity: @_config.Behaviour.FixedIntensity.ToString()% @@ -134,31 +160,31 @@ Intensity Max: @_config.Behaviour.IntensityRange.Max.ToString()% } - - + + -
+
@if (duration == "Fixed Duration") { - + } else { - - - + + + } -
-
+
+
Other Options - - - - - + + + + +
@@ -177,33 +203,62 @@ List of ShockOSC prefixed parameters and their states AvatarId: @ShockOsc.AvatarId
- - + +
@if (_showAllAvatarParams) { @foreach (var param in ShockOsc.AllAvatarParams) { - + } } else { @foreach (var param in ShockOsc.ParamsInUse) { - + } } - @{int i = 0;} - @foreach (var log in Serilog.LogStore.Logs) - { - if (i > 1000) - break; - i++; - @log.Message - } + + + Time + Source + Message + + + @context.Time.ToString("HH:mm:ss") + @context.SourceContextShort + @context.Message.TruncateAtChar(120) + + + @if (context.IsExpanded) + { + + + +
+ + + + +
+
+ + @context.Message + +
+
+ +
+ } +
+ @* *@ + @* *@ + @* *@ +
@@ -229,6 +284,14 @@ _advancedSettingsExpanded = !_advancedSettingsExpanded; } + private string GetLogClass(LogEventLevel level) + => $"log {level.ToString().ToLowerInvariant()}"; + + private void LogRowClick(TableRowClickEventArgs rowClickEventArgs) + { + rowClickEventArgs.Item.IsExpanded = !rowClickEventArgs.Item.IsExpanded; + } + private void OnParamsChange(bool shockOscParam) { // check Debug page is active @@ -284,7 +347,7 @@ { ShockOsc.SetAuthLoading = SetAuthLoading; ShockOsc.OnParamsChange = OnParamsChange; - MySink.NotificationAction = MsgNoty; + UiLogSink.NotificationAction = MsgNoty; LogStore.OnLogAdded = OnLogAdded; SetAuthLoading(ShockOsc.CurrentAuthState); @@ -292,4 +355,8 @@ intensity = _config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; duration = _config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; } + + private string RowClassFunc(LogStore.LogEntry? log, int arg2) => log == null ? string.Empty : GetLogClass(log.Level) + (log.IsExpanded ? " expanded" : ""); + + } \ No newline at end of file diff --git a/ShockOsc/Utils/UiUtils.cs b/ShockOsc/Utils/UiUtils.cs new file mode 100644 index 0000000..a8b5ca8 --- /dev/null +++ b/ShockOsc/Utils/UiUtils.cs @@ -0,0 +1,24 @@ +namespace OpenShock.ShockOsc.Utils; + +public static class UiUtils +{ + public static string? TruncateAtWord(this string? input, int length) + { + if (input == null || input.Length < length) + return input; + + var iNextSpace = input.LastIndexOf(" ", length, StringComparison.InvariantCultureIgnoreCase); + + return $"{input[..(iNextSpace > 0 ? iNextSpace : length)].Trim()}..."; + } + + public static string? TruncateAtChar(this string? input, int length) + { + if (input == null || input.Length < length) + return input; + + var max = Math.Min(input.Length, length); + + return $"{input[..max].Trim()}..."; + } +} \ No newline at end of file From 4b64da6b4424b449fe8587be155f79e5d1390a31 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Sat, 16 Mar 2024 06:26:44 +1300 Subject: [PATCH 11/95] Shocker nicknames --- ShockOsc/Config.cs | 11 ++++- ShockOsc/ShockLinkApi.cs | 14 ++++++- ShockOsc/ShockOsc.cs | 36 ++++++++++++----- ShockOsc/Ui/Components/MainLayout.razor | 53 ++++++++++++++++++------- ShockOsc/Updater.cs | 4 +- 5 files changed, 89 insertions(+), 29 deletions(-) diff --git a/ShockOsc/Config.cs b/ShockOsc/Config.cs index 9c6687f..77b8c57 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/Config.cs @@ -16,6 +16,7 @@ public static class Config static Config() { TryLoad(); + ShockOsc.RefreshShockers(); } private static void TryLoad() @@ -110,7 +111,7 @@ public static void Save() }, ShockLink = new Conf.OpenShockConf { - Shockers = new Dictionary(), + Shockers = new Dictionary(), OpenShockApi = null!, ApiToken = "", } @@ -229,7 +230,13 @@ public class OpenShockConf { public Uri OpenShockApi { get; set; } = new("https://api.shocklink.net/1"); public required string ApiToken { get; set; } - public required IReadOnlyDictionary Shockers { get; set; } + public required IReadOnlyDictionary Shockers { get; set; } + } + + public class ShockerConf + { + public required string NickName { get; set; } + public required Guid Id { get; set; } } } } \ No newline at end of file diff --git a/ShockOsc/ShockLinkApi.cs b/ShockOsc/ShockLinkApi.cs index 66af608..8474419 100644 --- a/ShockOsc/ShockLinkApi.cs +++ b/ShockOsc/ShockLinkApi.cs @@ -39,13 +39,23 @@ public static async Task GetShockers() } // populate config - var shockerList = new Dictionary(); + var shockerList = new Dictionary(); foreach (var shocker in Shockers) { - shockerList.Add(shocker.Name, shocker.Id); + // get nickname from config + var nickName = string.Empty; + if (Config.ConfigInstance.ShockLink.Shockers.TryGetValue(shocker.Name, out var confShocker)) + nickName = confShocker.NickName; + + shockerList.Add(shocker.Name, new Config.Conf.ShockerConf + { + NickName = nickName, + Id = shocker.Id, + }); } Config.ConfigInstance.ShockLink.Shockers = shockerList; Config.Save(); + ShockOsc.RefreshShockers(); } else { diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 1c14bf1..1d182da 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -81,7 +81,14 @@ public static async Task StartMain() _logger.Information("Starting ShockOsc version {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "error"); - _logger.Information("Found shockers: {Shockers}", Config.ConfigInstance.ShockLink.Shockers.Select(x => x.Key)); + var shockerList = ""; + foreach (var shocker in Config.ConfigInstance.ShockLink.Shockers) + { + shockerList += string.IsNullOrEmpty(shocker.Value.NickName) + ? $"{shocker.Key}, " + : $"{shocker.Value.NickName}, "; + } + _logger.Information("Found shockers: {Shockers}", shockerList); ConnectToHub(); @@ -159,10 +166,7 @@ public static void SetAuthSate(AuthState state) private static void OnParamChange(bool shockOscParam) { - if (OnParamsChange == null) - return; - - OnParamsChange.Invoke(shockOscParam); + OnParamsChange?.Invoke(shockOscParam); } private static void FoundVrcClient() @@ -181,13 +185,27 @@ private static void FoundVrcClient() OsTask.Run(SenderLoopAsync); OsTask.Run(CheckLoop); + _logger.Information("Ready"); + OsTask.Run(UnderscoreConfig.SendUpdateForAll); + } + + public static void RefreshShockers() + { Shockers.Clear(); Shockers.TryAdd("_All", new Shocker(Guid.Empty, "_All")); - foreach (var (shockerName, shockerId) in Config.ConfigInstance.ShockLink.Shockers) - Shockers.TryAdd(shockerName, new Shocker(shockerId, shockerName)); + foreach (var (shockerName, shocker) in Config.ConfigInstance.ShockLink.Shockers) + { + if (string.IsNullOrEmpty(shocker.NickName)) + Shockers.TryAdd(shockerName, new Shocker(shocker.Id, shockerName)); + else + Shockers.TryAdd(shocker.NickName, new Shocker(shocker.Id, shocker.NickName)); + } + } - _logger.Information("Ready"); - OsTask.Run(UnderscoreConfig.SendUpdateForAll); + public static void SaveShockers() + { + RefreshShockers(); + Config.Save(); } private static void OnAvatarChange(Dictionary? parameters, string avatarId) diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index a533993..e03f3ff 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,6 +1,4 @@ -@using System.Text.Json.Serialization -@using System.Diagnostics -@using OpenShock.ShockOsc.Logging +@using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Layout @using OpenShock.ShockOsc.Models; @using OpenShock.ShockOsc.Utils @@ -45,10 +43,10 @@ @@ -189,15 +185,43 @@ background: linear-gradient(180deg, rgba(2,0,36,0) 0%, rgba(121,19,9,0) 58%, rgb - Refresh - @foreach (var shocker in _config.ShockLink.Shockers) + Refresh + @if (_editShockers) { - // side by side flex - - @shocker.Key - @shocker.Value - + Save } + else + { + Rename + } +
+
+ + + Name + Guid + + + + @if (_editShockers) + { + + } + else + { + @if (string.IsNullOrEmpty(@context.Value.NickName)) + { + @context.Key + } + else + { + @context.Value.NickName + } + } + + @context.Value.Id + +
List of ShockOSC prefixed parameters and their states @@ -268,6 +292,7 @@ background: linear-gradient(180deg, rgba(2,0,36,0) 0%, rgba(121,19,9,0) 58%, rgb @code { private int activePageIndex = 0; private bool _advancedSettingsExpanded = false; + private bool _editShockers = false; private bool _showAllAvatarParams = false; private Config.Conf _config; diff --git a/ShockOsc/Updater.cs b/ShockOsc/Updater.cs index d91a2bc..a33cd87 100644 --- a/ShockOsc/Updater.cs +++ b/ShockOsc/Updater.cs @@ -16,8 +16,8 @@ public static class Updater private static readonly Version CurrentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? throw new Exception("Could not determine ShockOsc version"); public static bool UpdateAvailable { get; private set; } - public static Version LatestVersion { get; private set; } - public static Uri LatestDownloadUrl { get; private set; } + public static Version? LatestVersion { get; private set; } + public static Uri? LatestDownloadUrl { get; private set; } static Updater() { From f8f78b7642f9a9443fc8f25a23b5c9e61c311e46 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Sat, 16 Mar 2024 06:31:54 +1300 Subject: [PATCH 12/95] Minimum window size --- ShockOsc/Ui/App.xaml.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ShockOsc/Ui/App.xaml.cs b/ShockOsc/Ui/App.xaml.cs index 957215a..d03e4d5 100644 --- a/ShockOsc/Ui/App.xaml.cs +++ b/ShockOsc/Ui/App.xaml.cs @@ -17,6 +17,8 @@ protected override Window CreateWindow(IActivationState activationState) if (window != null) { window.Title = "ShockOSC"; + window.MinimumHeight = 600; + window.MinimumWidth = 1000; } return window; From d36249fcfac534f0e72b746b190698730088f754 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 16 Mar 2024 02:21:16 +0100 Subject: [PATCH 13/95] Refactor namespae, limit update rate in params --- ShockOsc/Logging/UiLogSink.cs | 5 ++--- ShockOsc/MauiProgram.cs | 3 ++- ShockOsc/Platforms/Tizen/tizen-manifest.xml | 2 +- ShockOsc/Platforms/Windows/App.xaml | 4 ++-- ShockOsc/Platforms/Windows/App.xaml.cs | 4 ++-- ShockOsc/Platforms/Windows/app.manifest | 2 +- ShockOsc/ShockOsc.cs | 11 ++++++---- ShockOsc/Ui/App.xaml | 3 +-- ShockOsc/Ui/App.xaml.cs | 2 +- ShockOsc/Ui/Components/MainLayout.razor | 24 +++++++++++++++++++-- ShockOsc/Ui/MainPage.xaml | 7 +++--- ShockOsc/Ui/MainPage.xaml.cs | 2 +- 12 files changed, 45 insertions(+), 24 deletions(-) diff --git a/ShockOsc/Logging/UiLogSink.cs b/ShockOsc/Logging/UiLogSink.cs index b604ad4..ddc37be 100644 --- a/ShockOsc/Logging/UiLogSink.cs +++ b/ShockOsc/Logging/UiLogSink.cs @@ -64,10 +64,9 @@ public static class UiLogSinkExtensions { public static LoggerConfiguration UiLogSink( - this LoggerSinkConfiguration sinkConfiguration, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + this LoggerSinkConfiguration sinkConfiguration) { var sink = new UiLogSink(); - return sinkConfiguration.Sink(sink, restrictedToMinimumLevel); + return sinkConfiguration.Sink(sink); } } \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 31fc757..c492f36 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,7 +1,8 @@ using Microsoft.Extensions.Logging; using MudBlazor.Services; +using OpenShock.ShockOsc.Ui; -namespace ShockOsc; +namespace OpenShock.ShockOsc; public static class MauiProgram { diff --git a/ShockOsc/Platforms/Tizen/tizen-manifest.xml b/ShockOsc/Platforms/Tizen/tizen-manifest.xml index 8e1d506..0835032 100644 --- a/ShockOsc/Platforms/Tizen/tizen-manifest.xml +++ b/ShockOsc/Platforms/Tizen/tizen-manifest.xml @@ -1,7 +1,7 @@  - + maui-appicon-placeholder diff --git a/ShockOsc/Platforms/Windows/App.xaml b/ShockOsc/Platforms/Windows/App.xaml index b10c7df..676ebfd 100644 --- a/ShockOsc/Platforms/Windows/App.xaml +++ b/ShockOsc/Platforms/Windows/App.xaml @@ -1,8 +1,8 @@  + xmlns:local="using:OpenShock.ShockOsc.WinUI"> diff --git a/ShockOsc/Platforms/Windows/App.xaml.cs b/ShockOsc/Platforms/Windows/App.xaml.cs index 3ab885b..3edcae2 100644 --- a/ShockOsc/Platforms/Windows/App.xaml.cs +++ b/ShockOsc/Platforms/Windows/App.xaml.cs @@ -1,9 +1,9 @@ -using Microsoft.UI.Xaml; + // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. -namespace ShockOsc.WinUI; +namespace OpenShock.ShockOsc.Platforms.Windows; /// /// Provides application-specific behavior to supplement the default Application class. diff --git a/ShockOsc/Platforms/Windows/app.manifest b/ShockOsc/Platforms/Windows/app.manifest index 3ca1f8a..56bb151 100644 --- a/ShockOsc/Platforms/Windows/app.manifest +++ b/ShockOsc/Platforms/Windows/app.manifest @@ -1,6 +1,6 @@ - + diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 1d182da..b260fe3 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -55,9 +55,11 @@ public enum AuthState public static async Task StartMain() { Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() .Filter.ByExcluding(ev => - ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")) - .WriteTo.UiLogSink(LogEventLevel.Information) + ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter.ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) + .WriteTo.UiLogSink() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); // ReSharper disable once RedundantAssignment @@ -71,8 +73,9 @@ public static async Task StartMain() Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .Filter.ByExcluding(ev => - ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")) - .WriteTo.UiLogSink(LogEventLevel.Debug) + ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter.ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) + .WriteTo.UiLogSink() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); } diff --git a/ShockOsc/Ui/App.xaml b/ShockOsc/Ui/App.xaml index 411f4f6..ac2c6bc 100644 --- a/ShockOsc/Ui/App.xaml +++ b/ShockOsc/Ui/App.xaml @@ -1,8 +1,7 @@  + x:Class="OpenShock.ShockOsc.Ui.App"> diff --git a/ShockOsc/Ui/App.xaml.cs b/ShockOsc/Ui/App.xaml.cs index d03e4d5..e0525fd 100644 --- a/ShockOsc/Ui/App.xaml.cs +++ b/ShockOsc/Ui/App.xaml.cs @@ -1,4 +1,4 @@ -namespace ShockOsc; +namespace OpenShock.ShockOsc.Ui; public partial class App : Application { diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index e03f3ff..6038921 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -326,8 +326,8 @@ // only redraw page when needed if (!_showAllAvatarParams && !shockOscParam) return; - - InvokeAsync(StateHasChanged); + + _updateQueued = true; } private void OnLogAdded() @@ -383,5 +383,25 @@ private string RowClassFunc(LogStore.LogEntry? log, int arg2) => log == null ? string.Empty : GetLogClass(log.Level) + (log.IsExpanded ? " expanded" : ""); + private bool _updateQueued = true; + + protected override void OnInitialized() + { + OsTask.Run(UpdateParams); + } + + private async Task UpdateParams() + { + while (true) + { + await Task.Delay(250); + + if (!_updateQueued) + continue; + _updateQueued = false; + + await InvokeAsync(StateHasChanged); + } + } } \ No newline at end of file diff --git a/ShockOsc/Ui/MainPage.xaml b/ShockOsc/Ui/MainPage.xaml index 40ef064..ae996ca 100644 --- a/ShockOsc/Ui/MainPage.xaml +++ b/ShockOsc/Ui/MainPage.xaml @@ -1,14 +1,13 @@  - + diff --git a/ShockOsc/Ui/MainPage.xaml.cs b/ShockOsc/Ui/MainPage.xaml.cs index 3ad95cd..d33e113 100644 --- a/ShockOsc/Ui/MainPage.xaml.cs +++ b/ShockOsc/Ui/MainPage.xaml.cs @@ -1,4 +1,4 @@ -namespace ShockOsc; +namespace OpenShock.ShockOsc.Ui; public partial class MainPage : ContentPage { From 613702f173152a2a94f729a99ca299caa3fef4c5 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 16 Mar 2024 04:05:26 +0100 Subject: [PATCH 14/95] Impl enable/disable shockers --- ShockOsc/Config.cs | 1 + ShockOsc/ShockLinkApi.cs | 6 ++++++ ShockOsc/ShockOsc.cs | 2 ++ ShockOsc/Ui/Components/MainLayout.razor | 12 +++++++++++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ShockOsc/Config.cs b/ShockOsc/Config.cs index 77b8c57..9663d7f 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/Config.cs @@ -237,6 +237,7 @@ public class ShockerConf { public required string NickName { get; set; } public required Guid Id { get; set; } + public required bool Enabled { get; set; } = true; } } } \ No newline at end of file diff --git a/ShockOsc/ShockLinkApi.cs b/ShockOsc/ShockLinkApi.cs index 8474419..7cd7237 100644 --- a/ShockOsc/ShockLinkApi.cs +++ b/ShockOsc/ShockLinkApi.cs @@ -44,13 +44,19 @@ public static async Task GetShockers() { // get nickname from config var nickName = string.Empty; + var enabled = true; + if (Config.ConfigInstance.ShockLink.Shockers.TryGetValue(shocker.Name, out var confShocker)) + { nickName = confShocker.NickName; + enabled = confShocker.Enabled; + } shockerList.Add(shocker.Name, new Config.Conf.ShockerConf { NickName = nickName, Id = shocker.Id, + Enabled = enabled }); } Config.ConfigInstance.ShockLink.Shockers = shockerList; diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index b260fe3..b47583f 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -198,6 +198,8 @@ public static void RefreshShockers() Shockers.TryAdd("_All", new Shocker(Guid.Empty, "_All")); foreach (var (shockerName, shocker) in Config.ConfigInstance.ShockLink.Shockers) { + if(!shocker.Enabled) continue; + if (string.IsNullOrEmpty(shocker.NickName)) Shockers.TryAdd(shockerName, new Shocker(shocker.Id, shockerName)); else diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 6038921..8985d12 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -198,14 +198,18 @@
+ Enabled Name Guid + + + @if (_editShockers) { - + } else { @@ -342,6 +346,12 @@ Snackbar.Add(msg, severity); } + private Task OnShockerConfigUpdate() + { + ShockOsc.RefreshShockers(); + return Task.CompletedTask; + } + private Task OnSettingsValueChange() { _config.Behaviour.RandomIntensity = intensity == "Random Intensity"; From 2ff4af1e5098fd0faf007b4fd58f59bf5eda5628 Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 4 Apr 2024 18:52:18 +0200 Subject: [PATCH 15/95] Yes --- ShockOsc/Config.cs | 34 ++++--- ShockOsc/MauiProgram.cs | 33 +++++- ShockOsc/Models/Control.cs | 13 --- ShockOsc/Models/ControlLog.cs | 10 -- ShockOsc/Models/ControlLogSender.cs | 8 -- ShockOsc/Models/ControlType.cs | 10 -- ShockOsc/Models/GenericIn.cs | 7 -- ShockOsc/Models/GenericIni.cs | 6 -- ShockOsc/OpenShockApi.cs | 63 ++++++++++++ ShockOsc/ShockLinkApi.cs | 71 ------------- ShockOsc/ShockOsc.cs | 63 +++--------- ShockOsc/ShockOsc.csproj | 11 +- ShockOsc/Ui/App.xaml.cs | 2 - ShockOsc/Ui/Components/Layout/Login.razor | 2 +- ShockOsc/Ui/Components/MainLayout.razor | 36 +++---- ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 106 ++++++++++++++++++++ ShockOsc/UserHubClient.cs | 53 +++++----- ShockOsc/WebRequestApi.cs | 78 -------------- 18 files changed, 288 insertions(+), 318 deletions(-) delete mode 100644 ShockOsc/Models/Control.cs delete mode 100644 ShockOsc/Models/ControlLog.cs delete mode 100644 ShockOsc/Models/ControlLogSender.cs delete mode 100644 ShockOsc/Models/ControlType.cs delete mode 100644 ShockOsc/Models/GenericIn.cs delete mode 100644 ShockOsc/Models/GenericIni.cs create mode 100644 ShockOsc/OpenShockApi.cs delete mode 100644 ShockOsc/ShockLinkApi.cs create mode 100644 ShockOsc/Ui/Components/Tabs/GroupsTab.razor delete mode 100644 ShockOsc/WebRequestApi.cs diff --git a/ShockOsc/Config.cs b/ShockOsc/Config.cs index 9663d7f..b033004 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/Config.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using OpenShock.SDK.CSharp.Models; using OpenShock.ShockOsc.Models; using Serilog; @@ -109,21 +110,32 @@ public static void Save() DisableWhileAfk = true, ForceUnmute = false }, - ShockLink = new Conf.OpenShockConf + OpenShock = new Conf.OpenShockConf { - Shockers = new Dictionary(), - OpenShockApi = null!, - ApiToken = "", - } + Shockers = new Dictionary(), + Token = "", + }, + Groups = new Dictionary() }; public class Conf { public required OscConf Osc { get; set; } public required BehaviourConf Behaviour { get; set; } - public required OpenShockConf ShockLink { get; set; } - public ChatboxConf Chatbox { get; set; } = new(); + public required OpenShockConf OpenShock { get; set; } + public required ChatboxConf Chatbox { get; set; } + + public IDictionary Groups { get; set; } = new Dictionary(); + public Version? LastIgnoredVersion { get; set; } + + + public sealed class Group + { + public required string Name { get; set; } + public IList Shockers { get; set; } = new List(); + } + public class ChatboxConf { @@ -228,15 +240,13 @@ public enum BoneHeldAction public class OpenShockConf { - public Uri OpenShockApi { get; set; } = new("https://api.shocklink.net/1"); - public required string ApiToken { get; set; } - public required IReadOnlyDictionary Shockers { get; set; } + public Uri Backend { get; set; } = new("https://api.shocklink.net"); + public required string Token { get; set; } + public required IReadOnlyDictionary Shockers { get; set; } } public class ShockerConf { - public required string NickName { get; set; } - public required Guid Id { get; set; } public required bool Enabled { get; set; } = true; } } diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index c492f36..e933a2f 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.Logging; using MudBlazor.Services; +using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.Ui; +using Serilog; namespace OpenShock.ShockOsc; @@ -20,7 +22,36 @@ public static MauiApp CreateMauiApp() builder.Services.AddBlazorWebViewDeveloperTools(); builder.Logging.AddDebug(); #endif + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .Filter.ByExcluding(ev => + ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter.ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) + .WriteTo.UiLogSink() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") + .CreateLogger(); - return builder.Build(); + // ReSharper disable once RedundantAssignment + var isDebug = false; +#if DEBUG + isDebug = true; +#endif + if (isDebug) + { + Log.Information("Debug logging enabled"); + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Filter.ByExcluding(ev => + ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter.ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) + .WriteTo.UiLogSink() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + } + + builder.Services.AddSerilog(Log.Logger); + + var mauiApp = builder.Build(); + + return mauiApp; } } \ No newline at end of file diff --git a/ShockOsc/Models/Control.cs b/ShockOsc/Models/Control.cs deleted file mode 100644 index b05a563..0000000 --- a/ShockOsc/Models/Control.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OpenShock.ShockOsc.Models; - -public class Control -{ - public required Guid Id { get; set; } - public required ControlType Type { get; set; } - [Range(1, 100)] - public required byte Intensity { get; set; } - [Range(300, 30000)] - public required uint Duration { get; set; } -} \ No newline at end of file diff --git a/ShockOsc/Models/ControlLog.cs b/ShockOsc/Models/ControlLog.cs deleted file mode 100644 index 225e82b..0000000 --- a/ShockOsc/Models/ControlLog.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OpenShock.ShockOsc.Models; - -public class ControlLog -{ - public required GenericIn Shocker { get; set; } - public required ControlType Type { get; set; } - public required byte Intensity { get; set; } - public required uint Duration { get; set; } - public required DateTime ExecutedAt { get; set; } -} \ No newline at end of file diff --git a/ShockOsc/Models/ControlLogSender.cs b/ShockOsc/Models/ControlLogSender.cs deleted file mode 100644 index d1444f7..0000000 --- a/ShockOsc/Models/ControlLogSender.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OpenShock.ShockOsc.Models; - -public class ControlLogSender : GenericIni -{ - public required string ConnectionId { get; set; } - public required string? CustomName { get; set; } - public required IDictionary AdditionalItems { get; set; } -} \ No newline at end of file diff --git a/ShockOsc/Models/ControlType.cs b/ShockOsc/Models/ControlType.cs deleted file mode 100644 index 6f26eae..0000000 --- a/ShockOsc/Models/ControlType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OpenShock.ShockOsc.Models; - -[EnumAsInteger] -public enum ControlType -{ - Stop = 0, - Shock = 1, - Vibrate = 2, - Sound = 3 -} \ No newline at end of file diff --git a/ShockOsc/Models/GenericIn.cs b/ShockOsc/Models/GenericIn.cs deleted file mode 100644 index 75fe85a..0000000 --- a/ShockOsc/Models/GenericIn.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace OpenShock.ShockOsc.Models; - -public class GenericIn -{ - public required Guid Id { get; set; } - public required string Name { get; set; } -} \ No newline at end of file diff --git a/ShockOsc/Models/GenericIni.cs b/ShockOsc/Models/GenericIni.cs deleted file mode 100644 index 0a84114..0000000 --- a/ShockOsc/Models/GenericIni.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenShock.ShockOsc.Models; - -public class GenericIni : GenericIn -{ - public required Uri Image { get; set; } -} \ No newline at end of file diff --git a/ShockOsc/OpenShockApi.cs b/ShockOsc/OpenShockApi.cs new file mode 100644 index 0000000..6e40dd8 --- /dev/null +++ b/ShockOsc/OpenShockApi.cs @@ -0,0 +1,63 @@ +using Serilog; +using OpenShock.SDK.CSharp; +using OpenShock.SDK.CSharp.Models; + +namespace OpenShock.ShockOsc; + +public static class OpenShockApi +{ + private static readonly ILogger Logger = Log.ForContext(typeof(OpenShockApi)); + + private static OpenShockApiClient _client; + + static OpenShockApi() + { + SetupApiClient(); + } + + public static void SetupApiClient() + { + _client = new OpenShockApiClient(new ApiClientOptions + { + Server = Config.ConfigInstance.OpenShock.Backend, + Token = Config.ConfigInstance.OpenShock.Token + }); + } + + public static IReadOnlyCollection Shockers = Array.Empty(); + + public static async Task GetShockers() + { + var response = await _client.GetOwnShockers(); + + response.Switch(success => + { + Shockers = success.Value.SelectMany(x => x.Shockers).ToArray(); + + // re-populate config with previous data if present, this also deletes any shockers that are no longer present + var shockerList = new Dictionary(); + foreach (var shocker in Shockers) + { + var enabled = true; + + if (Config.ConfigInstance.OpenShock.Shockers.TryGetValue(shocker.Id, out var confShocker)) + { + enabled = confShocker.Enabled; + } + + shockerList.Add(shocker.Id, new Config.Conf.ShockerConf + { + Enabled = enabled + }); + } + Config.ConfigInstance.OpenShock.Shockers = shockerList; + Config.Save(); + ShockOsc.RefreshShockers(); + }, + error => + { + Logger.Error("We are not authenticated with the OpenShock API!"); + // TODO: handle unauthenticated error + }); + } +} \ No newline at end of file diff --git a/ShockOsc/ShockLinkApi.cs b/ShockOsc/ShockLinkApi.cs deleted file mode 100644 index 7cd7237..0000000 --- a/ShockOsc/ShockLinkApi.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Serilog; -using System.Net; -using System.Text.Json; - -namespace OpenShock.ShockOsc; - -public class ShockLinkApi -{ - private static readonly ILogger Logger = Log.ForContext(typeof(ShockLinkApi)); - - private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }; - - public static List Shockers = new(); - - public static async Task GetShockers() - { - var response = await WebRequestApi.DoRequest(new WebRequestApi.RequestData - { - url = $"{Config.ConfigInstance.ShockLink.OpenShockApi}/shockers/own" - }); - if (response.Item1 == HttpStatusCode.OK) - { - Shockers.Clear(); - var shockers = JsonSerializer.Deserialize(response.Item2, _jsonOptions); - if (shockers == null || shockers.Data.Length == 0) - { - Logger.Error("Failed to deserialize shockers: {response}", response); - return; - } - foreach (var device in shockers.Data) - { - foreach (var shocker in device.Shockers) - { - Shockers.Add(shocker); - } - } - - // populate config - var shockerList = new Dictionary(); - foreach (var shocker in Shockers) - { - // get nickname from config - var nickName = string.Empty; - var enabled = true; - - if (Config.ConfigInstance.ShockLink.Shockers.TryGetValue(shocker.Name, out var confShocker)) - { - nickName = confShocker.NickName; - enabled = confShocker.Enabled; - } - - shockerList.Add(shocker.Name, new Config.Conf.ShockerConf - { - NickName = nickName, - Id = shocker.Id, - Enabled = enabled - }); - } - Config.ConfigInstance.ShockLink.Shockers = shockerList; - Config.Save(); - ShockOsc.RefreshShockers(); - } - else - { - Logger.Error("Failed to fetch shockers: {response}", response); - } - } -} \ No newline at end of file diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index b47583f..0ef89a2 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -3,6 +3,8 @@ using System.Net; using System.Reflection; using LucHeart.CoreOSC; +using OpenShock.SDK.CSharp.Live.Models; +using OpenShock.SDK.CSharp.Models; using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.OscChangeTracker; @@ -54,45 +56,12 @@ public enum AuthState public static async Task StartMain() { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() - .Filter.ByExcluding(ev => - ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter.ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) - .WriteTo.UiLogSink() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") - .CreateLogger(); - - // ReSharper disable once RedundantAssignment - var isDebug = false; -#if DEBUG - isDebug = true; -#endif - if (isDebug) - { - Log.Information("Debug logging enabled"); - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .Filter.ByExcluding(ev => - ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter.ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) - .WriteTo.UiLogSink() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") - .CreateLogger(); - } - + _logger = Log.ForContext(typeof(ShockOsc)); _logger.Information("Starting ShockOsc version {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "error"); - var shockerList = ""; - foreach (var shocker in Config.ConfigInstance.ShockLink.Shockers) - { - shockerList += string.IsNullOrEmpty(shocker.Value.NickName) - ? $"{shocker.Key}, " - : $"{shocker.Value.NickName}, "; - } - _logger.Information("Found shockers: {Shockers}", shockerList); - ConnectToHub(); _logger.Information("Creating OSC Query Server..."); @@ -129,18 +98,18 @@ private static void ConnectToHub() { _logger.Information("Init user hub..."); SetAuthSate(AuthState.NotAuthenticated); - if (string.IsNullOrEmpty(Config.ConfigInstance.ShockLink.ApiToken)) + if (string.IsNullOrEmpty(Config.ConfigInstance.OpenShock.Token)) return; SetAuthSate(AuthState.Authenticating); - UserHubClient.InitializeAsync().ContinueWith(task => + UserHubClient.SetupLiveClient().ContinueWith(task => { if (task.IsFaulted) SetAuthSate(AuthState.NotAuthenticated); if (task.IsCompletedSuccessfully) { - ShockLinkApi.GetShockers(); + OpenShockApi.GetShockers(); SetAuthSate(AuthState.Authenticated); } }); @@ -148,7 +117,7 @@ private static void ConnectToHub() public static void Logout() { - Config.ConfigInstance.ShockLink.ApiToken = string.Empty; + Config.ConfigInstance.OpenShock.Token = string.Empty; Config.Save(); UserHubClient.Disconnect(); SetAuthSate(AuthState.NotAuthenticated); @@ -196,15 +165,15 @@ public static void RefreshShockers() { Shockers.Clear(); Shockers.TryAdd("_All", new Shocker(Guid.Empty, "_All")); - foreach (var (shockerName, shocker) in Config.ConfigInstance.ShockLink.Shockers) - { - if(!shocker.Enabled) continue; - - if (string.IsNullOrEmpty(shocker.NickName)) - Shockers.TryAdd(shockerName, new Shocker(shocker.Id, shockerName)); - else - Shockers.TryAdd(shocker.NickName, new Shocker(shocker.Id, shocker.NickName)); - } + // foreach (var (shockerName, shocker) in Config.ConfigInstance.OpenShock.Shockers) + // { + // if(!shocker.Enabled) continue; + // + // if (string.IsNullOrEmpty(shocker.NickName)) + // Shockers.TryAdd(shockerName, new Shocker(shocker.Id, shockerName)); + // else + // Shockers.TryAdd(shocker.NickName, new Shocker(shocker.Id, shocker.NickName)); + // } } public static void SaveShockers() diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 090f201..52c6502 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -47,16 +47,19 @@ - + - - - + + + + + + diff --git a/ShockOsc/Ui/App.xaml.cs b/ShockOsc/Ui/App.xaml.cs index e0525fd..51b0229 100644 --- a/ShockOsc/Ui/App.xaml.cs +++ b/ShockOsc/Ui/App.xaml.cs @@ -5,9 +5,7 @@ public partial class App : Application public App() { _ = OpenShock.ShockOsc.ShockOsc.StartMain(); - InitializeComponent(); - MainPage = new MainPage(); } diff --git a/ShockOsc/Ui/Components/Layout/Login.razor b/ShockOsc/Ui/Components/Layout/Login.razor index 87bd8a2..e0ff8dd 100644 --- a/ShockOsc/Ui/Components/Layout/Login.razor +++ b/ShockOsc/Ui/Components/Layout/Login.razor @@ -9,7 +9,7 @@
@if (!Loading) { - +
Continue
diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 8985d12..1fe98c0 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,9 +1,11 @@ @using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Layout +@using OpenShock.ShockOsc.Ui.Components.Tabs @using OpenShock.ShockOsc.Models; @using OpenShock.ShockOsc.Utils @using Serilog @using Serilog.Events +@using OpenShock.SDK.CSharp.Models @inject ISnackbar Snackbar @inherits LayoutComponentBase @@ -74,8 +76,12 @@ } else { + + + + Chatbox Options @@ -185,7 +191,7 @@ - Refresh + Refresh @if (_editShockers) { Save @@ -194,9 +200,9 @@ { Rename } -
-
- +
+
+ Enabled Name @@ -204,26 +210,10 @@ - - - - @if (_editShockers) - { - - } - else - { - @if (string.IsNullOrEmpty(@context.Value.NickName)) - { - @context.Key - } - else - { - @context.Value.NickName - } - } + - @context.Value.Id + @context.Name + @context.Id
diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor new file mode 100644 index 0000000..7d2dab8 --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -0,0 +1,106 @@ +@using System.Text.RegularExpressions +@using Swan + +@code { + + public Guid? Group { get; set; } = null; + + public void AddGroup() + { + var groupId = Guid.NewGuid(); + Config.ConfigInstance.Groups.Add(groupId, new Config.Conf.Group { Name = "New Group" }); + Group = groupId; + + InvokeAsync(StateHasChanged); + Config.Save(); + } + + public void DeleteGroup() + { + if (Group == null) return; + + Config.ConfigInstance.Groups.Remove(Group.Value); + Group = null; + InvokeAsync(StateHasChanged); + Config.Save(); + } + + private Task OnSettingsValueChange() + { + Config.Save(); + return Task.CompletedTask; + } + + private Task OnGroupSelect() + { + if (CurrentGroup != null) _selectedShockers = [..CurrentGroup.Shockers]; + + InvokeAsync(StateHasChanged); + return Task.CompletedTask; + } + + private Task OnSelectedShockersUpdate() + { + if (CurrentGroup != null) + { + CurrentGroup.Shockers = _selectedShockers.ToList(); + Config.Save(); + } + + return Task.CompletedTask; + } + + private Config.Conf.Group? CurrentGroup => Group == null ? null : Config.ConfigInstance.Groups.TryGetValue(Group.Value, out var group) ? group : null; + + private static Regex _nameRegex = new Regex(@"^[a-zA-Z0-9\/\\ -]+$", RegexOptions.Compiled); + + private string NameValidation(string name) + { + if (string.IsNullOrEmpty(name)) return "Name cannot be empty"; + if (name.Length > 30) return "Name cannot be longer than 30 characters"; + if (!_nameRegex.IsMatch(name)) return "Can only contain letters, numbers, and /\\- characters, this is due to unity animator parameters restrictions"; + return string.Empty; + } + + private HashSet _selectedShockers = []; +} + +Add New Group +Delete Group +

+ + @foreach (var group in Config.ConfigInstance.Groups) + { + @group.Value.Name + } + + +@if (CurrentGroup != null) +{ + + Group Settings + +
+ + +
+ + + Shockers in Group + +
+ + + + Name + + + @OpenShockApi.Shockers.First(x => x.Id == context).Name + + +
+} +else +{ + Please select a group to edit +} \ No newline at end of file diff --git a/ShockOsc/UserHubClient.cs b/ShockOsc/UserHubClient.cs index f5a3ff4..cc19a0e 100644 --- a/ShockOsc/UserHubClient.cs +++ b/ShockOsc/UserHubClient.cs @@ -1,7 +1,6 @@ -using Microsoft.AspNetCore.Http.Connections; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.Logging; -using OpenShock.ShockOsc.Models; +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Live; +using OpenShock.SDK.CSharp.Live.Models; using Serilog; using ILogger = Serilog.ILogger; @@ -11,40 +10,44 @@ public static class UserHubClient { private static readonly ILogger Logger = Log.ForContext(typeof(UserHubClient)); public static string? ConnectionId { get; set; } + + private static OpenShockApiLiveClient? _liveClient; - private static HubConnection? Connection; - public static async Task InitializeAsync() + public static async Task SetupLiveClient() { - Connection?.DisposeAsync(); - Connection = new HubConnectionBuilder() - .WithUrl($"{Config.ConfigInstance.ShockLink.OpenShockApi}/hubs/user", HttpTransportType.WebSockets, - options => { options.Headers.Add("OpenShockToken", Config.ConfigInstance.ShockLink.ApiToken); }) - .WithAutomaticReconnect() - .ConfigureLogging(builder => + if(_liveClient != null) await _liveClient.DisposeAsync(); + _liveClient = new OpenShockApiLiveClient(new ApiLiveClientOptions() + { + Server = Config.ConfigInstance.OpenShock.Backend, + Token = Config.ConfigInstance.OpenShock.Token, + ConfigureLogging = builder => { builder.ClearProviders(); builder.SetMinimumLevel(LogLevel.Trace); builder.AddSerilog(); - }) - .AddJsonProtocol(options => - { - options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; - options.PayloadSerializerOptions.Converters.Add(new CustomJsonStringEnumConverter()); - }) - .Build(); - Connection.On>("Log", LogReceive); - Connection.On("Welcome", WelcomeReceive); - Connection.Closed += exception => { + } + }); + + _liveClient.OnLog(LogReceive); + _liveClient.OnWelcome(WelcomeReceive); + + + _liveClient.Connection.Closed += exception => { ShockOsc.SetAuthSate(ShockOsc.AuthState.NotAuthenticated); return Task.CompletedTask; }; - await Connection.StartAsync(); + + await _liveClient.StartAsync(); } - public static Task? Control(params Control[] data) => Connection?.SendAsync("ControlV2", data, "ShockOsc"); + + public static Task? Control(params Control[] data) => _liveClient?.Control(data, "ShockOsc"); - public static void Disconnect() => Connection?.DisposeAsync(); + public static async Task Disconnect() + { + if (_liveClient != null) await _liveClient.DisposeAsync(); + } #region Handlers diff --git a/ShockOsc/WebRequestApi.cs b/ShockOsc/WebRequestApi.cs deleted file mode 100644 index 30dcf90..0000000 --- a/ShockOsc/WebRequestApi.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Serilog; -using System.Net; -using System.Reflection; -using System.Text; - -namespace OpenShock.ShockOsc; - -public static class WebRequestApi -{ - private static readonly ILogger Logger = Log.ForContext(typeof(WebRequestApi)); - private static readonly HttpClient client; - private static readonly Version currentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? throw new Exception("Could not determine ShockOsc version"); - - static WebRequestApi() - { - ServicePointManager.DefaultConnectionLimit = 10; - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; - - var handler = new HttpClientHandler(); - client = new HttpClient(handler) - { - BaseAddress = Config.ConfigInstance.ShockLink.OpenShockApi, - DefaultRequestHeaders = - { - {"User-Agent", $"ShockOsc/{currentVersion}"}, - {"OpenShockToken", Config.ConfigInstance.ShockLink.ApiToken} - } - }; - } - - public class RequestData - { - public string url = string.Empty; - public HttpMethod method = HttpMethod.Get; - public string? body; - public Dictionary? headers; - } - - public static async Task<(HttpStatusCode, string)> DoRequest(RequestData requestData) - { - var request = new HttpRequestMessage(requestData.method, requestData.url); - if (requestData.headers != null) - { - foreach (var header in requestData.headers) - { - request.Headers.Add(header.Key, header.Value); - } - } - - if (requestData.headers != null && requestData.body != null && !requestData.headers.ContainsKey("Content-Type")) - { - request.Headers.Add("Content-Type", "application/json; charset=utf-8"); - } - - if (requestData.body != null) - { - request.Content = new StringContent(requestData.body, Encoding.UTF8, "application/json"); - } - - try - { - var response = await client.SendAsync(request); - var responseString = await response.Content.ReadAsStringAsync(); - Logger.Information($"{request.RequestUri} {responseString}"); - return (response.StatusCode, responseString); - } - catch (WebException webException) - { - if (webException.Response is HttpWebResponse response) - { - var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd(); - return (response.StatusCode, responseString); - } - } - - return (HttpStatusCode.InternalServerError, string.Empty); - } -} \ No newline at end of file From 3d23720ff1237fd5011eb8f7ab935a5e7945cacc Mon Sep 17 00:00:00 2001 From: Natsumi Date: Fri, 5 Apr 2024 06:19:22 +1300 Subject: [PATCH 16/95] Fix activePageIndex, Add AnyIntensity & AnyCooldownPercentage --- ShockOsc/ShockOsc.cs | 13 ++++++++++++- ShockOsc/Ui/Components/MainLayout.razor | 26 ++++++++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 0ef89a2..5307750 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -51,6 +51,7 @@ public enum AuthState public static Dictionary AllAvatarParams = new(); public static Action? OnParamsChange; + public static Action? OnConfigUpdate; public static Action? SetAuthLoading; public static AuthState CurrentAuthState = AuthState.NotAuthenticated; @@ -437,6 +438,8 @@ private static async Task SenderLoopAsync() private static readonly ChangeTrackedOscParam ParamAnyActive = new("_Any", "_Active", false); private static readonly ChangeTrackedOscParam ParamAnyCooldown = new("_Any", "_Cooldown", false); + private static readonly ChangeTrackedOscParam ParamAnyCooldownPercentage = new("_Any", "_CooldownPercentage", 0f); + private static readonly ChangeTrackedOscParam ParamAnyIntensity = new("_Any", "_Intensity", 0f); private static async Task InstantShock(Shocker shocker, uint duration, byte intensity) { @@ -484,6 +487,8 @@ private static async Task SendParams() // TODO: maybe force resend on avatar change var anyActive = false; var anyCooldown = false; + var anyCooldownPercentage = 0f; + var anyIntensity = 0f; foreach (var shocker in Shockers.Values) { @@ -510,10 +515,14 @@ private static async Task SendParams() if (isActive) anyActive = true; if (onCoolDown) anyCooldown = true; + anyCooldownPercentage = Math.Max(anyCooldownPercentage, cooldownPercentage); + anyIntensity = Math.Max(anyIntensity, GetFloatScaled(shocker.LastIntensity)); } await ParamAnyActive.SetValue(anyActive); await ParamAnyCooldown.SetValue(anyCooldown); + await ParamAnyCooldownPercentage.SetValue(anyCooldownPercentage); + await ParamAnyIntensity.SetValue(anyIntensity); } private static async Task CheckLoop() @@ -739,5 +748,7 @@ private static Task CancelAction(Shocker shocker) } private static float LerpFloat(float min, float max, float t) => min + (max - min) * t; - private static float ClampFloat(float value) => value < 0 ? 0 : value > 1 ? 1 : value; + public static float ClampFloat(float value) => value < 0 ? 0 : value > 1 ? 1 : value; + public static uint LerpUint(uint min, uint max, float t) => (uint)(min + (max - min) * t); + public static uint ClampUint(uint value, uint min, uint max) => value < min ? min : value > max ? max : value; } diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 1fe98c0..b71f983 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,4 +1,4 @@ -@using OpenShock.ShockOsc.Logging +@using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Layout @using OpenShock.ShockOsc.Ui.Components.Tabs @using OpenShock.ShockOsc.Models; @@ -83,11 +83,11 @@ - + Chatbox Options - +

@@ -210,7 +210,7 @@ - + @context.Name @context.Id @@ -219,7 +219,8 @@
List of ShockOSC prefixed parameters and their states - AvatarId: @ShockOsc.AvatarId +
+ Avatar ID: @ShockOsc.AvatarId
@@ -314,20 +315,27 @@ private void OnParamsChange(bool shockOscParam) { // check Debug page is active - if (activePageIndex != 2) + if (activePageIndex != 3) return; // only redraw page when needed if (!_showAllAvatarParams && !shockOscParam) return; - + _updateQueued = true; } private void OnLogAdded() { // check Log page is active - if (activePageIndex == 3) + if (activePageIndex == 4) + InvokeAsync(StateHasChanged); + } + + private void OnConfigUpdate() + { + // check Config page is active + if (activePageIndex == 1) InvokeAsync(StateHasChanged); } @@ -348,7 +356,6 @@ _config.Behaviour.RandomDuration = duration == "Random Duration"; ValidateSettings(); - Logger.Information("settings changed"); Config.Save(); return Task.CompletedTask; } @@ -372,6 +379,7 @@ { ShockOsc.SetAuthLoading = SetAuthLoading; ShockOsc.OnParamsChange = OnParamsChange; + ShockOsc.OnConfigUpdate = OnConfigUpdate; UiLogSink.NotificationAction = MsgNoty; LogStore.OnLogAdded = OnLogAdded; From 66a7486cb2d31298e7b40c7f8ffc3b14159ad20b Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 4 Apr 2024 20:57:16 +0200 Subject: [PATCH 17/95] Fix ms logger spam :) --- ShockOsc/MauiProgram.cs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index e933a2f..592eed7 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -22,31 +22,29 @@ public static MauiApp CreateMauiApp() builder.Services.AddBlazorWebViewDeveloperTools(); builder.Logging.AddDebug(); #endif - - Log.Logger = new LoggerConfiguration() + + var loggerConfiguration = new LoggerConfiguration() .MinimumLevel.Information() .Filter.ByExcluding(ev => - ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter.ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) + ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter + .ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) + .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information) .WriteTo.UiLogSink() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") - .CreateLogger(); + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"); // ReSharper disable once RedundantAssignment - var isDebug = false; + var isDebug = Environment.GetCommandLineArgs().Any(x => x.Equals("--debug", StringComparison.InvariantCultureIgnoreCase)); + #if DEBUG isDebug = true; #endif if (isDebug) { - Log.Information("Debug logging enabled"); - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .Filter.ByExcluding(ev => - ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter.ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) - .WriteTo.UiLogSink() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") - .CreateLogger(); + loggerConfiguration.MinimumLevel.Debug(); } + + Log.Logger = loggerConfiguration.CreateLogger(); builder.Services.AddSerilog(Log.Logger); From d104c9fce75d921454a07402b79f31be4fcbd1f7 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Fri, 5 Apr 2024 11:44:52 +1300 Subject: [PATCH 18/95] Build out UnderscoreConfig --- ShockOsc/ShockOsc.cs | 2 +- ShockOsc/Ui/Components/MainLayout.razor | 36 +++++++- ShockOsc/UnderscoreConfig.cs | 114 ++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 5307750..ebf2772 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -452,7 +452,7 @@ private static async Task InstantShock(Shocker shocker, uint duration, byte inte SendParams(); shocker.TriggerMethod = TriggerMethod.None; - var inSeconds = ((float)duration / 1000).ToString(CultureInfo.InvariantCulture); + var inSeconds = MathF.Round(duration / 1000f, 1).ToString(CultureInfo.InvariantCulture); _logger.Information( "Sending shock to {Shocker} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s", shocker.Name, intensity, intensityPercentage, inSeconds); diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index b71f983..d0ab87b 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -183,7 +183,7 @@ Other Options - + @@ -350,13 +350,45 @@ return Task.CompletedTask; } - private Task OnSettingsValueChange() + private DateTime _lastSettingsSave = DateTime.Now; + private CancellationTokenSource _cts = new CancellationTokenSource(); + + private async Task OnSettingsValueChange() + { + if ((DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) + { + await OnSettingsValueSave(); + return; + } + + _cts.Cancel(); + _cts = new CancellationTokenSource(); + + try + { + await Task.Delay(10, _cts.Token); + + if (!_cts.Token.IsCancellationRequested && + (DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) + { + await OnSettingsValueSave(); + } + } + catch (TaskCanceledException) + { + // Task was cancelled, which is expected + } + } + + private Task OnSettingsValueSave() { + _lastSettingsSave = DateTime.Now; _config.Behaviour.RandomIntensity = intensity == "Random Intensity"; _config.Behaviour.RandomDuration = duration == "Random Duration"; ValidateSettings(); Config.Save(); + UnderscoreConfig.SendUpdateForAll(); return Task.CompletedTask; } diff --git a/ShockOsc/UnderscoreConfig.cs b/ShockOsc/UnderscoreConfig.cs index c26a555..a9a5045 100644 --- a/ShockOsc/UnderscoreConfig.cs +++ b/ShockOsc/UnderscoreConfig.cs @@ -11,11 +11,112 @@ public static class UnderscoreConfig public static void HandleCommand(string parameterName, object?[] arguments) { var settingName = parameterName[8..]; + + var settingPath = settingName.Split('/'); + if (settingPath.Length > 2) + { + Logger.Warning("Invalid setting path: {SettingPath}", settingPath); + return; + } + + if (settingPath.Length == 2) + { + var shockerName = settingPath[0]; + var action = settingPath[1]; + if (!ShockOsc.Shockers.ContainsKey(shockerName) && shockerName != "_All") + { + Logger.Warning("Unknown shocker {Shocker}", shockerName); + Logger.Debug("Param: {Param}", action); + return; + } + + var shocker = ShockOsc.Shockers[shockerName]; + var value = arguments.ElementAtOrDefault(0); + + // TODO: support groups + + switch (action) + { + case "MinIntensity": + // 0..100% + if (value is float minIntensityFloat) + { + var currentMinIntensity = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.IntensityRange.Min / 100f); + if (minIntensityFloat == currentMinIntensity) return; + + Config.ConfigInstance.Behaviour.IntensityRange.Min = ShockOsc.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); + ValidateSettings(); + Config.Save(); + ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + } + break; + + case "MaxIntensity": + // 0..100% + if (value is float maxIntensityFloat) + { + var currentMaxIntensity = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.IntensityRange.Max / 100f); + if (maxIntensityFloat == currentMaxIntensity) return; + + Config.ConfigInstance.Behaviour.IntensityRange.Max = ShockOsc.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); + ValidateSettings(); + Config.Save(); + ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + } + break; + + case "Duration": + // 0..10sec + if (value is float durationFloat) + { + var currentDuration = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.FixedDuration / 10000f); + if (durationFloat == currentDuration) return; + + Config.ConfigInstance.Behaviour.FixedDuration = ShockOsc.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); + ValidateSettings(); + Config.Save(); + ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + } + break; + + case "CooldownTime": + // 0..100sec + if (value is float cooldownTimeFloat) + { + var currentCooldownTime = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.CooldownTime / 100000f); + if (cooldownTimeFloat == currentCooldownTime) return; + + Config.ConfigInstance.Behaviour.CooldownTime = ShockOsc.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); + ValidateSettings(); + Config.Save(); + ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + } + break; + + + case "HoldTime": + // 0..1sec + if (value is float holdTimeFloat) + { + var currentHoldTime = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.HoldTime / 1000f); + if (holdTimeFloat == currentHoldTime) return; + + Config.ConfigInstance.Behaviour.HoldTime = ShockOsc.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); + ValidateSettings(); + Config.Save(); + ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + } + break; + } + } + switch (settingName) { case "Paused": if (arguments.ElementAtOrDefault(0) is bool stateBool) { + if (KillSwitch == stateBool) return; + KillSwitch = stateBool; Logger.Information("Paused state set to: {KillSwitch}", KillSwitch); } @@ -23,8 +124,21 @@ public static void HandleCommand(string parameterName, object?[] arguments) } } + private static void ValidateSettings() + { + if (Config.ConfigInstance.Behaviour.IntensityRange.Min > Config.ConfigInstance.Behaviour.IntensityRange.Max) + { + Config.ConfigInstance.Behaviour.IntensityRange.Max = Config.ConfigInstance.Behaviour.IntensityRange.Min; + } + } + public static async Task SendUpdateForAll() { await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/Paused", KillSwitch); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.IntensityRange.Min / 100f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.IntensityRange.Max / 100f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.FixedDuration / 10000f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.CooldownTime / 100000f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.HoldTime / 1000f)); } } \ No newline at end of file From 5ded87fcaca44cb9aa9099dc894d1faa69231265 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 5 Apr 2024 19:04:46 +0200 Subject: [PATCH 19/95] Split off parts, and rework backend logic --- ShockOsc/Backend/AuthState.cs | 8 + ShockOsc/Backend/BackendLiveApiManager.cs | 37 ++ ShockOsc/Backend/OpenShockApi.cs | 65 +++ ShockOsc/CustomJsonStringEnumConverter.cs | 16 - ShockOsc/EnumAsIntegerAttribute.cs | 6 - ShockOsc/MauiProgram.cs | 41 +- ShockOsc/OpenShockApi.cs | 63 --- ShockOsc/OscClient.cs | 4 +- ShockOsc/OscHandler.cs | 13 + ShockOsc/Services/BackendControlService.cs | 14 + ShockOsc/ShockOsc.cs | 140 +++--- ShockOsc/ShockOsc.csproj | 2 +- .../{Config.cs => ShockOscConfigManager.cs} | 34 +- ShockOsc/Ui/Components/Layout/Login.razor | 22 - ShockOsc/Ui/Components/MainLayout.razor | 416 +++++------------- ShockOsc/Ui/Components/Tabs/DebugTab.razor | 65 +++ ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 20 +- ShockOsc/Ui/Components/Tabs/LogsTab.razor | 50 +++ ShockOsc/Ui/Components/Tabs/ShockersTab.razor | 29 ++ ShockOsc/Ui/Components/UpdateDialog.razor | 4 +- ShockOsc/Ui/Components/UpdateLogout.razor | 4 +- ShockOsc/Ui/Main.razor | 8 +- .../Pages/Authentication/Authenticate.razor | 55 +++ ShockOsc/Ui/Utils/ThemeDefinition.cs | 31 ++ ShockOsc/UnderscoreConfig.cs | 44 +- ShockOsc/Updater.cs | 4 +- ShockOsc/UserHubClient.cs | 71 --- 27 files changed, 630 insertions(+), 636 deletions(-) create mode 100644 ShockOsc/Backend/AuthState.cs create mode 100644 ShockOsc/Backend/BackendLiveApiManager.cs create mode 100644 ShockOsc/Backend/OpenShockApi.cs delete mode 100644 ShockOsc/CustomJsonStringEnumConverter.cs delete mode 100644 ShockOsc/EnumAsIntegerAttribute.cs delete mode 100644 ShockOsc/OpenShockApi.cs create mode 100644 ShockOsc/OscHandler.cs create mode 100644 ShockOsc/Services/BackendControlService.cs rename ShockOsc/{Config.cs => ShockOscConfigManager.cs} (90%) delete mode 100644 ShockOsc/Ui/Components/Layout/Login.razor create mode 100644 ShockOsc/Ui/Components/Tabs/DebugTab.razor create mode 100644 ShockOsc/Ui/Components/Tabs/LogsTab.razor create mode 100644 ShockOsc/Ui/Components/Tabs/ShockersTab.razor create mode 100644 ShockOsc/Ui/Pages/Authentication/Authenticate.razor create mode 100644 ShockOsc/Ui/Utils/ThemeDefinition.cs delete mode 100644 ShockOsc/UserHubClient.cs diff --git a/ShockOsc/Backend/AuthState.cs b/ShockOsc/Backend/AuthState.cs new file mode 100644 index 0000000..b08445d --- /dev/null +++ b/ShockOsc/Backend/AuthState.cs @@ -0,0 +1,8 @@ +namespace OpenShock.ShockOsc.Backend; + +public enum AuthState +{ + NotAuthenticated, + Authenticating, + Authenticated +} \ No newline at end of file diff --git a/ShockOsc/Backend/BackendLiveApiManager.cs b/ShockOsc/Backend/BackendLiveApiManager.cs new file mode 100644 index 0000000..73264c7 --- /dev/null +++ b/ShockOsc/Backend/BackendLiveApiManager.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Live; +using Serilog; + +namespace OpenShock.ShockOsc.Backend; + +public sealed class BackendLiveApiManager +{ + private readonly ILogger _logger; + private readonly ShockOscConfigManager.ShockOscConfig _config; + private readonly OpenShockApiLiveClient _openShockApiLiveClient; + + public BackendLiveApiManager(ILogger logger, ShockOscConfigManager.ShockOscConfig config, OpenShockApiLiveClient openShockApiLiveClient) + { + _logger = logger; + _config = config; + _openShockApiLiveClient = openShockApiLiveClient; + } + + + public async Task SetupLiveClient() + { + await _openShockApiLiveClient.Setup(new ApiLiveClientOptions() + { + Token = _config.OpenShock.Token, + Server = _config.OpenShock.Backend, + ConfigureLogging = builder => + { + builder.ClearProviders(); + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddSerilog(); + } + }); + //await _openShockApiLiveClient.StartAsync(); + } + +} \ No newline at end of file diff --git a/ShockOsc/Backend/OpenShockApi.cs b/ShockOsc/Backend/OpenShockApi.cs new file mode 100644 index 0000000..a69dbb0 --- /dev/null +++ b/ShockOsc/Backend/OpenShockApi.cs @@ -0,0 +1,65 @@ +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp; +using OpenShock.SDK.CSharp.Models; + +namespace OpenShock.ShockOsc.Backend; + +public sealed class OpenShockApi +{ + private readonly ILogger _logger; + private readonly ShockOscConfigManager.ShockOscConfig _config; + private OpenShockApiClient _client; + + public OpenShockApi(ILogger logger, ShockOscConfigManager.ShockOscConfig config) + { + _logger = logger; + _config = config; + SetupApiClient(); + } + + public void SetupApiClient() + { + _client = new OpenShockApiClient(new ApiClientOptions + { + Server = _config.OpenShock.Backend, + Token = _config.OpenShock.Token + }); + } + + public IReadOnlyCollection Shockers = Array.Empty(); + + public async Task RefreshShockers() + { + var response = await _client.GetOwnShockers(); + + response.Switch(success => + { + Shockers = success.Value.SelectMany(x => x.Shockers).ToArray(); + + // re-populate config with previous data if present, this also deletes any shockers that are no longer present + var shockerList = new Dictionary(); + foreach (var shocker in Shockers) + { + var enabled = true; + + if (ShockOscConfigManager.ConfigInstance.OpenShock.Shockers.TryGetValue(shocker.Id, out var confShocker)) + { + enabled = confShocker.Enabled; + } + + shockerList.Add(shocker.Id, new ShockOscConfigManager.ShockOscConfig.ShockerConf + { + Enabled = enabled + }); + } + ShockOscConfigManager.ConfigInstance.OpenShock.Shockers = shockerList; + ShockOscConfigManager.Save(); + ShockOsc.RefreshShockers(); + }, + error => + { + _logger.LogError("We are not authenticated with the OpenShock API!"); + // TODO: handle unauthenticated error + }); + } +} \ No newline at end of file diff --git a/ShockOsc/CustomJsonStringEnumConverter.cs b/ShockOsc/CustomJsonStringEnumConverter.cs deleted file mode 100644 index 23cd58e..0000000 --- a/ShockOsc/CustomJsonStringEnumConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace OpenShock.ShockOsc; - -public class CustomJsonStringEnumConverter : JsonConverterFactory -{ - private static readonly JsonStringEnumConverter JsonStringEnumConverter = new(); - - public override bool CanConvert(Type typeToConvert) => - !typeToConvert.IsDefined(typeof(EnumAsIntegerAttribute), false) && - JsonStringEnumConverter.CanConvert(typeToConvert); - - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => - JsonStringEnumConverter.CreateConverter(typeToConvert, options); -} \ No newline at end of file diff --git a/ShockOsc/EnumAsIntegerAttribute.cs b/ShockOsc/EnumAsIntegerAttribute.cs deleted file mode 100644 index e9cc220..0000000 --- a/ShockOsc/EnumAsIntegerAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenShock.ShockOsc; - -[AttributeUsage(AttributeTargets.Enum)] -public class EnumAsIntegerAttribute : Attribute -{ -} \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 592eed7..45a13a6 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Logging; using MudBlazor.Services; +using OpenShock.SDK.CSharp.Live; +using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.Ui; using Serilog; @@ -11,18 +13,9 @@ public static class MauiProgram public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); - builder - .UseMauiApp() - .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); - - builder.Services.AddMudServices(); - builder.Services.AddMauiBlazorWebView(); - -#if DEBUG - builder.Services.AddBlazorWebViewDeveloperTools(); - builder.Logging.AddDebug(); -#endif - + + // <---- Services ----> + var loggerConfiguration = new LoggerConfiguration() .MinimumLevel.Information() .Filter.ByExcluding(ev => @@ -48,8 +41,28 @@ public static MauiApp CreateMauiApp() builder.Services.AddSerilog(Log.Logger); - var mauiApp = builder.Build(); + builder.Services.AddSingleton(ShockOscConfigManager.ConfigInstance); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + builder.Services.AddMudServices(); + builder.Services.AddMauiBlazorWebView(); + + // <---- App ----> + + builder + .UseMauiApp() + .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); + + +#if DEBUG + builder.Services.AddBlazorWebViewDeveloperTools(); +#endif - return mauiApp; + return builder.Build();; } } \ No newline at end of file diff --git a/ShockOsc/OpenShockApi.cs b/ShockOsc/OpenShockApi.cs deleted file mode 100644 index 6e40dd8..0000000 --- a/ShockOsc/OpenShockApi.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Serilog; -using OpenShock.SDK.CSharp; -using OpenShock.SDK.CSharp.Models; - -namespace OpenShock.ShockOsc; - -public static class OpenShockApi -{ - private static readonly ILogger Logger = Log.ForContext(typeof(OpenShockApi)); - - private static OpenShockApiClient _client; - - static OpenShockApi() - { - SetupApiClient(); - } - - public static void SetupApiClient() - { - _client = new OpenShockApiClient(new ApiClientOptions - { - Server = Config.ConfigInstance.OpenShock.Backend, - Token = Config.ConfigInstance.OpenShock.Token - }); - } - - public static IReadOnlyCollection Shockers = Array.Empty(); - - public static async Task GetShockers() - { - var response = await _client.GetOwnShockers(); - - response.Switch(success => - { - Shockers = success.Value.SelectMany(x => x.Shockers).ToArray(); - - // re-populate config with previous data if present, this also deletes any shockers that are no longer present - var shockerList = new Dictionary(); - foreach (var shocker in Shockers) - { - var enabled = true; - - if (Config.ConfigInstance.OpenShock.Shockers.TryGetValue(shocker.Id, out var confShocker)) - { - enabled = confShocker.Enabled; - } - - shockerList.Add(shocker.Id, new Config.Conf.ShockerConf - { - Enabled = enabled - }); - } - Config.ConfigInstance.OpenShock.Shockers = shockerList; - Config.Save(); - ShockOsc.RefreshShockers(); - }, - error => - { - Logger.Error("We are not authenticated with the OpenShock API!"); - // TODO: handle unauthenticated error - }); - } -} \ No newline at end of file diff --git a/ShockOsc/OscClient.cs b/ShockOsc/OscClient.cs index fec1ae4..499e83a 100644 --- a/ShockOsc/OscClient.cs +++ b/ShockOsc/OscClient.cs @@ -8,7 +8,7 @@ namespace OpenShock.ShockOsc; public static class OscClient { private static OscDuplex? _gameConnection; - private static readonly OscSender HoscySenderClient = new(new IPEndPoint(IPAddress.Loopback, Config.ConfigInstance.Osc.HoscySendPort)); + private static readonly OscSender HoscySenderClient = new(new IPEndPoint(IPAddress.Loopback, ShockOscConfigManager.ConfigInstance.Osc.HoscySendPort)); private static readonly ILogger Logger = Log.ForContext(typeof(OscClient)); static OscClient() @@ -43,7 +43,7 @@ public static ValueTask SendGameMessage(string address, params object?[]?argumen public static ValueTask SendChatboxMessage(string message) { - if (Config.ConfigInstance.Osc.Hoscy) return HoscySenderChannel.Writer.WriteAsync(new OscMessage("/hoscy/message", message)); + if (ShockOscConfigManager.ConfigInstance.Osc.Hoscy) return HoscySenderChannel.Writer.WriteAsync(new OscMessage("/hoscy/message", message)); return GameSenderChannel.Writer.WriteAsync(new OscMessage("/chatbox/input", message, true)); } diff --git a/ShockOsc/OscHandler.cs b/ShockOsc/OscHandler.cs new file mode 100644 index 0000000..224bdb1 --- /dev/null +++ b/ShockOsc/OscHandler.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; + +namespace OpenShock.ShockOsc; + +public sealed class OscHandler +{ + + + public OscHandler(ILogger logger) + { + + } +} \ No newline at end of file diff --git a/ShockOsc/Services/BackendControlService.cs b/ShockOsc/Services/BackendControlService.cs new file mode 100644 index 0000000..6a9b78a --- /dev/null +++ b/ShockOsc/Services/BackendControlService.cs @@ -0,0 +1,14 @@ +using OpenShock.ShockOsc.Backend; +using OpenShock.ShockOsc.Ui.Components; + +namespace OpenShock.ShockOsc.Services; + +public sealed class BackendControlService +{ + private readonly BackendLiveApiManager _backendLiveApiManager; + + public BackendControlService(BackendLiveApiManager backendLiveApiManager) + { + _backendLiveApiManager = backendLiveApiManager; + } +} \ No newline at end of file diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index ebf2772..3cc6a46 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -5,6 +5,7 @@ using LucHeart.CoreOSC; using OpenShock.SDK.CSharp.Live.Models; using OpenShock.SDK.CSharp.Models; +using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.OscChangeTracker; @@ -40,12 +41,7 @@ public static class ShockOsc "IShock" }; - public enum AuthState - { - NotAuthenticated, - Authenticating, - Authenticated - } + public static Dictionary ParamsInUse = new(); public static Dictionary AllAvatarParams = new(); @@ -63,8 +59,6 @@ public static async Task StartMain() _logger.Information("Starting ShockOsc version {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "error"); - ConnectToHub(); - _logger.Information("Creating OSC Query Server..."); _ = new OscQueryServer( "ShockOsc", // service name @@ -74,7 +68,7 @@ public static async Task StartMain() ); // listen for VRC on every network interface - if (Config.ConfigInstance.Osc.QuestSupport) + if (ShockOscConfigManager.ConfigInstance.Osc.QuestSupport) { var host = await Dns.GetHostEntryAsync(Dns.GetHostName()); foreach (var ip in host.AddressList) @@ -94,42 +88,7 @@ public static async Task StartMain() await Task.Delay(Timeout.Infinite).ConfigureAwait(false); } - - private static void ConnectToHub() - { - _logger.Information("Init user hub..."); - SetAuthSate(AuthState.NotAuthenticated); - if (string.IsNullOrEmpty(Config.ConfigInstance.OpenShock.Token)) - return; - - SetAuthSate(AuthState.Authenticating); - UserHubClient.SetupLiveClient().ContinueWith(task => - { - if (task.IsFaulted) - SetAuthSate(AuthState.NotAuthenticated); - - if (task.IsCompletedSuccessfully) - { - OpenShockApi.GetShockers(); - SetAuthSate(AuthState.Authenticated); - } - }); - } - - public static void Logout() - { - Config.ConfigInstance.OpenShock.Token = string.Empty; - Config.Save(); - UserHubClient.Disconnect(); - SetAuthSate(AuthState.NotAuthenticated); - } - - public static void ClickLogin() - { - Config.Save(); - _logger.Information("Clicking login"); - ConnectToHub(); - } + public static void SetAuthSate(AuthState state) { @@ -180,7 +139,7 @@ public static void RefreshShockers() public static void SaveShockers() { RefreshShockers(); - Config.Save(); + ShockOscConfigManager.Save(); } private static void OnAvatarChange(Dictionary? parameters, string avatarId) @@ -360,7 +319,7 @@ private static async Task ReceiveLogic() return; } - if (_isAfk && Config.ConfigInstance.Behaviour.DisableWhileAfk) + if (_isAfk && ShockOscConfigManager.ConfigInstance.Behaviour.DisableWhileAfk) { shocker.TriggerMethod = TriggerMethod.None; await LogIgnoredAfk(); @@ -384,8 +343,8 @@ private static async Task ReceiveLogic() shocker.TriggerMethod = TriggerMethod.PhysBoneRelease; shocker.LastActive = DateTime.UtcNow; } - else if (Config.ConfigInstance.Behaviour.WhileBoneHeld != - Config.Conf.BehaviourConf.BoneHeldAction.None) + else if (ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld != + ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None) { await CancelAction(shocker); } @@ -412,19 +371,19 @@ private static async Task ReceiveLogic() private static ValueTask LogIgnoredKillSwitchActive() { _logger.Information("Ignoring shock, kill switch is active"); - if (string.IsNullOrEmpty(Config.ConfigInstance.Chatbox.IgnoredKillSwitchActive)) return ValueTask.CompletedTask; + if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive)) return ValueTask.CompletedTask; return OscClient.SendChatboxMessage( - $"{Config.ConfigInstance.Chatbox.Prefix}{Config.ConfigInstance.Chatbox.IgnoredKillSwitchActive}"); + $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive}"); } private static ValueTask LogIgnoredAfk() { _logger.Information("Ignoring shock, user is AFK"); - if (string.IsNullOrEmpty(Config.ConfigInstance.Chatbox.IgnoredAfk)) return ValueTask.CompletedTask; + if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk)) return ValueTask.CompletedTask; return OscClient.SendChatboxMessage( - $"{Config.ConfigInstance.Chatbox.Prefix}{Config.ConfigInstance.Chatbox.IgnoredAfk}"); + $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk}"); } private static async Task SenderLoopAsync() @@ -459,7 +418,7 @@ private static async Task InstantShock(Shocker shocker, uint duration, byte inte await ControlShocker(shocker.Id, duration, intensity, ControlType.Shock); - if (!Config.ConfigInstance.Osc.Chatbox) return; + if (!ShockOscConfigManager.ConfigInstance.Osc.Chatbox) return; // Chatbox message local var dat = new { @@ -469,8 +428,8 @@ private static async Task InstantShock(Shocker shocker, uint duration, byte inte Duration = duration, DurationSeconds = inSeconds }; - var template = Config.ConfigInstance.Chatbox.Types[ControlType.Shock]; - var msg = $"{Config.ConfigInstance.Chatbox.Prefix}{Smart.Format(template.Local, dat)}"; + var template = ShockOscConfigManager.ConfigInstance.Chatbox.Types[ControlType.Shock]; + var msg = $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{Smart.Format(template.Local, dat)}"; await OscClient.SendChatboxMessage(msg); } @@ -480,7 +439,7 @@ private static async Task InstantShock(Shocker shocker, uint duration, byte inte /// /// private static float GetFloatScaled(byte intensity) => - ClampFloat((float)intensity / Config.ConfigInstance.Behaviour.IntensityRange.Max); + ClampFloat((float)intensity / ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max); private static async Task SendParams() { @@ -494,7 +453,7 @@ private static async Task SendParams() { var isActive = shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; var isActiveOrOnCooldown = - shocker.LastExecuted.AddMilliseconds(Config.ConfigInstance.Behaviour.CooldownTime) + shocker.LastExecuted.AddMilliseconds(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime) .AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; if (!isActiveOrOnCooldown && shocker.LastIntensity > 0) shocker.LastIntensity = 0; @@ -506,7 +465,7 @@ private static async Task SendParams() cooldownPercentage = ClampFloat(1 - (float)(DateTime.UtcNow - shocker.LastExecuted.AddMilliseconds(shocker.LastDuration)) - .TotalMilliseconds / Config.ConfigInstance.Behaviour.CooldownTime); + .TotalMilliseconds / ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime); await shocker.ParamActive.SetValue(isActive); await shocker.ParamCooldown.SetValue(onCoolDown); @@ -544,7 +503,7 @@ private static async Task CheckLoop() private static byte GetIntensity() { - var config = Config.ConfigInstance.Behaviour; + var config = ShockOscConfigManager.ConfigInstance.Behaviour; if (!config.RandomIntensity) return config.FixedIntensity; var rir = config.IntensityRange; @@ -554,15 +513,15 @@ private static byte GetIntensity() private static async Task CheckLogic() { - var config = Config.ConfigInstance.Behaviour; + var config = ShockOscConfigManager.ConfigInstance.Behaviour; foreach (var (pos, shocker) in Shockers) { var isActiveOrOnCooldown = - shocker.LastExecuted.AddMilliseconds(Config.ConfigInstance.Behaviour.CooldownTime) + shocker.LastExecuted.AddMilliseconds(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime) .AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; if (shocker.TriggerMethod == TriggerMethod.None && - Config.ConfigInstance.Behaviour.WhileBoneHeld != Config.Conf.BehaviourConf.BoneHeldAction.None && + ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld != ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None && !isActiveOrOnCooldown && shocker.IsGrabbed && shocker.LastVibration < DateTime.UtcNow.Subtract(TimeSpan.FromMilliseconds(300))) @@ -574,7 +533,7 @@ private static async Task CheckLogic() _logger.Debug("Vibrating {Shocker} at {Intensity}", pos, vibrationIntensity); await ControlShocker(shocker.Id, 1000, (byte)vibrationIntensity, - Config.ConfigInstance.Behaviour.WhileBoneHeld == Config.Conf.BehaviourConf.BoneHeldAction.Shock + ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld == ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.Shock ? ControlType.Shock : ControlType.Vibrate); } @@ -623,7 +582,7 @@ await ControlShocker(shocker.Id, 1000, (byte)vibrationIntensity, private static uint GetDuration() { - var config = Config.ConfigInstance.Behaviour; + var config = ShockOscConfigManager.ConfigInstance.Behaviour; if (!config.RandomDuration) return config.FixedDuration; var rdr = config.DurationRange; @@ -633,31 +592,32 @@ private static uint GetDuration() private static Task ControlShocker(Guid shockerId, uint duration, byte intensity, ControlType type) { - if (shockerId == Guid.Empty) - return UserHubClient.Control(Shockers.Where(x => x.Value.Id != Guid.Empty).Select(x => new Control - { - Id = x.Value.Id, - Intensity = intensity, - Duration = duration, - Type = type - }).ToArray()); - - return UserHubClient.Control(new Control - { - Id = shockerId, - Intensity = intensity, - Duration = duration, - Type = type - }); + // if (shockerId == Guid.Empty) + // return BackendLiveApiManager.Control(Shockers.Where(x => x.Value.Id != Guid.Empty).Select(x => new Control + // { + // Id = x.Value.Id, + // Intensity = intensity, + // Duration = duration, + // Type = type + // }).ToArray()); + // + // return BackendLiveApiManager.Control(new Control + // { + // Id = shockerId, + // Intensity = intensity, + // Duration = duration, + // Type = type + // }); + return Task.CompletedTask; } public static async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) { - if (sender.ConnectionId == UserHubClient.ConnectionId) - { - _logger.Debug("Ignoring remote command log cause it was the local connection"); - return; - } + // if (sender.ConnectionId == BackendLiveApiManager.ConnectionId) + // { + // _logger.Debug("Ignoring remote command log cause it was the local connection"); + // return; + // } var inSeconds = ((float)log.Duration / 1000).ToString(CultureInfo.InvariantCulture); @@ -670,8 +630,8 @@ public static async Task RemoteActivateShocker(ControlLogSender sender, ControlL "Received remote {Type} for \"{ShockerName}\" at {Intensity}%:{Duration}s by {SenderCustomName} [{Sender}]", log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.CustomName, sender.Name); - var template = Config.ConfigInstance.Chatbox.Types[log.Type]; - if (Config.ConfigInstance.Osc.Chatbox && Config.ConfigInstance.Chatbox.DisplayRemoteControl && template.Enabled) + var template = ShockOscConfigManager.ConfigInstance.Chatbox.Types[log.Type]; + if (ShockOscConfigManager.ConfigInstance.Osc.Chatbox && ShockOscConfigManager.ConfigInstance.Chatbox.DisplayRemoteControl && template.Enabled) { // Chatbox message remote var dat = new @@ -685,7 +645,7 @@ public static async Task RemoteActivateShocker(ControlLogSender sender, ControlL }; var msg = - $"{Config.ConfigInstance.Chatbox.Prefix}{Smart.Format(sender.CustomName == null ? template.Remote : template.RemoteWithCustomName, dat)}"; + $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{Smart.Format(sender.CustomName == null ? template.Remote : template.RemoteWithCustomName, dat)}"; await OscClient.SendChatboxMessage(msg); } @@ -732,7 +692,7 @@ public static async Task RemoteActivateShocker(ControlLogSender sender, ControlL private static async Task ForceUnmute() { - if (!Config.ConfigInstance.Behaviour.ForceUnmute || !_isMuted) return; + if (!ShockOscConfigManager.ConfigInstance.Behaviour.ForceUnmute || !_isMuted) return; _logger.Debug("Force unmuting..."); await OscClient.SendGameMessage("/input/Voice", false); await Task.Delay(50); diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 52c6502..a85e441 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -56,7 +56,7 @@ - + diff --git a/ShockOsc/Config.cs b/ShockOsc/ShockOscConfigManager.cs similarity index 90% rename from ShockOsc/Config.cs rename to ShockOsc/ShockOscConfigManager.cs index b033004..a5f7ad4 100644 --- a/ShockOsc/Config.cs +++ b/ShockOsc/ShockOscConfigManager.cs @@ -6,15 +6,15 @@ namespace OpenShock.ShockOsc; -public static class Config +public static class ShockOscConfigManager { - private static readonly ILogger Logger = Log.ForContext(typeof(Config)); - private static Conf? _internalConfig; + private static readonly ILogger Logger = Log.ForContext(typeof(ShockOscConfigManager)); + private static ShockOscConfig? _internalConfig; private static readonly string Path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\ShockOSC\config.json"; - public static Conf ConfigInstance => _internalConfig!; + public static ShockOscConfig ConfigInstance => _internalConfig!; - static Config() + static ShockOscConfigManager() { TryLoad(); ShockOsc.RefreshShockers(); @@ -33,7 +33,7 @@ private static void TryLoad() Logger.Verbose("Config file is not empty"); try { - _internalConfig = JsonSerializer.Deserialize(json, Options); + _internalConfig = JsonSerializer.Deserialize(json, Options); Logger.Information("Successfully loaded config"); } catch (JsonException e) @@ -49,7 +49,7 @@ private static void TryLoad() _internalConfig = GetDefaultConfig(); Save(); var jsonNew = File.ReadAllText(Path); - _internalConfig = JsonSerializer.Deserialize(jsonNew, Options); + _internalConfig = JsonSerializer.Deserialize(jsonNew, Options); Logger.Information("New configuration file generated! Please configure it!"); } @@ -77,25 +77,25 @@ public static void Save() } } - private static Conf GetDefaultConfig() => new() + private static ShockOscConfig GetDefaultConfig() => new() { - Osc = new Conf.OscConf + Osc = new ShockOscConfig.OscConf { Chatbox = true, Hoscy = false, HoscySendPort = 9001, QuestSupport = false }, - Chatbox = new Conf.ChatboxConf + Chatbox = new ShockOscConfig.ChatboxConf { DisplayRemoteControl = true, - HoscyType = Conf.ChatboxConf.HoscyMessageType.Message, + HoscyType = ShockOscConfig.ChatboxConf.HoscyMessageType.Message, Prefix = null!, Types = null!, IgnoredKillSwitchActive = null!, IgnoredAfk = null! }, - Behaviour = new Conf.BehaviourConf + Behaviour = new ShockOscConfig.BehaviourConf { RandomDuration = false, RandomIntensity = false, @@ -106,19 +106,19 @@ public static void Save() FixedIntensity = 50, CooldownTime = 5000, HoldTime = 250, - WhileBoneHeld = Conf.BehaviourConf.BoneHeldAction.Vibrate, + WhileBoneHeld = ShockOscConfig.BehaviourConf.BoneHeldAction.Vibrate, DisableWhileAfk = true, ForceUnmute = false }, - OpenShock = new Conf.OpenShockConf + OpenShock = new ShockOscConfig.OpenShockConf { - Shockers = new Dictionary(), + Shockers = new Dictionary(), Token = "", }, - Groups = new Dictionary() + Groups = new Dictionary() }; - public class Conf + public class ShockOscConfig { public required OscConf Osc { get; set; } public required BehaviourConf Behaviour { get; set; } diff --git a/ShockOsc/Ui/Components/Layout/Login.razor b/ShockOsc/Ui/Components/Layout/Login.razor deleted file mode 100644 index e0ff8dd..0000000 --- a/ShockOsc/Ui/Components/Layout/Login.razor +++ /dev/null @@ -1,22 +0,0 @@ -@code { - [Parameter] - public bool Loading { get; set; } -} - - - - Login -
- @if (!Loading) - { - -
- Continue -
- } - else - { - - } -
-
\ No newline at end of file diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index d0ab87b..3e4ae6a 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,45 +1,23 @@ @using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Layout @using OpenShock.ShockOsc.Ui.Components.Tabs -@using OpenShock.ShockOsc.Models; @using OpenShock.ShockOsc.Utils @using Serilog -@using Serilog.Events @using OpenShock.SDK.CSharp.Models +@using OpenShock.ShockOsc.Ui.Utils @inject ISnackbar Snackbar @inherits LayoutComponentBase +@inject ShockOscConfigManager.ShockOscConfig Config - +@page "/main" + + @code { private static readonly ILogger Logger = Log.ForContext(typeof(MainLayout)); - readonly MudTheme _myCustomTheme = new() - { - Palette = new PaletteDark - { - Primary = "#8f38fd", - PrimaryDarken = "#722cca", - Secondary = Colors.Green.Accent4, - AppbarBackground = Colors.Red.Default, - Background = "#2f2f2f", - Surface = "#1f1f1f", - }, - LayoutProperties = new LayoutProperties - { - DrawerWidthLeft = "260px", - DrawerWidthRight = "300px" - }, - Typography = new Typography - { - Default = new Default - { - FontFamily = new string[] { "'Poppins', Roboto, Helvetica, Arial, sans-serif" } - }, - } - }; } @@ -64,266 +42,156 @@ - + @* *@ - @if (!_authenticated) - { - - } - else - { - - - - - - - - - Chatbox Options - - - -
-
- - - - - @foreach (Config.Conf.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(Config.Conf.ChatboxConf.HoscyMessageType))) - { - @hoscyMessageType - } - - -
-
- - @if (_advancedSettingsExpanded) - { - Advanced Settings - } - else + + + + + + + + + Chatbox Options + + + +
+
+ + + + + @foreach (ShockOscConfigManager.ShockOscConfig.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.ChatboxConf.HoscyMessageType))) { - Advanced Settings + @hoscyMessageType } - - - - - -
- - @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) - { - - - - - - - } - -
-
+
- - - Shocker Options - - - - @foreach (Config.Conf.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(Config.Conf.BehaviourConf.BoneHeldAction))) - { - @boneHeldAction - } - - - - -
- - - - -
- @if (intensity == "Fixed Intensity") - { - Intensity: @_config.Behaviour.FixedIntensity.ToString()% - } - else - { - Intensity Min: @_config.Behaviour.IntensityRange.Min.ToString()% -
- Intensity Max: @_config.Behaviour.IntensityRange.Max.ToString()% - } - - - - -
- @if (duration == "Fixed Duration") +
+
+ + @if (_advancedSettingsExpanded) { - + Advanced Settings } else { - - - + Advanced Settings } -
-
-
- - - Other Options - - - - - + + + + +
+ + @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) + { + + + + + + + } + +
+ +
-
- - Refresh - @if (_editShockers) + + Shocker Options + + + + @foreach (ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction))) + { + @boneHeldAction + } + + + + +
+ + + + +
+ @if (intensity == "Fixed Intensity") { - Save + Intensity: @Config.Behaviour.FixedIntensity.ToString()% } else { - Rename + Intensity Min: @Config.Behaviour.IntensityRange.Min.ToString()% +
+ Intensity Max: @Config.Behaviour.IntensityRange.Max.ToString()% } + + + +
-
- - - Enabled - Name - Guid - - - - - - @context.Name - @context.Id - - -
- - List of ShockOSC prefixed parameters and their states -
- Avatar ID: @ShockOsc.AvatarId -
- - -
- @if (_showAllAvatarParams) + @if (duration == "Fixed Duration") { - @foreach (var param in ShockOsc.AllAvatarParams) - { - - } + } else { - @foreach (var param in ShockOsc.ParamsInUse) - { - - } + + + } -
- - - - Time - Source - Message - - - @context.Time.ToString("HH:mm:ss") - @context.SourceContextShort - @context.Message.TruncateAtChar(120) - - - @if (context.IsExpanded) - { - - - -
- - - - -
-
- - @context.Message - -
-
- -
- } -
- @* *@ - @* *@ - @* *@ -
-
-
-
- } +
+
+ + + + Other Options + + + + + + + + + + + + + + + + + + + + @code { private int activePageIndex = 0; private bool _advancedSettingsExpanded = false; private bool _editShockers = false; - private bool _showAllAvatarParams = false; - - private Config.Conf _config; + private string intensity = "Fixed Intensity"; private string duration = "Fixed Duration"; - - private bool _loading = false; - private bool _authenticated = false; - + private void OnAdvancedSettingsClick() { _advancedSettingsExpanded = !_advancedSettingsExpanded; } - private string GetLogClass(LogEventLevel level) - => $"log {level.ToString().ToLowerInvariant()}"; - - private void LogRowClick(TableRowClickEventArgs rowClickEventArgs) - { - rowClickEventArgs.Item.IsExpanded = !rowClickEventArgs.Item.IsExpanded; - } - private void OnParamsChange(bool shockOscParam) - { - // check Debug page is active - if (activePageIndex != 3) - return; - - // only redraw page when needed - if (!_showAllAvatarParams && !shockOscParam) - return; - - _updateQueued = true; - } private void OnLogAdded() { @@ -344,12 +212,6 @@ Snackbar.Add(msg, severity); } - private Task OnShockerConfigUpdate() - { - ShockOsc.RefreshShockers(); - return Task.CompletedTask; - } - private DateTime _lastSettingsSave = DateTime.Now; private CancellationTokenSource _cts = new CancellationTokenSource(); @@ -383,65 +245,29 @@ private Task OnSettingsValueSave() { _lastSettingsSave = DateTime.Now; - _config.Behaviour.RandomIntensity = intensity == "Random Intensity"; - _config.Behaviour.RandomDuration = duration == "Random Duration"; + Config.Behaviour.RandomIntensity = intensity == "Random Intensity"; + Config.Behaviour.RandomDuration = duration == "Random Duration"; ValidateSettings(); - Config.Save(); + ShockOscConfigManager.Save(); UnderscoreConfig.SendUpdateForAll(); return Task.CompletedTask; } private void ValidateSettings() { - if (_config.Behaviour.IntensityRange.Min > _config.Behaviour.IntensityRange.Max) - { - _config.Behaviour.IntensityRange.Max = _config.Behaviour.IntensityRange.Min; - } + if (Config.Behaviour.IntensityRange.Min > Config.Behaviour.IntensityRange.Max) Config.Behaviour.IntensityRange.Max = Config.Behaviour.IntensityRange.Min; } - private void SetAuthLoading(ShockOsc.AuthState authState) - { - _authenticated = authState == ShockOsc.AuthState.Authenticated; - _loading = authState == ShockOsc.AuthState.Authenticating; - InvokeAsync(StateHasChanged); - } protected override async Task OnInitializedAsync() { - ShockOsc.SetAuthLoading = SetAuthLoading; - ShockOsc.OnParamsChange = OnParamsChange; + ShockOsc.OnConfigUpdate = OnConfigUpdate; UiLogSink.NotificationAction = MsgNoty; LogStore.OnLogAdded = OnLogAdded; - - SetAuthLoading(ShockOsc.CurrentAuthState); - _config = Config.ConfigInstance; - intensity = _config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; - duration = _config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; - } - - private string RowClassFunc(LogStore.LogEntry? log, int arg2) => log == null ? string.Empty : GetLogClass(log.Level) + (log.IsExpanded ? " expanded" : ""); - - private bool _updateQueued = true; - - protected override void OnInitialized() - { - OsTask.Run(UpdateParams); - } - - private async Task UpdateParams() - { - while (true) - { - await Task.Delay(250); - - if (!_updateQueued) - continue; - _updateQueued = false; - - await InvokeAsync(StateHasChanged); - } + + intensity = Config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; + duration = Config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; } - } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/DebugTab.razor b/ShockOsc/Ui/Components/Tabs/DebugTab.razor new file mode 100644 index 0000000..76a8b45 --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/DebugTab.razor @@ -0,0 +1,65 @@ +@using OpenShock.ShockOsc.Utils +List of ShockOSC prefixed parameters and their states +
+Avatar ID: @ShockOsc.AvatarId +
+ + +
+@if (_showAllAvatarParams) +{ + @foreach (var param in ShockOsc.AllAvatarParams) + { + + } +} +else +{ + @foreach (var param in ShockOsc.ParamsInUse) + { + + } +} + +@code { + private bool _showAllAvatarParams = false; + + protected override async Task OnInitializedAsync() + { + ShockOsc.OnParamsChange = OnParamsChange; + } + + private void OnParamsChange(bool shockOscParam) + { + // check Debug page is active + // if (activePageIndex != 3) + // return; + + // only redraw page when needed + if (!_showAllAvatarParams && !shockOscParam) + return; + + _updateQueued = true; + } + + private bool _updateQueued = true; + + protected override void OnInitialized() + { + OsTask.Run(UpdateParams); + } + + private async Task UpdateParams() + { + while (true) + { + await Task.Delay(250); + + if (!_updateQueued) + continue; + _updateQueued = false; + + await InvokeAsync(StateHasChanged); + } + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index 7d2dab8..ab444b7 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -1,5 +1,7 @@ @using System.Text.RegularExpressions -@using Swan +@using OpenShock.ShockOsc.Backend +@inject OpenShockApi OpenShockApi + @code { @@ -8,26 +10,26 @@ public void AddGroup() { var groupId = Guid.NewGuid(); - Config.ConfigInstance.Groups.Add(groupId, new Config.Conf.Group { Name = "New Group" }); + ShockOscConfigManager.ConfigInstance.Groups.Add(groupId, new ShockOscConfigManager.ShockOscConfig.Group { Name = "New Group" }); Group = groupId; InvokeAsync(StateHasChanged); - Config.Save(); + ShockOscConfigManager.Save(); } public void DeleteGroup() { if (Group == null) return; - Config.ConfigInstance.Groups.Remove(Group.Value); + ShockOscConfigManager.ConfigInstance.Groups.Remove(Group.Value); Group = null; InvokeAsync(StateHasChanged); - Config.Save(); + ShockOscConfigManager.Save(); } private Task OnSettingsValueChange() { - Config.Save(); + ShockOscConfigManager.Save(); return Task.CompletedTask; } @@ -44,13 +46,13 @@ if (CurrentGroup != null) { CurrentGroup.Shockers = _selectedShockers.ToList(); - Config.Save(); + ShockOscConfigManager.Save(); } return Task.CompletedTask; } - private Config.Conf.Group? CurrentGroup => Group == null ? null : Config.ConfigInstance.Groups.TryGetValue(Group.Value, out var group) ? group : null; + private ShockOscConfigManager.ShockOscConfig.Group? CurrentGroup => Group == null ? null : ShockOscConfigManager.ConfigInstance.Groups.TryGetValue(Group.Value, out var group) ? group : null; private static Regex _nameRegex = new Regex(@"^[a-zA-Z0-9\/\\ -]+$", RegexOptions.Compiled); @@ -69,7 +71,7 @@ Delete Group

- @foreach (var group in Config.ConfigInstance.Groups) + @foreach (var group in ShockOscConfigManager.ConfigInstance.Groups) { @group.Value.Name } diff --git a/ShockOsc/Ui/Components/Tabs/LogsTab.razor b/ShockOsc/Ui/Components/Tabs/LogsTab.razor new file mode 100644 index 0000000..e1b85d8 --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/LogsTab.razor @@ -0,0 +1,50 @@ +@using OpenShock.ShockOsc.Logging +@using OpenShock.ShockOsc.Utils +@using Serilog.Events + + + + Time + Source + Message + + + @context.Time.ToString("HH:mm:ss") + @context.SourceContextShort + @context.Message.TruncateAtChar(120) + + + @if (context.IsExpanded) + { + + + +
+ + + + +
+
+ + @context.Message + +
+
+ +
+ } +
+
+ +@code { + private string GetLogClass(LogEventLevel level) + => $"log {level.ToString().ToLowerInvariant()}"; + + private void LogRowClick(TableRowClickEventArgs rowClickEventArgs) + { + rowClickEventArgs.Item.IsExpanded = !rowClickEventArgs.Item.IsExpanded; + } + + private string RowClassFunc(LogStore.LogEntry? log, int arg2) => log == null ? string.Empty : GetLogClass(log.Level) + (log.IsExpanded ? " expanded" : ""); +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor b/ShockOsc/Ui/Components/Tabs/ShockersTab.razor new file mode 100644 index 0000000..089f03c --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/ShockersTab.razor @@ -0,0 +1,29 @@ +@using OpenShock.ShockOsc.Backend +@inject OpenShockApi OpenShockApi +@inject ShockOscConfigManager.ShockOscConfig Config + +Refresh +
+
+ + + Enabled + Name + Guid + + + + + + @context.Name + @context.Id + + + +@code { + private Task OnShockerConfigUpdate() + { + ShockOsc.RefreshShockers(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/UpdateDialog.razor b/ShockOsc/Ui/Components/UpdateDialog.razor index bdaa727..c1b4dfb 100644 --- a/ShockOsc/Ui/Components/UpdateDialog.razor +++ b/ShockOsc/Ui/Components/UpdateDialog.razor @@ -6,8 +6,8 @@ private void Skip() { - Config.ConfigInstance.LastIgnoredVersion = Updater.LatestVersion; - Config.Save(); + ShockOscConfigManager.ConfigInstance.LastIgnoredVersion = Updater.LatestVersion; + ShockOscConfigManager.Save(); MudDialog.Close(DialogResult.Ok(true)); } diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Components/UpdateLogout.razor index 6965851..6aa87d1 100644 --- a/ShockOsc/Ui/Components/UpdateLogout.razor +++ b/ShockOsc/Ui/Components/UpdateLogout.razor @@ -22,7 +22,7 @@ } - @if (Updater.UpdateAvailable) + @if (true || Updater.UpdateAvailable) { @@ -33,7 +33,7 @@ @if (Authenticated) { - + diff --git a/ShockOsc/Ui/Main.razor b/ShockOsc/Ui/Main.razor index 90b699a..5b60fbb 100644 --- a/ShockOsc/Ui/Main.razor +++ b/ShockOsc/Ui/Main.razor @@ -1,12 +1,16 @@ @using OpenShock.ShockOsc.Ui.Components + - - + +

Sorry, there's nothing at this address.

+ +

Page is loading...

+
\ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor new file mode 100644 index 0000000..91524af --- /dev/null +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -0,0 +1,55 @@ +@using OpenShock.ShockOsc.Ui.Utils +@using OpenShock.SDK.CSharp.Live +@using OpenShock.ShockOsc.Backend +@inherits LayoutComponentBase + +@inject ShockOscConfigManager.ShockOscConfig ShockOscConfig +@inject NavigationManager NavigationManager +@inject BackendLiveApiManager LiveApiManager +@inject OpenShockApiLiveClient LiveApiClient +@inject OpenShockApi ApiClient + +@page "/" + + + + + + + + Login +
+ @if (!Loading) + { + +
+ Continue +
+ } + else + { + + } +
+
+ +@code { + public bool Loading { get; set; } + + public async Task Login() + { + ShockOscConfigManager.Save(); + + await ProceedAuthenticated(); + } + + private async Task ProceedAuthenticated() + { + Loading = true; + + await LiveApiManager.SetupLiveClient(); + await LiveApiClient.StartAsync(); + + NavigationManager.NavigateTo("main"); + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Utils/ThemeDefinition.cs b/ShockOsc/Ui/Utils/ThemeDefinition.cs new file mode 100644 index 0000000..963a101 --- /dev/null +++ b/ShockOsc/Ui/Utils/ThemeDefinition.cs @@ -0,0 +1,31 @@ +using MudBlazor; + +namespace OpenShock.ShockOsc.Ui.Utils; + +public static class ThemeDefinition +{ + public static readonly MudTheme ShockOscTheme = new() + { + Palette = new PaletteDark + { + Primary = "#8f38fd", + PrimaryDarken = "#722cca", + Secondary = MudBlazor.Colors.Green.Accent4, + AppbarBackground = MudBlazor.Colors.Red.Default, + Background = "#2f2f2f", + Surface = "#1f1f1f", + }, + LayoutProperties = new LayoutProperties + { + DrawerWidthLeft = "260px", + DrawerWidthRight = "300px" + }, + Typography = new Typography + { + Default = new Default + { + FontFamily = new string[] { "'Poppins', Roboto, Helvetica, Arial, sans-serif" } + }, + } + }; +} \ No newline at end of file diff --git a/ShockOsc/UnderscoreConfig.cs b/ShockOsc/UnderscoreConfig.cs index a9a5045..6be292a 100644 --- a/ShockOsc/UnderscoreConfig.cs +++ b/ShockOsc/UnderscoreConfig.cs @@ -41,12 +41,12 @@ public static void HandleCommand(string parameterName, object?[] arguments) // 0..100% if (value is float minIntensityFloat) { - var currentMinIntensity = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.IntensityRange.Min / 100f); + var currentMinIntensity = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min / 100f); if (minIntensityFloat == currentMinIntensity) return; - Config.ConfigInstance.Behaviour.IntensityRange.Min = ShockOsc.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); + ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min = ShockOsc.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); ValidateSettings(); - Config.Save(); + ShockOscConfigManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -55,12 +55,12 @@ public static void HandleCommand(string parameterName, object?[] arguments) // 0..100% if (value is float maxIntensityFloat) { - var currentMaxIntensity = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.IntensityRange.Max / 100f); + var currentMaxIntensity = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max / 100f); if (maxIntensityFloat == currentMaxIntensity) return; - Config.ConfigInstance.Behaviour.IntensityRange.Max = ShockOsc.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); + ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max = ShockOsc.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); ValidateSettings(); - Config.Save(); + ShockOscConfigManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -69,12 +69,12 @@ public static void HandleCommand(string parameterName, object?[] arguments) // 0..10sec if (value is float durationFloat) { - var currentDuration = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.FixedDuration / 10000f); + var currentDuration = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.FixedDuration / 10000f); if (durationFloat == currentDuration) return; - Config.ConfigInstance.Behaviour.FixedDuration = ShockOsc.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); + ShockOscConfigManager.ConfigInstance.Behaviour.FixedDuration = ShockOsc.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); ValidateSettings(); - Config.Save(); + ShockOscConfigManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -83,12 +83,12 @@ public static void HandleCommand(string parameterName, object?[] arguments) // 0..100sec if (value is float cooldownTimeFloat) { - var currentCooldownTime = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.CooldownTime / 100000f); + var currentCooldownTime = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime / 100000f); if (cooldownTimeFloat == currentCooldownTime) return; - Config.ConfigInstance.Behaviour.CooldownTime = ShockOsc.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); + ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime = ShockOsc.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); ValidateSettings(); - Config.Save(); + ShockOscConfigManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -98,12 +98,12 @@ public static void HandleCommand(string parameterName, object?[] arguments) // 0..1sec if (value is float holdTimeFloat) { - var currentHoldTime = ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.HoldTime / 1000f); + var currentHoldTime = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.HoldTime / 1000f); if (holdTimeFloat == currentHoldTime) return; - Config.ConfigInstance.Behaviour.HoldTime = ShockOsc.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); + ShockOscConfigManager.ConfigInstance.Behaviour.HoldTime = ShockOsc.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); ValidateSettings(); - Config.Save(); + ShockOscConfigManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -126,19 +126,19 @@ public static void HandleCommand(string parameterName, object?[] arguments) private static void ValidateSettings() { - if (Config.ConfigInstance.Behaviour.IntensityRange.Min > Config.ConfigInstance.Behaviour.IntensityRange.Max) + if (ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min > ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max) { - Config.ConfigInstance.Behaviour.IntensityRange.Max = Config.ConfigInstance.Behaviour.IntensityRange.Min; + ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max = ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min; } } public static async Task SendUpdateForAll() { await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/Paused", KillSwitch); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.IntensityRange.Min / 100f)); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.IntensityRange.Max / 100f)); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.FixedDuration / 10000f)); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.CooldownTime / 100000f)); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", ShockOsc.ClampFloat(Config.ConfigInstance.Behaviour.HoldTime / 1000f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min / 100f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max / 100f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.FixedDuration / 10000f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime / 100000f)); + await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.HoldTime / 1000f)); } } \ No newline at end of file diff --git a/ShockOsc/Updater.cs b/ShockOsc/Updater.cs index a33cd87..479ecf9 100644 --- a/ShockOsc/Updater.cs +++ b/ShockOsc/Updater.cs @@ -94,8 +94,8 @@ public static async Task CheckUpdate() UpdateAvailable = true; LatestVersion = latestVersion.Value.Item1; LatestDownloadUrl = latestVersion.Value.Item2.BrowserDownloadUrl; - if (Config.ConfigInstance.LastIgnoredVersion != null && - Config.ConfigInstance.LastIgnoredVersion >= latestVersion.Value.Item1) + if (ShockOscConfigManager.ConfigInstance.LastIgnoredVersion != null && + ShockOscConfigManager.ConfigInstance.LastIgnoredVersion >= latestVersion.Value.Item1) { Logger.Information("ShockOsc is not up to date. Skipping update due to previous postpone. You can reenable the updater by setting 'LastIgnoredVersion' to null"); return false; diff --git a/ShockOsc/UserHubClient.cs b/ShockOsc/UserHubClient.cs deleted file mode 100644 index cc19a0e..0000000 --- a/ShockOsc/UserHubClient.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Microsoft.Extensions.Logging; -using OpenShock.SDK.CSharp.Live; -using OpenShock.SDK.CSharp.Live.Models; -using Serilog; -using ILogger = Serilog.ILogger; - -namespace OpenShock.ShockOsc; - -public static class UserHubClient -{ - private static readonly ILogger Logger = Log.ForContext(typeof(UserHubClient)); - public static string? ConnectionId { get; set; } - - private static OpenShockApiLiveClient? _liveClient; - - - public static async Task SetupLiveClient() - { - if(_liveClient != null) await _liveClient.DisposeAsync(); - _liveClient = new OpenShockApiLiveClient(new ApiLiveClientOptions() - { - Server = Config.ConfigInstance.OpenShock.Backend, - Token = Config.ConfigInstance.OpenShock.Token, - ConfigureLogging = builder => - { - builder.ClearProviders(); - builder.SetMinimumLevel(LogLevel.Trace); - builder.AddSerilog(); - } - }); - - _liveClient.OnLog(LogReceive); - _liveClient.OnWelcome(WelcomeReceive); - - - _liveClient.Connection.Closed += exception => { - ShockOsc.SetAuthSate(ShockOsc.AuthState.NotAuthenticated); - return Task.CompletedTask; - }; - - await _liveClient.StartAsync(); - } - - - public static Task? Control(params Control[] data) => _liveClient?.Control(data, "ShockOsc"); - - public static async Task Disconnect() - { - if (_liveClient != null) await _liveClient.DisposeAsync(); - } - - #region Handlers - - private static Task WelcomeReceive(string connectionId) - { - ConnectionId = connectionId; - ShockOsc.SetAuthSate(ShockOsc.AuthState.Authenticated); - return Task.CompletedTask; - } - - private static async Task LogReceive(ControlLogSender sender, ICollection logs) - { - Logger.Debug("Received Sender: {@Sender} Logs: {@Logs}", sender, logs); - - foreach (var log in logs) - await ShockOsc.RemoteActivateShocker(sender, log); - } - - #endregion - -} \ No newline at end of file From 942a8b82076cdd974b112d1254490d0c831f2792 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 6 Apr 2024 22:27:05 +0200 Subject: [PATCH 20/95] More layout split --- ShockOsc/Ui/Components/MainLayout.razor | 195 +----------------- ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 189 +++++++++++++++++ ShockOsc/Ui/Components/Tabs/DebugTab.razor | 30 +-- ShockOsc/Ui/Components/Tabs/LogsTab.razor | 17 ++ .../Pages/Authentication/Authenticate.razor | 13 +- 5 files changed, 237 insertions(+), 207 deletions(-) create mode 100644 ShockOsc/Ui/Components/Tabs/ConfigTab.razor diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 3e4ae6a..bec758f 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,9 +1,7 @@ @using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Layout @using OpenShock.ShockOsc.Ui.Components.Tabs -@using OpenShock.ShockOsc.Utils @using Serilog -@using OpenShock.SDK.CSharp.Models @using OpenShock.ShockOsc.Ui.Utils @inject ISnackbar Snackbar @inherits LayoutComponentBase @@ -55,112 +53,7 @@ - - Chatbox Options - - - -
-
- - - - - @foreach (ShockOscConfigManager.ShockOscConfig.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.ChatboxConf.HoscyMessageType))) - { - @hoscyMessageType - } - - -
-
- - @if (_advancedSettingsExpanded) - { - Advanced Settings - } - else - { - Advanced Settings - } - - - - - -
- - @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) - { - - - - - - - } - -
-
-
- - - Shocker Options - - - - @foreach (ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction))) - { - @boneHeldAction - } - - - - -
- - - - -
- @if (intensity == "Fixed Intensity") - { - Intensity: @Config.Behaviour.FixedIntensity.ToString()% - } - else - { - Intensity Min: @Config.Behaviour.IntensityRange.Min.ToString()% -
- Intensity Max: @Config.Behaviour.IntensityRange.Max.ToString()% - } - - - - -
- @if (duration == "Fixed Duration") - { - - } - else - { - - - - } -
-
-
- - - Other Options - - - - - - - +
@@ -178,96 +71,14 @@ @code { private int activePageIndex = 0; - private bool _advancedSettingsExpanded = false; - private bool _editShockers = false; - - - private string intensity = "Fixed Intensity"; - private string duration = "Fixed Duration"; - - private void OnAdvancedSettingsClick() - { - _advancedSettingsExpanded = !_advancedSettingsExpanded; - } - - - - private void OnLogAdded() - { - // check Log page is active - if (activePageIndex == 4) - InvokeAsync(StateHasChanged); - } - - private void OnConfigUpdate() - { - // check Config page is active - if (activePageIndex == 1) - InvokeAsync(StateHasChanged); - } - private void MsgNoty(string msg, Severity severity) { Snackbar.Add(msg, severity); } - - private DateTime _lastSettingsSave = DateTime.Now; - private CancellationTokenSource _cts = new CancellationTokenSource(); - - private async Task OnSettingsValueChange() - { - if ((DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) - { - await OnSettingsValueSave(); - return; - } - - _cts.Cancel(); - _cts = new CancellationTokenSource(); - - try - { - await Task.Delay(10, _cts.Token); - - if (!_cts.Token.IsCancellationRequested && - (DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) - { - await OnSettingsValueSave(); - } - } - catch (TaskCanceledException) - { - // Task was cancelled, which is expected - } - } - - private Task OnSettingsValueSave() - { - _lastSettingsSave = DateTime.Now; - Config.Behaviour.RandomIntensity = intensity == "Random Intensity"; - Config.Behaviour.RandomDuration = duration == "Random Duration"; - ValidateSettings(); - - ShockOscConfigManager.Save(); - UnderscoreConfig.SendUpdateForAll(); - return Task.CompletedTask; - } - - private void ValidateSettings() - { - if (Config.Behaviour.IntensityRange.Min > Config.Behaviour.IntensityRange.Max) Config.Behaviour.IntensityRange.Max = Config.Behaviour.IntensityRange.Min; - } - - - protected override async Task OnInitializedAsync() + + protected override void OnInitialized() { - - ShockOsc.OnConfigUpdate = OnConfigUpdate; UiLogSink.NotificationAction = MsgNoty; - LogStore.OnLogAdded = OnLogAdded; - - intensity = Config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; - duration = Config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; } } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor new file mode 100644 index 0000000..34c53c7 --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -0,0 +1,189 @@ +@inject ShockOscConfigManager.ShockOscConfig Config +@using OpenShock.SDK.CSharp.Models +@implements IDisposable + + + + Chatbox Options + + + +
+
+ + + + + @foreach (ShockOscConfigManager.ShockOscConfig.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.ChatboxConf.HoscyMessageType))) + { + @hoscyMessageType + } + + +
+
+ + @if (_advancedSettingsExpanded) + { + Advanced Settings + } + else + { + Advanced Settings + } + + + + + +
+ + @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) + { + + + + + + + } + +
+
+
+ + + Shocker Options + + + + @foreach (ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction))) + { + @boneHeldAction + } + + + + +
+ + + + +
+ @if (intensity == "Fixed Intensity") + { + Intensity: @Config.Behaviour.FixedIntensity.ToString()% + } + else + { + Intensity Min: @Config.Behaviour.IntensityRange.Min.ToString()% +
+ Intensity Max: @Config.Behaviour.IntensityRange.Max.ToString()% + } + + + + +
+ @if (duration == "Fixed Duration") + { + + } + else + { + + + + } +
+
+
+ + + Other Options + + + + + + + +@code { + private bool _advancedSettingsExpanded = false; + + private string intensity = "Fixed Intensity"; + private string duration = "Fixed Duration"; + + private DateTime _lastSettingsSave = DateTime.Now; + private CancellationTokenSource _cts = new CancellationTokenSource(); + + private void OnAdvancedSettingsClick() + { + _advancedSettingsExpanded = !_advancedSettingsExpanded; + } + + private async Task OnSettingsValueChange() + { + if ((DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) + { + await OnSettingsValueSave(); + return; + } + + _cts.Cancel(); + _cts = new CancellationTokenSource(); + + try + { + await Task.Delay(10, _cts.Token); + + if (!_cts.Token.IsCancellationRequested && + (DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) + { + await OnSettingsValueSave(); + } + } + catch (TaskCanceledException) + { + // Task was cancelled, which is expected + } + } + + private Task OnSettingsValueSave() + { + _lastSettingsSave = DateTime.Now; + Config.Behaviour.RandomIntensity = intensity == "Random Intensity"; + Config.Behaviour.RandomDuration = duration == "Random Duration"; + ValidateSettings(); + + ShockOscConfigManager.Save(); + UnderscoreConfig.SendUpdateForAll(); + return Task.CompletedTask; + } + + private void ValidateSettings() + { + if (Config.Behaviour.IntensityRange.Min > Config.Behaviour.IntensityRange.Max) Config.Behaviour.IntensityRange.Max = Config.Behaviour.IntensityRange.Min; + } + + protected override void OnInitialized() + { + + ShockOsc.OnConfigUpdate = OnConfigUpdate; + + intensity = Config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; + duration = Config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; + } + + private void OnConfigUpdate() + { + InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + ShockOsc.OnConfigUpdate = null; + } + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/DebugTab.razor b/ShockOsc/Ui/Components/Tabs/DebugTab.razor index 76a8b45..26875b9 100644 --- a/ShockOsc/Ui/Components/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DebugTab.razor @@ -1,4 +1,7 @@ @using OpenShock.ShockOsc.Utils +@using Serilog.Sinks.SystemConsole.Themes +@implements IAsyncDisposable + List of ShockOSC prefixed parameters and their states
Avatar ID: @ShockOsc.AvatarId @@ -23,18 +26,9 @@ else @code { private bool _showAllAvatarParams = false; - - protected override async Task OnInitializedAsync() - { - ShockOsc.OnParamsChange = OnParamsChange; - } - + private void OnParamsChange(bool shockOscParam) { - // check Debug page is active - // if (activePageIndex != 3) - // return; - // only redraw page when needed if (!_showAllAvatarParams && !shockOscParam) return; @@ -46,20 +40,30 @@ else protected override void OnInitialized() { + ShockOsc.OnParamsChange = OnParamsChange; OsTask.Run(UpdateParams); } private async Task UpdateParams() { - while (true) + while (!_cts.IsCancellationRequested) { - await Task.Delay(250); - if (!_updateQueued) continue; _updateQueued = false; await InvokeAsync(StateHasChanged); + + await Task.Delay(200); } } + + private CancellationTokenSource _cts = new CancellationTokenSource(); + + + public async ValueTask DisposeAsync() + { + await _cts.CancelAsync(); + } + } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/LogsTab.razor b/ShockOsc/Ui/Components/Tabs/LogsTab.razor index e1b85d8..76c0fbc 100644 --- a/ShockOsc/Ui/Components/Tabs/LogsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/LogsTab.razor @@ -1,6 +1,7 @@ @using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Utils @using Serilog.Events +@implements IDisposable @@ -47,4 +48,20 @@ } private string RowClassFunc(LogStore.LogEntry? log, int arg2) => log == null ? string.Empty : GetLogClass(log.Level) + (log.IsExpanded ? " expanded" : ""); + + protected override void OnInitialized() + { + LogStore.OnLogAdded = OnLogAdded; + } + + private void OnLogAdded() + { + InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + LogStore.OnLogAdded = null; + } + } \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index 91524af..2785d48 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -1,6 +1,7 @@ @using OpenShock.ShockOsc.Ui.Utils @using OpenShock.SDK.CSharp.Live @using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Ui.Components.Layout @inherits LayoutComponentBase @inject ShockOscConfigManager.ShockOscConfig ShockOscConfig @@ -15,6 +16,8 @@ + + Login @@ -34,12 +37,18 @@ @code { - public bool Loading { get; set; } + private bool Loading { get; set; } + + protected override async Task OnInitializedAsync() + { + if(string.IsNullOrEmpty(ShockOscConfig.OpenShock.Token)) return; + + await ProceedAuthenticated(); + } public async Task Login() { ShockOscConfigManager.Save(); - await ProceedAuthenticated(); } From e2f5ed9441ed8c7c5b068e4d4ef5b8577bcea504 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 7 Apr 2024 01:46:20 +0200 Subject: [PATCH 21/95] More groups rework --- ShockOsc/Backend/OpenShockApi.cs | 5 +- ShockOsc/MauiProgram.cs | 20 +- .../Models/{Shocker.cs => ProgramGroup.cs} | 12 +- .../OscChangeTracker/ChangeTrackedOscParam.cs | 11 +- ShockOsc/OscClient.cs | 51 +-- ShockOsc/OscQueryLibrary/OscQueryServer.cs | 23 +- ShockOsc/ShockOsc.cs | 333 ++++++++---------- ShockOsc/ShockOscConfigManager.cs | 1 - ShockOsc/Ui/App.xaml.cs | 1 - ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 8 +- ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 2 + ShockOsc/Ui/Components/Tabs/ShockersTab.razor | 5 +- .../Pages/Authentication/Authenticate.razor | 3 + .../Ui/Utils/AsynchronousEventExtensions.cs | 17 + ShockOsc/UnderscoreConfig.cs | 46 ++- 15 files changed, 267 insertions(+), 271 deletions(-) rename ShockOsc/Models/{Shocker.cs => ProgramGroup.cs} (83%) create mode 100644 ShockOsc/Ui/Utils/AsynchronousEventExtensions.cs diff --git a/ShockOsc/Backend/OpenShockApi.cs b/ShockOsc/Backend/OpenShockApi.cs index a69dbb0..f1a3707 100644 --- a/ShockOsc/Backend/OpenShockApi.cs +++ b/ShockOsc/Backend/OpenShockApi.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using OpenShock.SDK.CSharp; +using OpenShock.SDK.CSharp.Live.Utils; using OpenShock.SDK.CSharp.Models; namespace OpenShock.ShockOsc.Backend; @@ -25,6 +26,8 @@ public void SetupApiClient() Token = _config.OpenShock.Token }); } + + public event Func, Task>? OnShockersUpdated; public IReadOnlyCollection Shockers = Array.Empty(); @@ -54,7 +57,7 @@ public async Task RefreshShockers() } ShockOscConfigManager.ConfigInstance.OpenShock.Shockers = shockerList; ShockOscConfigManager.Save(); - ShockOsc.RefreshShockers(); + OnShockersUpdated.Raise(Shockers); }, error => { diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 45a13a6..d3a55b4 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,8 +1,10 @@ -using Microsoft.Extensions.Logging; +using System.Net; +using Microsoft.Extensions.Logging; using MudBlazor.Services; using OpenShock.SDK.CSharp.Live; using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Logging; +using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Ui; using Serilog; @@ -42,13 +44,27 @@ public static MauiApp CreateMauiApp() builder.Services.AddSerilog(Log.Logger); builder.Services.AddSingleton(ShockOscConfigManager.ConfigInstance); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + var listenAddress = ShockOscConfigManager.ConfigInstance.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; + builder.Services.AddSingleton(provider => + { + var shockOsc = provider.GetRequiredService(); + return new OscQueryServer("ShockOsc", listenAddress, shockOsc.FoundVrcClient, shockOsc.OnAvatarChange); + }); + builder.Services.AddMudServices(); builder.Services.AddMauiBlazorWebView(); diff --git a/ShockOsc/Models/Shocker.cs b/ShockOsc/Models/ProgramGroup.cs similarity index 83% rename from ShockOsc/Models/Shocker.cs rename to ShockOsc/Models/ProgramGroup.cs index 08a8628..8b5a932 100644 --- a/ShockOsc/Models/Shocker.cs +++ b/ShockOsc/Models/ProgramGroup.cs @@ -2,7 +2,7 @@ namespace OpenShock.ShockOsc.Models; -public class Shocker +public sealed class ProgramGroup { public DateTime LastActive { get; set; } public DateTime LastExecuted { get; set; } @@ -22,15 +22,15 @@ public class Shocker public string Name { get; } public TriggerMethod TriggerMethod { get; set; } - public Shocker(Guid id, string name) + public ProgramGroup(Guid id, string name, OscClient oscClient) { Id = id; Name = name; - ParamActive = new ChangeTrackedOscParam(Name, "_Active", false); - ParamCooldown = new ChangeTrackedOscParam(Name, "_Cooldown", false); - ParamCooldownPercentage = new ChangeTrackedOscParam(Name, "_CooldownPercentage", 0f); - ParamIntensity = new ChangeTrackedOscParam(Name, "_Intensity", 0f); + ParamActive = new ChangeTrackedOscParam(Name, "_Active", false, oscClient); + ParamCooldown = new ChangeTrackedOscParam(Name, "_Cooldown", false, oscClient); + ParamCooldownPercentage = new ChangeTrackedOscParam(Name, "_CooldownPercentage", 0f, oscClient); + ParamIntensity = new ChangeTrackedOscParam(Name, "_Intensity", 0f, oscClient); } public void Reset() diff --git a/ShockOsc/OscChangeTracker/ChangeTrackedOscParam.cs b/ShockOsc/OscChangeTracker/ChangeTrackedOscParam.cs index d3c29a1..ee95215 100644 --- a/ShockOsc/OscChangeTracker/ChangeTrackedOscParam.cs +++ b/ShockOsc/OscChangeTracker/ChangeTrackedOscParam.cs @@ -4,27 +4,30 @@ namespace OpenShock.ShockOsc.OscChangeTracker; public class ChangeTrackedOscParam : IChangeTrackedOscParam { + private readonly OscClient _oscClient; + // ReSharper disable once StaticMemberInGenericType private static readonly ILogger Logger = Log.ForContext(typeof(ChangeTrackedOscParam<>)); public string Address { get; } public T Value { get; private set; } - public ChangeTrackedOscParam(string address, T initialValue) + public ChangeTrackedOscParam(string address, T initialValue, OscClient oscClient) { + _oscClient = oscClient; Address = address; Value = initialValue; } - public ChangeTrackedOscParam(string shockerName, string suffix, T initialValue) : this( - $"/avatar/parameters/ShockOsc/{shockerName}{suffix}", initialValue) + public ChangeTrackedOscParam(string shockerName, string suffix, T initialValue, OscClient oscClient) : this( + $"/avatar/parameters/ShockOsc/{shockerName}{suffix}", initialValue, oscClient) { } public ValueTask Send() { Logger.Debug("Sending parameter update for [{ParameterAddress}] with value [{Value}]", Address, Value); - return OscClient.SendGameMessage(Address, Value); + return _oscClient.SendGameMessage(Address, Value); } public ValueTask SetValue(T value) diff --git a/ShockOsc/OscClient.cs b/ShockOsc/OscClient.cs index 499e83a..fbc2175 100644 --- a/ShockOsc/OscClient.cs +++ b/ShockOsc/OscClient.cs @@ -1,57 +1,58 @@ using System.Net; using System.Threading.Channels; using LucHeart.CoreOSC; -using Serilog; +using Microsoft.Extensions.Logging; namespace OpenShock.ShockOsc; -public static class OscClient +public sealed class OscClient { - private static OscDuplex? _gameConnection; - private static readonly OscSender HoscySenderClient = new(new IPEndPoint(IPAddress.Loopback, ShockOscConfigManager.ConfigInstance.Osc.HoscySendPort)); - private static readonly ILogger Logger = Log.ForContext(typeof(OscClient)); + private readonly ILogger _logger; + private OscDuplex? _gameConnection; + private readonly OscSender _hoscySenderClient = new(new IPEndPoint(IPAddress.Loopback, ShockOscConfigManager.ConfigInstance.Osc.HoscySendPort)); - static OscClient() + public OscClient(ILogger logger) { + _logger = logger; Task.Run(GameSenderLoop); Task.Run(HoscySenderLoop); } - public static void CreateGameConnection(IPAddress ipAddress, ushort receivePort, ushort sendPort) + public void CreateGameConnection(IPAddress ipAddress, ushort receivePort, ushort sendPort) { _gameConnection?.Dispose(); _gameConnection = null; - Logger.Debug("Creating game connection with receive port {ReceivePort} and send port {SendPort}", receivePort, sendPort); + _logger.LogDebug("Creating game connection with receive port {ReceivePort} and send port {SendPort}", receivePort, sendPort); _gameConnection = new(new IPEndPoint(ipAddress, receivePort), new IPEndPoint(ipAddress, sendPort)); } - private static readonly Channel GameSenderChannel = Channel.CreateUnbounded(new UnboundedChannelOptions() + private readonly Channel _gameSenderChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true }); - private static readonly Channel HoscySenderChannel = Channel.CreateUnbounded(new UnboundedChannelOptions() + private readonly Channel _hoscySenderChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true }); - public static ValueTask SendGameMessage(string address, params object?[]?arguments) + public ValueTask SendGameMessage(string address, params object?[]?arguments) { arguments ??= Array.Empty(); - return GameSenderChannel.Writer.WriteAsync(new OscMessage(address, arguments)); + return _gameSenderChannel.Writer.WriteAsync(new OscMessage(address, arguments)); } - public static ValueTask SendChatboxMessage(string message) + public ValueTask SendChatboxMessage(string message) { - if (ShockOscConfigManager.ConfigInstance.Osc.Hoscy) return HoscySenderChannel.Writer.WriteAsync(new OscMessage("/hoscy/message", message)); + if (ShockOscConfigManager.ConfigInstance.Osc.Hoscy) return _hoscySenderChannel.Writer.WriteAsync(new OscMessage("/hoscy/message", message)); - return GameSenderChannel.Writer.WriteAsync(new OscMessage("/chatbox/input", message, true)); + return _gameSenderChannel.Writer.WriteAsync(new OscMessage("/chatbox/input", message, true)); } - private static async Task GameSenderLoop() + private async Task GameSenderLoop() { - Logger.Debug("Starting game sender loop"); - await foreach (var oscMessage in GameSenderChannel.Reader.ReadAllAsync()) + _logger.LogDebug("Starting game sender loop"); + await foreach (var oscMessage in _gameSenderChannel.Reader.ReadAllAsync()) { if (_gameConnection == null) continue; try @@ -60,28 +61,28 @@ private static async Task GameSenderLoop() } catch (Exception e) { - Logger.Error(e, "GameSenderClient send failed"); + _logger.LogError(e, "GameSenderClient send failed"); } } } - private static async Task HoscySenderLoop() + private async Task HoscySenderLoop() { - Logger.Debug("Starting hoscy sender loop"); - await foreach (var oscMessage in HoscySenderChannel.Reader.ReadAllAsync()) + _logger.LogDebug("Starting hoscy sender loop"); + await foreach (var oscMessage in _hoscySenderChannel.Reader.ReadAllAsync()) { try { - await HoscySenderClient.SendAsync(oscMessage); + await _hoscySenderClient.SendAsync(oscMessage); } catch (Exception e) { - Logger.Error(e, "HoscySenderClient send failed"); + _logger.LogError(e, "HoscySenderClient send failed"); } } } - public static Task? ReceiveGameMessage() + public Task? ReceiveGameMessage() { return _gameConnection?.ReceiveMessageAsync(); } diff --git a/ShockOsc/OscQueryLibrary/OscQueryServer.cs b/ShockOsc/OscQueryLibrary/OscQueryServer.cs index 0211a5a..d31dd6f 100644 --- a/ShockOsc/OscQueryLibrary/OscQueryServer.cs +++ b/ShockOsc/OscQueryLibrary/OscQueryServer.cs @@ -14,8 +14,10 @@ public class OscQueryServer : IDisposable { private static readonly ILogger Logger = Log.ForContext(typeof(OscQueryServer)); + private static readonly HttpClient Client = new(); + private readonly ushort _httpPort; - private readonly string _ipAddress; + private readonly IPAddress _ipAddress; public static string OscIpAddress; public static ushort OscReceivePort; public static ushort OscSendPort; @@ -33,7 +35,7 @@ public class OscQueryServer : IDisposable private static event Action, string>? ParameterUpdate; private static readonly Dictionary ParameterList = new(); - public OscQueryServer(string serviceName, string ipAddress, + public OscQueryServer(string serviceName, IPAddress ipAddress, Action? foundVrcClient = null, Action, string>? parameterUpdate = null) { @@ -79,10 +81,10 @@ private void AdvertiseOscQueryServer() { var httpProfile = new ServiceProfile(_serviceName, OscHttpServiceName, _httpPort, - new[] { IPAddress.Parse(_ipAddress) }); + new[] { _ipAddress }); var oscProfile = new ServiceProfile(_serviceName, OscUdpServiceName, OscReceivePort, - new[] { IPAddress.Parse(_ipAddress) }); + new[] { _ipAddress }); _serviceDiscovery.Advertise(httpProfile); _serviceDiscovery.Advertise(oscProfile); } @@ -154,10 +156,10 @@ private async Task FetchOscSendPortFromVrc(IPAddress ipAddress, int port) var url = $"http://{ipAddress}:{port}?HOST_INFO"; Logger.Debug("OSCQueryHttpClient: Fetching OSC send port from {Url}", url); var response = string.Empty; - var client = new HttpClient(); + try { - response = await client.GetStringAsync(url); + response = await Client.GetStringAsync(url); var rootNode = JsonSerializer.Deserialize(response); if (rootNode?.OSC_PORT == null) { @@ -188,10 +190,9 @@ private static async Task FetchJsonFromVrc(IPAddress ipAddress, int port) Logger.Debug("OSCQueryHttpClient: Fetching new parameters from {Url}", url); var response = string.Empty; var avatarId = string.Empty; - var client = new HttpClient(); try { - response = await client.GetStringAsync(url); + response = await Client.GetStringAsync(url); var rootNode = JsonSerializer.Deserialize(response); if (rootNode?.CONTENTS?.avatar?.CONTENTS?.parameters?.CONTENTS == null) { @@ -250,7 +251,7 @@ public static async Task GetParameters() private ushort FindAvailableTcpPort() { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.Bind(new IPEndPoint(IPAddress.Parse(_ipAddress), port: 0)); + socket.Bind(new IPEndPoint(_ipAddress, port: 0)); ushort port = 0; if (socket.LocalEndPoint != null) port = (ushort)((IPEndPoint)socket.LocalEndPoint).Port; @@ -260,7 +261,7 @@ private ushort FindAvailableTcpPort() private ushort FindAvailableUdpPort() { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.Bind(new IPEndPoint(IPAddress.Parse(_ipAddress), port: 0)); + socket.Bind(new IPEndPoint(_ipAddress, port: 0)); ushort port = 0; if (socket.LocalEndPoint != null) port = (ushort)((IPEndPoint)socket.LocalEndPoint).Port; @@ -288,7 +289,7 @@ private void SetupJsonObjects() { NAME = _serviceName, OSC_PORT = OscReceivePort, - OSC_IP = _ipAddress, + OSC_IP = _ipAddress.ToString(), OSC_TRANSPORT = "UDP", EXTENSIONS = new OscQueryModels.Extensions { diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 3cc6a46..bf309af 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -1,33 +1,38 @@ using System.Collections.Concurrent; using System.Globalization; using System.Net; -using System.Reflection; using LucHeart.CoreOSC; +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Live; using OpenShock.SDK.CSharp.Live.Models; using OpenShock.SDK.CSharp.Models; using OpenShock.ShockOsc.Backend; -using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.OscChangeTracker; using OpenShock.ShockOsc.OscQueryLibrary; +using OpenShock.ShockOsc.Ui.Utils; using OpenShock.ShockOsc.Utils; -using Serilog; -using Serilog.Events; using SmartFormat; #pragma warning disable CS4014 namespace OpenShock.ShockOsc; -public static class ShockOsc +public sealed class ShockOsc { - private static ILogger _logger = null!; + private readonly ILogger _logger; + private readonly OscClient _oscClient; + private readonly OpenShockApiLiveClient _liveClient; + private readonly UnderscoreConfig _underscoreConfig; + private static bool _oscServerActive; private static bool _isAfk; private static bool _isMuted; public static string AvatarId = string.Empty; private static readonly Random Random = new(); - public static readonly ConcurrentDictionary Shockers = new(); + public static readonly ConcurrentDictionary ProgramGroups = new(); + + public event Func? OnGroupsChanged; public static readonly string[] ShockerParams = { @@ -40,76 +45,47 @@ public static class ShockOsc "CooldownPercentage", "IShock" }; - - - + public static Dictionary ParamsInUse = new(); public static Dictionary AllAvatarParams = new(); public static Action? OnParamsChange; public static Action? OnConfigUpdate; - public static Action? SetAuthLoading; - public static AuthState CurrentAuthState = AuthState.NotAuthenticated; - - public static async Task StartMain() - { - - _logger = Log.ForContext(typeof(ShockOsc)); - - _logger.Information("Starting ShockOsc version {Version}", - Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "error"); - - _logger.Information("Creating OSC Query Server..."); - _ = new OscQueryServer( - "ShockOsc", // service name - "127.0.0.1", // ip address for udp and http server - FoundVrcClient, // optional callback on vrc discovery - OnAvatarChange // optional parameter list callback on vrc discovery - ); - - // listen for VRC on every network interface - if (ShockOscConfigManager.ConfigInstance.Osc.QuestSupport) - { - var host = await Dns.GetHostEntryAsync(Dns.GetHostName()); - foreach (var ip in host.AddressList) - { - if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) - continue; - - var ipAddress = ip.ToString(); - _ = new OscQueryServer( - "ShockOsc", // service name - ipAddress, // ip address for udp and http server - FoundVrcClient, // optional callback on vrc discovery - OnAvatarChange // parameter list callback on vrc discovery - ); - } - } - - await Task.Delay(Timeout.Infinite).ConfigureAwait(false); - } + private readonly ChangeTrackedOscParam _paramAnyActive; + private readonly ChangeTrackedOscParam _paramAnyCooldown; + private readonly ChangeTrackedOscParam _paramAnyCooldownPercentage; + private readonly ChangeTrackedOscParam _paramAnyIntensity; - public static void SetAuthSate(AuthState state) + public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi openShockApi, OpenShockApiLiveClient liveClient, UnderscoreConfig underscoreConfig) { - CurrentAuthState = state; - SetAuthLoading?.Invoke(state); + _logger = logger; + _oscClient = oscClient; + _liveClient = liveClient; + _underscoreConfig = underscoreConfig; + + _paramAnyActive = new ChangeTrackedOscParam("_Any", "_Active", false, _oscClient); + _paramAnyCooldown = new ChangeTrackedOscParam("_Any", "_Cooldown", false, _oscClient); + _paramAnyCooldownPercentage = new ChangeTrackedOscParam("_Any", "_CooldownPercentage", 0f, _oscClient); + _paramAnyIntensity = new ChangeTrackedOscParam("_Any", "_Intensity", 0f, _oscClient); } - + + public void RaiseOnGroupsChanged() => OnGroupsChanged.Raise(); + private static void OnParamChange(bool shockOscParam) { OnParamsChange?.Invoke(shockOscParam); } - private static void FoundVrcClient() + public void FoundVrcClient() { - _logger.Information("Found VRC client"); + _logger.LogInformation("Found VRC client"); // stop tasks _oscServerActive = false; Task.Delay(1000).Wait(); // wait for tasks to stop - OscClient.CreateGameConnection(IPAddress.Parse(OscQueryServer.OscIpAddress), OscQueryServer.OscReceivePort, OscQueryServer.OscSendPort); - _logger.Information("Connecting UDP Clients..."); + _oscClient.CreateGameConnection(IPAddress.Parse(OscQueryServer.OscIpAddress), OscQueryServer.OscReceivePort, OscQueryServer.OscSendPort); + _logger.LogInformation("Connecting UDP Clients..."); // Start tasks _oscServerActive = true; @@ -117,37 +93,16 @@ private static void FoundVrcClient() OsTask.Run(SenderLoopAsync); OsTask.Run(CheckLoop); - _logger.Information("Ready"); - OsTask.Run(UnderscoreConfig.SendUpdateForAll); + _logger.LogInformation("Ready"); + OsTask.Run(_underscoreConfig.SendUpdateForAll); } - public static void RefreshShockers() - { - Shockers.Clear(); - Shockers.TryAdd("_All", new Shocker(Guid.Empty, "_All")); - // foreach (var (shockerName, shocker) in Config.ConfigInstance.OpenShock.Shockers) - // { - // if(!shocker.Enabled) continue; - // - // if (string.IsNullOrEmpty(shocker.NickName)) - // Shockers.TryAdd(shockerName, new Shocker(shocker.Id, shockerName)); - // else - // Shockers.TryAdd(shocker.NickName, new Shocker(shocker.Id, shocker.NickName)); - // } - } - - public static void SaveShockers() - { - RefreshShockers(); - ShockOscConfigManager.Save(); - } - - private static void OnAvatarChange(Dictionary? parameters, string avatarId) + public void OnAvatarChange(Dictionary? parameters, string avatarId) { AvatarId = avatarId; try { - foreach (var obj in Shockers) + foreach (var obj in ProgramGroups) { obj.Value.Reset(); } @@ -156,7 +111,7 @@ private static void OnAvatarChange(Dictionary? parameters, stri if (parameters == null) { - _logger.Error("Failed to receive avatar parameters"); + _logger.LogError("Failed to receive avatar parameters"); return; } @@ -187,23 +142,23 @@ private static void OnAvatarChange(Dictionary? parameters, stri ParamsInUse.TryAdd(paramName, parameters[param]); } - if (!Shockers.ContainsKey(shockerName) && !shockerName.StartsWith("_")) + if (!ProgramGroups.ContainsKey(shockerName) && !shockerName.StartsWith("_")) { - _logger.Warning("Unknown shocker on avatar {Shocker}", shockerName); - _logger.Debug("Param: {Param}", param); + _logger.LogWarning("Unknown shocker on avatar {Shocker}", shockerName); + _logger.LogDebug("Param: {Param}", param); } } - _logger.Information("Loaded avatar config with {ParamCount} parameters", parameterCount); + _logger.LogInformation("Loaded avatar config with {ParamCount} parameters", parameterCount); } catch (Exception e) { - _logger.Error(e, "Error on avatar change logic"); + _logger.LogError(e, "Error on avatar change logic"); } OnParamChange(true); } - private static async Task ReceiverLoopAsync() + private async Task ReceiverLoopAsync() { while (_oscServerActive) { @@ -213,27 +168,27 @@ private static async Task ReceiverLoopAsync() } catch (Exception e) { - _logger.Error(e, "Error in receiver loop"); + _logger.LogError(e, "Error in receiver loop"); } } // ReSharper disable once FunctionNeverReturns } - private static async Task ReceiveLogic() + private async Task ReceiveLogic() { OscMessage received; try { - received = await OscClient.ReceiveGameMessage()!; + received = await _oscClient.ReceiveGameMessage()!; } catch (Exception e) { - _logger.Verbose(e, "Error receiving message"); + _logger.LogTrace(e, "Error receiving message"); return; } var addr = received.Address; - _logger.Verbose("Received message: {Addr}", addr); + _logger.LogTrace("Received message: {Addr}", addr); if (addr.StartsWith("/avatar/parameters/")) { @@ -251,17 +206,17 @@ private static async Task ReceiveLogic() { case "/avatar/change": var avatarId = received.Arguments.ElementAtOrDefault(0); - _logger.Debug("Avatar changed: {AvatarId}", avatarId); + _logger.LogDebug("Avatar changed: {AvatarId}", avatarId); OsTask.Run(OscQueryServer.GetParameters); - OsTask.Run(UnderscoreConfig.SendUpdateForAll); + OsTask.Run(_underscoreConfig.SendUpdateForAll); return; case "/avatar/parameters/AFK": _isAfk = received.Arguments.ElementAtOrDefault(0) is true; - _logger.Debug("Afk: {State}", _isAfk); + _logger.LogDebug("Afk: {State}", _isAfk); return; case "/avatar/parameters/MuteSelf": _isMuted = received.Arguments.ElementAtOrDefault(0) is true; - _logger.Debug("Muted: {State}", _isMuted); + _logger.LogDebug("Muted: {State}", _isMuted); return; } @@ -273,7 +228,7 @@ private static async Task ReceiveLogic() // Check if _Config if (pos.StartsWith("_Config/")) { - UnderscoreConfig.HandleCommand(pos, received.Arguments); + _underscoreConfig.HandleCommand(pos, received.Arguments); return; } @@ -296,15 +251,15 @@ private static async Task ReceiveLogic() if (!ShockerParams.Contains(action)) return; - if (!Shockers.ContainsKey(shockerName)) + if (!ProgramGroups.ContainsKey(shockerName)) { if (shockerName == "_Any") return; - _logger.Warning("Unknown shocker {Shocker}", shockerName); - _logger.Debug("Param: {Param}", pos); + _logger.LogWarning("Unknown shocker {Shocker}", shockerName); + _logger.LogDebug("Param: {Param}", pos); return; } - var shocker = Shockers[shockerName]; + var shocker = ProgramGroups[shockerName]; var value = received.Arguments.ElementAtOrDefault(0); switch (action) @@ -312,7 +267,7 @@ private static async Task ReceiveLogic() case "IShock": // TODO: check Cooldowns if (value is not true) return; - if (UnderscoreConfig.KillSwitch) + if (_underscoreConfig.KillSwitch) { shocker.TriggerMethod = TriggerMethod.None; await LogIgnoredKillSwitchActive(); @@ -368,25 +323,25 @@ private static async Task ReceiveLogic() else shocker.TriggerMethod = TriggerMethod.None; } - private static ValueTask LogIgnoredKillSwitchActive() + private ValueTask LogIgnoredKillSwitchActive() { - _logger.Information("Ignoring shock, kill switch is active"); + _logger.LogInformation("Ignoring shock, kill switch is active"); if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive)) return ValueTask.CompletedTask; - return OscClient.SendChatboxMessage( + return _oscClient.SendChatboxMessage( $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive}"); } - private static ValueTask LogIgnoredAfk() + private ValueTask LogIgnoredAfk() { - _logger.Information("Ignoring shock, user is AFK"); + _logger.LogInformation("Ignoring shock, user is AFK"); if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk)) return ValueTask.CompletedTask; - return OscClient.SendChatboxMessage( + return _oscClient.SendChatboxMessage( $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk}"); } - private static async Task SenderLoopAsync() + private async Task SenderLoopAsync() { while (_oscServerActive) { @@ -394,35 +349,30 @@ private static async Task SenderLoopAsync() await Task.Delay(300); } } - - private static readonly ChangeTrackedOscParam ParamAnyActive = new("_Any", "_Active", false); - private static readonly ChangeTrackedOscParam ParamAnyCooldown = new("_Any", "_Cooldown", false); - private static readonly ChangeTrackedOscParam ParamAnyCooldownPercentage = new("_Any", "_CooldownPercentage", 0f); - private static readonly ChangeTrackedOscParam ParamAnyIntensity = new("_Any", "_Intensity", 0f); - - private static async Task InstantShock(Shocker shocker, uint duration, byte intensity) + + private async Task InstantShock(ProgramGroup programGroup, uint duration, byte intensity) { - shocker.LastExecuted = DateTime.UtcNow; - shocker.LastDuration = duration; + programGroup.LastExecuted = DateTime.UtcNow; + programGroup.LastDuration = duration; var intensityPercentage = Math.Round(GetFloatScaled(intensity) * 100f); - shocker.LastIntensity = intensity; + programGroup.LastIntensity = intensity; ForceUnmute(); SendParams(); - shocker.TriggerMethod = TriggerMethod.None; + programGroup.TriggerMethod = TriggerMethod.None; var inSeconds = MathF.Round(duration / 1000f, 1).ToString(CultureInfo.InvariantCulture); - _logger.Information( + _logger.LogInformation( "Sending shock to {Shocker} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s", - shocker.Name, intensity, intensityPercentage, inSeconds); + programGroup.Name, intensity, intensityPercentage, inSeconds); - await ControlShocker(shocker.Id, duration, intensity, ControlType.Shock); + await ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock); if (!ShockOscConfigManager.ConfigInstance.Osc.Chatbox) return; // Chatbox message local var dat = new { - ShockerName = shocker.Name, + ShockerName = programGroup.Name, Intensity = intensity, IntensityPercentage = intensityPercentage, Duration = duration, @@ -430,7 +380,7 @@ private static async Task InstantShock(Shocker shocker, uint duration, byte inte }; var template = ShockOscConfigManager.ConfigInstance.Chatbox.Types[ControlType.Shock]; var msg = $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{Smart.Format(template.Local, dat)}"; - await OscClient.SendChatboxMessage(msg); + await _oscClient.SendChatboxMessage(msg); } /// @@ -441,7 +391,7 @@ private static async Task InstantShock(Shocker shocker, uint duration, byte inte private static float GetFloatScaled(byte intensity) => ClampFloat((float)intensity / ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max); - private static async Task SendParams() + private async Task SendParams() { // TODO: maybe force resend on avatar change var anyActive = false; @@ -449,7 +399,7 @@ private static async Task SendParams() var anyCooldownPercentage = 0f; var anyIntensity = 0f; - foreach (var shocker in Shockers.Values) + foreach (var shocker in ProgramGroups.Values) { var isActive = shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; var isActiveOrOnCooldown = @@ -478,13 +428,13 @@ private static async Task SendParams() anyIntensity = Math.Max(anyIntensity, GetFloatScaled(shocker.LastIntensity)); } - await ParamAnyActive.SetValue(anyActive); - await ParamAnyCooldown.SetValue(anyCooldown); - await ParamAnyCooldownPercentage.SetValue(anyCooldownPercentage); - await ParamAnyIntensity.SetValue(anyIntensity); + await _paramAnyActive.SetValue(anyActive); + await _paramAnyCooldown.SetValue(anyCooldown); + await _paramAnyCooldownPercentage.SetValue(anyCooldownPercentage); + await _paramAnyIntensity.SetValue(anyIntensity); } - private static async Task CheckLoop() + private async Task CheckLoop() { while (_oscServerActive) { @@ -494,14 +444,14 @@ private static async Task CheckLoop() } catch (Exception e) { - _logger.Error(e, "Error in check loop"); + _logger.LogError(e, "Error in check loop"); } await Task.Delay(20); } } - private static byte GetIntensity() + private byte GetIntensity() { var config = ShockOscConfigManager.ConfigInstance.Behaviour; @@ -511,76 +461,76 @@ private static byte GetIntensity() return (byte)intensityValue; } - private static async Task CheckLogic() + private async Task CheckLogic() { var config = ShockOscConfigManager.ConfigInstance.Behaviour; - foreach (var (pos, shocker) in Shockers) + foreach (var (pos, programGroup) in ProgramGroups) { var isActiveOrOnCooldown = - shocker.LastExecuted.AddMilliseconds(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime) - .AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; + programGroup.LastExecuted.AddMilliseconds(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime) + .AddMilliseconds(programGroup.LastDuration) > DateTime.UtcNow; - if (shocker.TriggerMethod == TriggerMethod.None && + if (programGroup.TriggerMethod == TriggerMethod.None && ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld != ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None && !isActiveOrOnCooldown && - shocker.IsGrabbed && - shocker.LastVibration < DateTime.UtcNow.Subtract(TimeSpan.FromMilliseconds(300))) + programGroup.IsGrabbed && + programGroup.LastVibration < DateTime.UtcNow.Subtract(TimeSpan.FromMilliseconds(300))) { - var vibrationIntensity = shocker.LastStretchValue * 100f; + var vibrationIntensity = programGroup.LastStretchValue * 100f; if (vibrationIntensity < 1) vibrationIntensity = 1; - shocker.LastVibration = DateTime.UtcNow; + programGroup.LastVibration = DateTime.UtcNow; - _logger.Debug("Vibrating {Shocker} at {Intensity}", pos, vibrationIntensity); - await ControlShocker(shocker.Id, 1000, (byte)vibrationIntensity, + _logger.LogDebug("Vibrating {Shocker} at {Intensity}", pos, vibrationIntensity); + await ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld == ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.Shock ? ControlType.Shock : ControlType.Vibrate); } - if (shocker.TriggerMethod == TriggerMethod.None) + if (programGroup.TriggerMethod == TriggerMethod.None) continue; - if (shocker.TriggerMethod == TriggerMethod.Manual && - shocker.LastActive.AddMilliseconds(config.HoldTime) > DateTime.UtcNow) + if (programGroup.TriggerMethod == TriggerMethod.Manual && + programGroup.LastActive.AddMilliseconds(config.HoldTime) > DateTime.UtcNow) continue; if (isActiveOrOnCooldown) { - shocker.TriggerMethod = TriggerMethod.None; - _logger.Information("Ignoring shock {Shocker} is on cooldown", pos); + programGroup.TriggerMethod = TriggerMethod.None; + _logger.LogInformation("Ignoring shock, group {Shocker} is on cooldown", pos); continue; } - if (UnderscoreConfig.KillSwitch) + if (_underscoreConfig.KillSwitch) { - shocker.TriggerMethod = TriggerMethod.None; + programGroup.TriggerMethod = TriggerMethod.None; await LogIgnoredKillSwitchActive(); continue; } if (_isAfk && config.DisableWhileAfk) { - shocker.TriggerMethod = TriggerMethod.None; + programGroup.TriggerMethod = TriggerMethod.None; await LogIgnoredAfk(); continue; } byte intensity; - if (shocker.TriggerMethod == TriggerMethod.PhysBoneRelease) + if (programGroup.TriggerMethod == TriggerMethod.PhysBoneRelease) { intensity = (byte)LerpFloat(config.IntensityRange.Min, config.IntensityRange.Max, - shocker.LastStretchValue); - shocker.LastStretchValue = 0; + programGroup.LastStretchValue); + programGroup.LastStretchValue = 0; } else intensity = GetIntensity(); - InstantShock(shocker, GetDuration(), intensity); + InstantShock(programGroup, GetDuration(), intensity); } } - private static uint GetDuration() + private uint GetDuration() { var config = ShockOscConfigManager.ConfigInstance.Behaviour; @@ -590,43 +540,38 @@ private static uint GetDuration() (int)(rdr.Max / config.RandomDurationStep)) * config.RandomDurationStep); } - private static Task ControlShocker(Guid shockerId, uint duration, byte intensity, ControlType type) + private async Task ControlGroup(Guid groupId, uint duration, byte intensity, ControlType type) { - // if (shockerId == Guid.Empty) - // return BackendLiveApiManager.Control(Shockers.Where(x => x.Value.Id != Guid.Empty).Select(x => new Control - // { - // Id = x.Value.Id, - // Intensity = intensity, - // Duration = duration, - // Type = type - // }).ToArray()); - // - // return BackendLiveApiManager.Control(new Control - // { - // Id = shockerId, - // Intensity = intensity, - // Duration = duration, - // Type = type - // }); - return Task.CompletedTask; + if (!ShockOscConfigManager.ConfigInstance.Groups.TryGetValue(groupId, out var group)) return false; + + var controlCommands = group.Shockers.Select(x => new Control + { + Id = x, + Duration = duration, + Intensity = intensity, + Type = type + }); + + await _liveClient.Control(controlCommands); + return true; } - public static async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) + public async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) { // if (sender.ConnectionId == BackendLiveApiManager.ConnectionId) // { - // _logger.Debug("Ignoring remote command log cause it was the local connection"); + // _logger.LogDebug("Ignoring remote command log cause it was the local connection"); // return; // } var inSeconds = ((float)log.Duration / 1000).ToString(CultureInfo.InvariantCulture); if (sender.CustomName == null) - _logger.Information( + _logger.LogInformation( "Received remote {Type} for \"{ShockerName}\" at {Intensity}%:{Duration}s by {Sender}", log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.Name); else - _logger.Information( + _logger.LogInformation( "Received remote {Type} for \"{ShockerName}\" at {Intensity}%:{Duration}s by {SenderCustomName} [{Sender}]", log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.CustomName, sender.Name); @@ -646,10 +591,10 @@ public static async Task RemoteActivateShocker(ControlLogSender sender, ControlL var msg = $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{Smart.Format(sender.CustomName == null ? template.Remote : template.RemoteWithCustomName, dat)}"; - await OscClient.SendChatboxMessage(msg); + await _oscClient.SendChatboxMessage(msg); } - var shocker = Shockers.Values.Where(s => s.Id == log.Shocker.Id).ToArray(); + var shocker = ProgramGroups.Values.Where(s => s.Id == log.Shocker.Id).ToArray(); if (shocker.Length <= 0) return; @@ -678,7 +623,7 @@ public static async Task RemoteActivateShocker(ControlLogSender sender, ControlL case ControlType.Sound: break; default: - _logger.Error("ControlType was out of range. Value was: {Type}", log.Type); + _logger.LogError("ControlType was out of range. Value was: {Type}", log.Type); break; } @@ -690,21 +635,21 @@ public static async Task RemoteActivateShocker(ControlLogSender sender, ControlL } } - private static async Task ForceUnmute() + private async Task ForceUnmute() { if (!ShockOscConfigManager.ConfigInstance.Behaviour.ForceUnmute || !_isMuted) return; - _logger.Debug("Force unmuting..."); - await OscClient.SendGameMessage("/input/Voice", false); + _logger.LogDebug("Force unmuting..."); + await _oscClient.SendGameMessage("/input/Voice", false); await Task.Delay(50); - await OscClient.SendGameMessage("/input/Voice", true); + await _oscClient.SendGameMessage("/input/Voice", true); await Task.Delay(50); - await OscClient.SendGameMessage("/input/Voice", false); + await _oscClient.SendGameMessage("/input/Voice", false); } - private static Task CancelAction(Shocker shocker) + private Task CancelAction(ProgramGroup programGroup) { - _logger.Debug("Cancelling action"); - return ControlShocker(shocker.Id, 0, 0, ControlType.Stop); + _logger.LogDebug("Cancelling action"); + return ControlGroup(programGroup.Id, 0, 0, ControlType.Stop); } private static float LerpFloat(float min, float max, float t) => min + (max - min) * t; diff --git a/ShockOsc/ShockOscConfigManager.cs b/ShockOsc/ShockOscConfigManager.cs index a5f7ad4..53fd354 100644 --- a/ShockOsc/ShockOscConfigManager.cs +++ b/ShockOsc/ShockOscConfigManager.cs @@ -17,7 +17,6 @@ public static class ShockOscConfigManager static ShockOscConfigManager() { TryLoad(); - ShockOsc.RefreshShockers(); } private static void TryLoad() diff --git a/ShockOsc/Ui/App.xaml.cs b/ShockOsc/Ui/App.xaml.cs index 51b0229..72de8f2 100644 --- a/ShockOsc/Ui/App.xaml.cs +++ b/ShockOsc/Ui/App.xaml.cs @@ -4,7 +4,6 @@ public partial class App : Application { public App() { - _ = OpenShock.ShockOsc.ShockOsc.StartMain(); InitializeComponent(); MainPage = new MainPage(); } diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor index 34c53c7..e0505c8 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -1,6 +1,7 @@ @inject ShockOscConfigManager.ShockOscConfig Config @using OpenShock.SDK.CSharp.Models @implements IDisposable +@inject UnderscoreConfig underscoreConfig @@ -103,7 +104,7 @@ Other Options - + @@ -150,7 +151,7 @@ } } - private Task OnSettingsValueSave() + private async Task OnSettingsValueSave() { _lastSettingsSave = DateTime.Now; Config.Behaviour.RandomIntensity = intensity == "Random Intensity"; @@ -158,8 +159,7 @@ ValidateSettings(); ShockOscConfigManager.Save(); - UnderscoreConfig.SendUpdateForAll(); - return Task.CompletedTask; + await underscoreConfig.SendUpdateForAll(); } private void ValidateSettings() diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index ab444b7..570fe4a 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -1,6 +1,7 @@ @using System.Text.RegularExpressions @using OpenShock.ShockOsc.Backend @inject OpenShockApi OpenShockApi +@inject ShockOsc ShockOsc @code { @@ -15,6 +16,7 @@ InvokeAsync(StateHasChanged); ShockOscConfigManager.Save(); + ShockOsc.RaiseOnGroupsChanged(); } public void DeleteGroup() diff --git a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor b/ShockOsc/Ui/Components/Tabs/ShockersTab.razor index 089f03c..de141af 100644 --- a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ShockersTab.razor @@ -21,9 +21,8 @@ @code { - private Task OnShockerConfigUpdate() + private void OnShockerConfigUpdate() { - ShockOsc.RefreshShockers(); - return Task.CompletedTask; + ShockOscConfigManager.Save(); } } \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index 2785d48..5527776 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -49,6 +49,8 @@ public async Task Login() { ShockOscConfigManager.Save(); + ApiClient.SetupApiClient(); + await LiveApiManager.SetupLiveClient(); await ProceedAuthenticated(); } @@ -58,6 +60,7 @@ await LiveApiManager.SetupLiveClient(); await LiveApiClient.StartAsync(); + await ApiClient.RefreshShockers(); NavigationManager.NavigateTo("main"); } diff --git a/ShockOsc/Ui/Utils/AsynchronousEventExtensions.cs b/ShockOsc/Ui/Utils/AsynchronousEventExtensions.cs new file mode 100644 index 0000000..8479ba7 --- /dev/null +++ b/ShockOsc/Ui/Utils/AsynchronousEventExtensions.cs @@ -0,0 +1,17 @@ +namespace OpenShock.ShockOsc.Ui.Utils; + +public static class AsynchronousEventExtensions +{ + public static Task Raise(this Func? handlers) + { + if (handlers != null) + { + return Task.WhenAll(handlers.GetInvocationList() + .OfType>() + .Select(h => h())); + } + + return Task.CompletedTask; + } + +} \ No newline at end of file diff --git a/ShockOsc/UnderscoreConfig.cs b/ShockOsc/UnderscoreConfig.cs index 6be292a..e82f7d0 100644 --- a/ShockOsc/UnderscoreConfig.cs +++ b/ShockOsc/UnderscoreConfig.cs @@ -1,21 +1,29 @@ -using Serilog; +using Microsoft.Extensions.Logging; +using Serilog; namespace OpenShock.ShockOsc; -public static class UnderscoreConfig +public sealed class UnderscoreConfig { - private static readonly ILogger Logger = Log.ForContext(typeof(UnderscoreConfig)); + private readonly ILogger _logger; + private readonly OscClient _oscClient; + + public UnderscoreConfig(ILogger logger, OscClient oscClient) + { + _logger = logger; + _oscClient = oscClient; + } - public static bool KillSwitch { get; set; } = false; + public bool KillSwitch { get; set; } = false; - public static void HandleCommand(string parameterName, object?[] arguments) + public void HandleCommand(string parameterName, object?[] arguments) { var settingName = parameterName[8..]; var settingPath = settingName.Split('/'); if (settingPath.Length > 2) { - Logger.Warning("Invalid setting path: {SettingPath}", settingPath); + _logger.LogWarning("Invalid setting path: {SettingPath}", settingPath); return; } @@ -23,14 +31,14 @@ public static void HandleCommand(string parameterName, object?[] arguments) { var shockerName = settingPath[0]; var action = settingPath[1]; - if (!ShockOsc.Shockers.ContainsKey(shockerName) && shockerName != "_All") + if (!ShockOsc.ProgramGroups.ContainsKey(shockerName) && shockerName != "_All") { - Logger.Warning("Unknown shocker {Shocker}", shockerName); - Logger.Debug("Param: {Param}", action); + _logger.LogWarning("Unknown shocker {Shocker}", shockerName); + _logger.LogDebug("Param: {Param}", action); return; } - var shocker = ShockOsc.Shockers[shockerName]; + var shocker = ShockOsc.ProgramGroups[shockerName]; var value = arguments.ElementAtOrDefault(0); // TODO: support groups @@ -118,13 +126,13 @@ public static void HandleCommand(string parameterName, object?[] arguments) if (KillSwitch == stateBool) return; KillSwitch = stateBool; - Logger.Information("Paused state set to: {KillSwitch}", KillSwitch); + _logger.LogInformation("Paused state set to: {KillSwitch}", KillSwitch); } break; } } - private static void ValidateSettings() + private void ValidateSettings() { if (ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min > ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max) { @@ -132,13 +140,13 @@ private static void ValidateSettings() } } - public static async Task SendUpdateForAll() + public async Task SendUpdateForAll() { - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/Paused", KillSwitch); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min / 100f)); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max / 100f)); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.FixedDuration / 10000f)); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime / 100000f)); - await OscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.HoldTime / 1000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/Paused", KillSwitch); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min / 100f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max / 100f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.FixedDuration / 10000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime / 100000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.HoldTime / 1000f)); } } \ No newline at end of file From 7f8d46abf5377dfa081e3f92ccdf6a557c09e854 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 7 Apr 2024 03:01:38 +0200 Subject: [PATCH 22/95] yes --- ShockOsc/Backend/BackendLiveApiManager.cs | 1 - ShockOsc/MauiProgram.cs | 7 +- ShockOsc/ShockOsc.cs | 184 +++++++++++++------- ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 15 +- ShockOsc/UnderscoreConfig.cs | 8 +- 5 files changed, 139 insertions(+), 76 deletions(-) diff --git a/ShockOsc/Backend/BackendLiveApiManager.cs b/ShockOsc/Backend/BackendLiveApiManager.cs index 73264c7..00df616 100644 --- a/ShockOsc/Backend/BackendLiveApiManager.cs +++ b/ShockOsc/Backend/BackendLiveApiManager.cs @@ -31,7 +31,6 @@ await _openShockApiLiveClient.Setup(new ApiLiveClientOptions() builder.AddSerilog(); } }); - //await _openShockApiLiveClient.StartAsync(); } } \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index d3a55b4..8d4c33d 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -79,6 +79,11 @@ public static MauiApp CreateMauiApp() builder.Services.AddBlazorWebViewDeveloperTools(); #endif - return builder.Build();; + var app = builder.Build(); + + // Warmup + app.Services.GetRequiredService(); + + return app; } } \ No newline at end of file diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index bf309af..2e85d3c 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -24,15 +24,16 @@ public sealed class ShockOsc private readonly OscClient _oscClient; private readonly OpenShockApiLiveClient _liveClient; private readonly UnderscoreConfig _underscoreConfig; + private readonly ShockOscConfigManager.ShockOscConfig _config; private static bool _oscServerActive; private static bool _isAfk; private static bool _isMuted; public static string AvatarId = string.Empty; private static readonly Random Random = new(); - public static readonly ConcurrentDictionary ProgramGroups = new(); - - public event Func? OnGroupsChanged; + public static readonly ConcurrentDictionary ProgramGroups = new(); + + public event Func? OnGroupsChanged; public static readonly string[] ShockerParams = { @@ -45,33 +46,57 @@ public sealed class ShockOsc "CooldownPercentage", "IShock" }; - + public static Dictionary ParamsInUse = new(); public static Dictionary AllAvatarParams = new(); public static Action? OnParamsChange; public static Action? OnConfigUpdate; - + private readonly ChangeTrackedOscParam _paramAnyActive; private readonly ChangeTrackedOscParam _paramAnyCooldown; private readonly ChangeTrackedOscParam _paramAnyCooldownPercentage; private readonly ChangeTrackedOscParam _paramAnyIntensity; - public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi openShockApi, OpenShockApiLiveClient liveClient, UnderscoreConfig underscoreConfig) + private string connectionId = string.Empty; + + public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi openShockApi, + OpenShockApiLiveClient liveClient, UnderscoreConfig underscoreConfig, + ShockOscConfigManager.ShockOscConfig config) { _logger = logger; _oscClient = oscClient; _liveClient = liveClient; _underscoreConfig = underscoreConfig; + _config = config; _paramAnyActive = new ChangeTrackedOscParam("_Any", "_Active", false, _oscClient); _paramAnyCooldown = new ChangeTrackedOscParam("_Any", "_Cooldown", false, _oscClient); _paramAnyCooldownPercentage = new ChangeTrackedOscParam("_Any", "_CooldownPercentage", 0f, _oscClient); _paramAnyIntensity = new ChangeTrackedOscParam("_Any", "_Intensity", 0f, _oscClient); + + liveClient.OnWelcome += s => + { + connectionId = s; + return Task.CompletedTask; + }; + + liveClient.OnLog += RemoteActivateShockers; + + OnGroupsChanged += SetupGroups; + + SetupGroups().Wait(); + } + + private async Task SetupGroups() + { + ProgramGroups.Clear(); + ProgramGroups[Guid.Empty] = new ProgramGroup(Guid.Empty, "_All", _oscClient); + foreach (var (id, group) in _config.Groups) ProgramGroups[id] = new ProgramGroup(id, group.Name, _oscClient); } - - public void RaiseOnGroupsChanged() => OnGroupsChanged.Raise(); - + + public Task RaiseOnGroupsChanged() => OnGroupsChanged.Raise(); + private static void OnParamChange(bool shockOscParam) { OnParamsChange?.Invoke(shockOscParam); @@ -84,7 +109,8 @@ public void FoundVrcClient() _oscServerActive = false; Task.Delay(1000).Wait(); // wait for tasks to stop - _oscClient.CreateGameConnection(IPAddress.Parse(OscQueryServer.OscIpAddress), OscQueryServer.OscReceivePort, OscQueryServer.OscSendPort); + _oscClient.CreateGameConnection(IPAddress.Parse(OscQueryServer.OscIpAddress), OscQueryServer.OscReceivePort, + OscQueryServer.OscSendPort); _logger.LogInformation("Connecting UDP Clients..."); // Start tasks @@ -96,7 +122,7 @@ public void FoundVrcClient() _logger.LogInformation("Ready"); OsTask.Run(_underscoreConfig.SendUpdateForAll); } - + public void OnAvatarChange(Dictionary? parameters, string avatarId) { AvatarId = avatarId; @@ -142,7 +168,9 @@ public void OnAvatarChange(Dictionary? parameters, string avata ParamsInUse.TryAdd(paramName, parameters[param]); } - if (!ProgramGroups.ContainsKey(shockerName) && !shockerName.StartsWith("_")) + if (!ProgramGroups.Any(x => + x.Value.Name.Equals(shockerName, StringComparison.InvariantCultureIgnoreCase)) && + !shockerName.StartsWith('_')) { _logger.LogWarning("Unknown shocker on avatar {Shocker}", shockerName); _logger.LogDebug("Param: {Param}", param); @@ -155,6 +183,7 @@ public void OnAvatarChange(Dictionary? parameters, string avata { _logger.LogError(e, "Error on avatar change logic"); } + OnParamChange(true); } @@ -234,10 +263,10 @@ private async Task ReceiveLogic() var lastUnderscoreIndex = pos.LastIndexOf('_') + 1; var action = string.Empty; - var shockerName = pos; + var groupName = pos; if (lastUnderscoreIndex > 1) { - shockerName = pos[..(lastUnderscoreIndex - 1)]; + groupName = pos[..(lastUnderscoreIndex - 1)]; action = pos.Substring(lastUnderscoreIndex, pos.Length - lastUnderscoreIndex); } @@ -251,15 +280,16 @@ private async Task ReceiveLogic() if (!ShockerParams.Contains(action)) return; - if (!ProgramGroups.ContainsKey(shockerName)) + if (!ProgramGroups.Any(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase))) { - if (shockerName == "_Any") return; - _logger.LogWarning("Unknown shocker {Shocker}", shockerName); + if (groupName == "_Any") return; + _logger.LogWarning("Unknown group {GroupName}", groupName); _logger.LogDebug("Param: {Param}", pos); return; } - var shocker = ProgramGroups[shockerName]; + var programGroup = ProgramGroups + .First(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)).Value; var value = received.Arguments.ElementAtOrDefault(0); switch (action) @@ -269,43 +299,43 @@ private async Task ReceiveLogic() if (value is not true) return; if (_underscoreConfig.KillSwitch) { - shocker.TriggerMethod = TriggerMethod.None; + programGroup.TriggerMethod = TriggerMethod.None; await LogIgnoredKillSwitchActive(); return; } if (_isAfk && ShockOscConfigManager.ConfigInstance.Behaviour.DisableWhileAfk) { - shocker.TriggerMethod = TriggerMethod.None; + programGroup.TriggerMethod = TriggerMethod.None; await LogIgnoredAfk(); return; } - OsTask.Run(() => InstantShock(shocker, GetDuration(), GetIntensity())); + OsTask.Run(() => InstantShock(programGroup, GetDuration(), GetIntensity())); return; case "Stretch": if (value is float stretch) - shocker.LastStretchValue = stretch; + programGroup.LastStretchValue = stretch; return; case "IsGrabbed": var isGrabbed = value is true; - if (shocker.IsGrabbed && !isGrabbed) + if (programGroup.IsGrabbed && !isGrabbed) { // on physbone release - if (shocker.LastStretchValue != 0) + if (programGroup.LastStretchValue != 0) { - shocker.TriggerMethod = TriggerMethod.PhysBoneRelease; - shocker.LastActive = DateTime.UtcNow; + programGroup.TriggerMethod = TriggerMethod.PhysBoneRelease; + programGroup.LastActive = DateTime.UtcNow; } else if (ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld != - ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None) + ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None) { - await CancelAction(shocker); + await CancelAction(programGroup); } } - shocker.IsGrabbed = isGrabbed; + programGroup.IsGrabbed = isGrabbed; return; // Normal shocker actions case "": @@ -317,16 +347,17 @@ private async Task ReceiveLogic() if (value is true) { - shocker.TriggerMethod = TriggerMethod.Manual; - shocker.LastActive = DateTime.UtcNow; + programGroup.TriggerMethod = TriggerMethod.Manual; + programGroup.LastActive = DateTime.UtcNow; } - else shocker.TriggerMethod = TriggerMethod.None; + else programGroup.TriggerMethod = TriggerMethod.None; } private ValueTask LogIgnoredKillSwitchActive() { _logger.LogInformation("Ignoring shock, kill switch is active"); - if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive)) return ValueTask.CompletedTask; + if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive)) + return ValueTask.CompletedTask; return _oscClient.SendChatboxMessage( $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive}"); @@ -335,7 +366,8 @@ private ValueTask LogIgnoredKillSwitchActive() private ValueTask LogIgnoredAfk() { _logger.LogInformation("Ignoring shock, user is AFK"); - if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk)) return ValueTask.CompletedTask; + if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk)) + return ValueTask.CompletedTask; return _oscClient.SendChatboxMessage( $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk}"); @@ -349,7 +381,7 @@ private async Task SenderLoopAsync() await Task.Delay(300); } } - + private async Task InstantShock(ProgramGroup programGroup, uint duration, byte intensity) { programGroup.LastExecuted = DateTime.UtcNow; @@ -363,7 +395,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i programGroup.TriggerMethod = TriggerMethod.None; var inSeconds = MathF.Round(duration / 1000f, 1).ToString(CultureInfo.InvariantCulture); _logger.LogInformation( - "Sending shock to {Shocker} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s", + "Sending shock to {GroupName} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s", programGroup.Name, intensity, intensityPercentage, inSeconds); await ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock); @@ -415,7 +447,8 @@ private async Task SendParams() cooldownPercentage = ClampFloat(1 - (float)(DateTime.UtcNow - shocker.LastExecuted.AddMilliseconds(shocker.LastDuration)) - .TotalMilliseconds / ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime); + .TotalMilliseconds / + ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime); await shocker.ParamActive.SetValue(isActive); await shocker.ParamCooldown.SetValue(onCoolDown); @@ -471,7 +504,8 @@ private async Task CheckLogic() .AddMilliseconds(programGroup.LastDuration) > DateTime.UtcNow; if (programGroup.TriggerMethod == TriggerMethod.None && - ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld != ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None && + ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld != + ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None && !isActiveOrOnCooldown && programGroup.IsGrabbed && programGroup.LastVibration < DateTime.UtcNow.Subtract(TimeSpan.FromMilliseconds(300))) @@ -483,7 +517,8 @@ private async Task CheckLogic() _logger.LogDebug("Vibrating {Shocker} at {Intensity}", pos, vibrationIntensity); await ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, - ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld == ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.Shock + ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld == + ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.Shock ? ControlType.Shock : ControlType.Vibrate); } @@ -542,6 +577,22 @@ private uint GetDuration() private async Task ControlGroup(Guid groupId, uint duration, byte intensity, ControlType type) { + if(groupId == Guid.Empty) + { + var controlCommandsAll = _config.OpenShock.Shockers + .Where(x => x.Value.Enabled) + .Select(x => new Control + { + Id = x.Key, + Duration = duration, + Intensity = intensity, + Type = type + }); + await _liveClient.Control(controlCommandsAll); + return true; + } + + if (!ShockOscConfigManager.ConfigInstance.Groups.TryGetValue(groupId, out var group)) return false; var controlCommands = group.Shockers.Select(x => new Control @@ -551,19 +602,26 @@ private async Task ControlGroup(Guid groupId, uint duration, byte intensit Intensity = intensity, Type = type }); - + await _liveClient.Control(controlCommands); return true; } - public async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) + private async Task RemoteActivateShockers(ControlLogSender sender, ICollection logs) { - // if (sender.ConnectionId == BackendLiveApiManager.ConnectionId) - // { - // _logger.LogDebug("Ignoring remote command log cause it was the local connection"); - // return; - // } + if (sender.ConnectionId == connectionId) + { + _logger.LogDebug("Ignoring remote command log cause it was the local connection"); + return; + } + + foreach (var controlLog in logs) await RemoteActivateShocker(sender, controlLog); + + } + + private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) + { var inSeconds = ((float)log.Duration / 1000).ToString(CultureInfo.InvariantCulture); if (sender.CustomName == null) @@ -576,7 +634,8 @@ public async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.CustomName, sender.Name); var template = ShockOscConfigManager.ConfigInstance.Chatbox.Types[log.Type]; - if (ShockOscConfigManager.ConfigInstance.Osc.Chatbox && ShockOscConfigManager.ConfigInstance.Chatbox.DisplayRemoteControl && template.Enabled) + if (ShockOscConfigManager.ConfigInstance.Osc.Chatbox && + ShockOscConfigManager.ConfigInstance.Chatbox.DisplayRemoteControl && template.Enabled) { // Chatbox message remote var dat = new @@ -593,26 +652,25 @@ public async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{Smart.Format(sender.CustomName == null ? template.Remote : template.RemoteWithCustomName, dat)}"; await _oscClient.SendChatboxMessage(msg); } - - var shocker = ProgramGroups.Values.Where(s => s.Id == log.Shocker.Id).ToArray(); - if (shocker.Length <= 0) - return; - + + var configGroupsAffected = _config.Groups.Where(s => s.Value.Shockers.Any(x => x == log.Shocker.Id)).Select(x => x.Key).ToArray(); + var programGroupsAffected = ProgramGroups.Where(x => configGroupsAffected.Contains(x.Key)).Select(x => x.Value); var oneShock = false; - foreach (var pain in shocker) + foreach (var pain in programGroupsAffected) { switch (log.Type) + { case ControlType.Shock: - { - pain.LastIntensity = log.Intensity; - pain.LastDuration = log.Duration; - pain.LastExecuted = log.ExecutedAt; - - oneShock = true; - break; - } + { + pain.LastIntensity = log.Intensity; + pain.LastDuration = log.Duration; + pain.LastExecuted = log.ExecutedAt; + + oneShock = true; + break; + } case ControlType.Vibrate: pain.LastVibration = log.ExecutedAt; break; @@ -626,7 +684,7 @@ public async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) _logger.LogError("ControlType was out of range. Value was: {Type}", log.Type); break; } - + if (oneShock) { ForceUnmute(); @@ -656,4 +714,4 @@ private Task CancelAction(ProgramGroup programGroup) public static float ClampFloat(float value) => value < 0 ? 0 : value > 1 ? 1 : value; public static uint LerpUint(uint min, uint max, float t) => (uint)(min + (max - min) * t); public static uint ClampUint(uint value, uint min, uint max) => value < min ? min : value > max ? max : value; -} +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index 570fe4a..399af0b 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -8,31 +8,32 @@ public Guid? Group { get; set; } = null; - public void AddGroup() + public async Task AddGroup() { var groupId = Guid.NewGuid(); ShockOscConfigManager.ConfigInstance.Groups.Add(groupId, new ShockOscConfigManager.ShockOscConfig.Group { Name = "New Group" }); Group = groupId; - InvokeAsync(StateHasChanged); + await InvokeAsync(StateHasChanged); ShockOscConfigManager.Save(); - ShockOsc.RaiseOnGroupsChanged(); + await ShockOsc.RaiseOnGroupsChanged(); } - public void DeleteGroup() + public async Task DeleteGroup() { if (Group == null) return; ShockOscConfigManager.ConfigInstance.Groups.Remove(Group.Value); Group = null; - InvokeAsync(StateHasChanged); + await InvokeAsync(StateHasChanged); ShockOscConfigManager.Save(); + await ShockOsc.RaiseOnGroupsChanged(); } - private Task OnSettingsValueChange() + private async Task OnSettingsValueChange() { ShockOscConfigManager.Save(); - return Task.CompletedTask; + await ShockOsc.RaiseOnGroupsChanged(); } private Task OnGroupSelect() diff --git a/ShockOsc/UnderscoreConfig.cs b/ShockOsc/UnderscoreConfig.cs index e82f7d0..8e85bda 100644 --- a/ShockOsc/UnderscoreConfig.cs +++ b/ShockOsc/UnderscoreConfig.cs @@ -29,16 +29,16 @@ public void HandleCommand(string parameterName, object?[] arguments) if (settingPath.Length == 2) { - var shockerName = settingPath[0]; + var groupName = settingPath[0]; var action = settingPath[1]; - if (!ShockOsc.ProgramGroups.ContainsKey(shockerName) && shockerName != "_All") + if (!ShockOsc.ProgramGroups.Any(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)) && groupName != "_All") { - _logger.LogWarning("Unknown shocker {Shocker}", shockerName); + _logger.LogWarning("Unknown shocker {Shocker}", groupName); _logger.LogDebug("Param: {Param}", action); return; } - var shocker = ShockOsc.ProgramGroups[shockerName]; + var group = ShockOsc.ProgramGroups.First(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)); var value = arguments.ElementAtOrDefault(0); // TODO: support groups From a3256fb369068adfa8b56b92646d53bc99b38bcd Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 7 Apr 2024 06:41:06 +0200 Subject: [PATCH 23/95] fix config debounce --- ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 14 ++++++------- ShockOsc/Ui/Components/Tabs/LogsTab.razor | 22 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor index e0505c8..06644f7 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -74,13 +74,13 @@
@if (intensity == "Fixed Intensity") { - Intensity: @Config.Behaviour.FixedIntensity.ToString()% + Intensity: @Config.Behaviour.FixedIntensity.ToString()% } else { - Intensity Min: @Config.Behaviour.IntensityRange.Min.ToString()% + Intensity Min: @Config.Behaviour.IntensityRange.Min.ToString()%
- Intensity Max: @Config.Behaviour.IntensityRange.Max.ToString()% + Intensity Max: @Config.Behaviour.IntensityRange.Max.ToString()% } @@ -124,11 +124,11 @@ _advancedSettingsExpanded = !_advancedSettingsExpanded; } - private async Task OnSettingsValueChange() + private async Task OnSliderSettingsValueChange() { if ((DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) { - await OnSettingsValueSave(); + await OnSettingsValueChange(); return; } @@ -142,7 +142,7 @@ if (!_cts.Token.IsCancellationRequested && (DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) { - await OnSettingsValueSave(); + await OnSettingsValueChange(); } } catch (TaskCanceledException) @@ -151,7 +151,7 @@ } } - private async Task OnSettingsValueSave() + private async Task OnSettingsValueChange() { _lastSettingsSave = DateTime.Now; Config.Behaviour.RandomIntensity = intensity == "Random Intensity"; diff --git a/ShockOsc/Ui/Components/Tabs/LogsTab.razor b/ShockOsc/Ui/Components/Tabs/LogsTab.razor index 76c0fbc..2500f74 100644 --- a/ShockOsc/Ui/Components/Tabs/LogsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/LogsTab.razor @@ -64,4 +64,24 @@ LogStore.OnLogAdded = null; } -} \ No newline at end of file +} + + \ No newline at end of file From 1342fdbec874ee80edb0cef0bee7d12c7e8a8410 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Mon, 8 Apr 2024 06:44:48 +1200 Subject: [PATCH 24/95] OscQuery toggle, remove relative %, fix debug param list --- ShockOsc/MauiProgram.cs | 21 ++-- ShockOsc/ShockOsc.cs | 71 ++++++------ ShockOsc/ShockOscConfigManager.cs | 14 ++- ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 113 +++++++++++--------- ShockOsc/Ui/Components/Tabs/DebugTab.razor | 2 +- ShockOsc/Ui/Components/UpdateLogout.razor | 2 +- 6 files changed, 133 insertions(+), 90 deletions(-) diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 8d4c33d..e1cd5e2 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -59,11 +59,15 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); var listenAddress = ShockOscConfigManager.ConfigInstance.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; - builder.Services.AddSingleton(provider => + if (ShockOscConfigManager.ConfigInstance.Osc.OscQuery) { - var shockOsc = provider.GetRequiredService(); - return new OscQueryServer("ShockOsc", listenAddress, shockOsc.FoundVrcClient, shockOsc.OnAvatarChange); - }); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(provider => + { + var shockOsc = provider.GetRequiredService(); + return new OscQueryServer("ShockOsc", listenAddress, shockOsc.FoundVrcClient, shockOsc.OnAvatarChange); + }); + } builder.Services.AddMudServices(); builder.Services.AddMauiBlazorWebView(); @@ -80,9 +84,12 @@ public static MauiApp CreateMauiApp() #endif var app = builder.Build(); - - // Warmup - app.Services.GetRequiredService(); + + if (ShockOscConfigManager.ConfigInstance.Osc.OscQuery) + { + // Warmup + app.Services.GetRequiredService(); + } return app; } diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 2e85d3c..46617c7 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -47,7 +47,7 @@ public sealed class ShockOsc "IShock" }; - public static Dictionary ParamsInUse = new(); + public static Dictionary ShockOscParams = new(); public static Dictionary AllAvatarParams = new(); public static Action? OnParamsChange; @@ -86,6 +86,11 @@ public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi open OnGroupsChanged += SetupGroups; SetupGroups().Wait(); + + if (!ShockOscConfigManager.ConfigInstance.Osc.OscQuery) + { + FoundVrcClient(); + } } private async Task SetupGroups() @@ -109,8 +114,17 @@ public void FoundVrcClient() _oscServerActive = false; Task.Delay(1000).Wait(); // wait for tasks to stop - _oscClient.CreateGameConnection(IPAddress.Parse(OscQueryServer.OscIpAddress), OscQueryServer.OscReceivePort, - OscQueryServer.OscSendPort); + if (ShockOscConfigManager.ConfigInstance.Osc.OscQuery) + { + _oscClient.CreateGameConnection(IPAddress.Parse(OscQueryServer.OscIpAddress), OscQueryServer.OscReceivePort, + OscQueryServer.OscSendPort); + } + else + { + _oscClient.CreateGameConnection(IPAddress.Loopback, ShockOscConfigManager.ConfigInstance.Osc.OscReceivePort, + ShockOscConfigManager.ConfigInstance.Osc.OscSendPort); + } + _logger.LogInformation("Connecting UDP Clients..."); // Start tasks @@ -141,7 +155,7 @@ public void OnAvatarChange(Dictionary? parameters, string avata return; } - ParamsInUse.Clear(); + ShockOscParams.Clear(); AllAvatarParams.Clear(); foreach (var param in parameters.Keys) @@ -154,19 +168,16 @@ public void OnAvatarChange(Dictionary? parameters, string avata var paramName = param[28..]; var lastUnderscoreIndex = paramName.LastIndexOf('_') + 1; - var action = string.Empty; var shockerName = paramName; + // var action = string.Empty; if (lastUnderscoreIndex > 1) { shockerName = paramName[..(lastUnderscoreIndex - 1)]; - action = paramName.Substring(lastUnderscoreIndex, paramName.Length - lastUnderscoreIndex); - } - - if (ShockerParams.Contains(action)) - { - parameterCount++; - ParamsInUse.TryAdd(paramName, parameters[param]); + // action = paramName.Substring(lastUnderscoreIndex, paramName.Length - lastUnderscoreIndex); } + + parameterCount++; + ShockOscParams.TryAdd(param[28..], parameters[param]); if (!ProgramGroups.Any(x => x.Value.Name.Equals(shockerName, StringComparison.InvariantCultureIgnoreCase)) && @@ -254,6 +265,14 @@ private async Task ReceiveLogic() var pos = addr.Substring(28, addr.Length - 28); + if (ShockOscParams.ContainsKey(pos)) + { + ShockOscParams[pos] = received.Arguments[0]; + OnParamChange(true); + } + else + ShockOscParams.TryAdd(pos, received.Arguments[0]); + // Check if _Config if (pos.StartsWith("_Config/")) { @@ -270,14 +289,6 @@ private async Task ReceiveLogic() action = pos.Substring(lastUnderscoreIndex, pos.Length - lastUnderscoreIndex); } - if (ParamsInUse.ContainsKey(pos)) - { - ParamsInUse[pos] = received.Arguments[0]; - OnParamChange(true); - } - else - ParamsInUse.TryAdd(pos, received.Arguments[0]); - if (!ShockerParams.Contains(action)) return; if (!ProgramGroups.Any(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase))) @@ -386,7 +397,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i { programGroup.LastExecuted = DateTime.UtcNow; programGroup.LastDuration = duration; - var intensityPercentage = Math.Round(GetFloatScaled(intensity) * 100f); + var intensityPercentage = Math.Round(ClampFloat(intensity) * 100f); programGroup.LastIntensity = intensity; ForceUnmute(); @@ -415,13 +426,13 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i await _oscClient.SendChatboxMessage(msg); } - /// - /// Coverts to a 0-1 float and scale it to the max intensity - /// - /// - /// - private static float GetFloatScaled(byte intensity) => - ClampFloat((float)intensity / ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max); + // /// + // /// Coverts to a 0-1 float and scale it to the max intensity + // /// + // /// + // /// + // private static float GetFloatScaled(byte intensity) => + // ClampFloat((float)intensity / ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max); private async Task SendParams() { @@ -453,12 +464,12 @@ private async Task SendParams() await shocker.ParamActive.SetValue(isActive); await shocker.ParamCooldown.SetValue(onCoolDown); await shocker.ParamCooldownPercentage.SetValue(cooldownPercentage); - await shocker.ParamIntensity.SetValue(GetFloatScaled(shocker.LastIntensity)); + await shocker.ParamIntensity.SetValue(ClampFloat(shocker.LastIntensity)); if (isActive) anyActive = true; if (onCoolDown) anyCooldown = true; anyCooldownPercentage = Math.Max(anyCooldownPercentage, cooldownPercentage); - anyIntensity = Math.Max(anyIntensity, GetFloatScaled(shocker.LastIntensity)); + anyIntensity = Math.Max(anyIntensity, ClampFloat(shocker.LastIntensity)); } await _paramAnyActive.SetValue(anyActive); diff --git a/ShockOsc/ShockOscConfigManager.cs b/ShockOsc/ShockOscConfigManager.cs index 53fd354..53ca1db 100644 --- a/ShockOsc/ShockOscConfigManager.cs +++ b/ShockOsc/ShockOscConfigManager.cs @@ -38,6 +38,12 @@ private static void TryLoad() catch (JsonException e) { Logger.Fatal(e, "Error during deserialization/loading of config"); + Logger.Warning("Attempting to move old config and generate a new one"); + File.Move(Path, Path + ".old"); + Save(); + json = File.ReadAllText(Path); + _internalConfig = JsonSerializer.Deserialize(json, Options); + Logger.Information("Successfully loaded config"); return; } } @@ -83,7 +89,10 @@ public static void Save() Chatbox = true, Hoscy = false, HoscySendPort = 9001, - QuestSupport = false + QuestSupport = false, + OscQuery = true, + OscSendPort = 9000, + OscReceivePort = 9001 }, Chatbox = new ShockOscConfig.ChatboxConf { @@ -212,6 +221,9 @@ public class OscConf public required bool Hoscy { get; set; } public ushort HoscySendPort { get; set; } = 9001; public required bool QuestSupport { get; set; } + public required bool OscQuery { get; set; } + public ushort OscSendPort { get; set; } = 9000; + public ushort OscReceivePort { get; set; } = 9001; } public class BehaviourConf diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor index 06644f7..59cadaf 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -3,6 +3,63 @@ @implements IDisposable @inject UnderscoreConfig underscoreConfig + + Shocker Options + + + + @foreach (ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction))) + { + @boneHeldAction + } + + +
+
+ + +
+ + + + +
+ @if (intensity == "Fixed Intensity") + { + Intensity: @Config.Behaviour.FixedIntensity.ToString()% + } + else + { + Intensity Min: @Config.Behaviour.IntensityRange.Min.ToString()% +
+ Intensity Max: @Config.Behaviour.IntensityRange.Max.ToString()% + } + + + + +
+ @if (duration == "Fixed Duration") + { + + } + else + { + + + + } +
+
+
+ + + Game Options + + + + + Chatbox Options @@ -54,60 +111,16 @@ - Shocker Options + OSC Options - - - @foreach (ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction))) - { - @boneHeldAction - } - - - - -
- - - - -
- @if (intensity == "Fixed Intensity") - { - Intensity: @Config.Behaviour.FixedIntensity.ToString()% - } - else + + + @if (!Config.Osc.OscQuery) { - Intensity Min: @Config.Behaviour.IntensityRange.Min.ToString()%
- Intensity Max: @Config.Behaviour.IntensityRange.Max.ToString()% - } - - - - -
- @if (duration == "Fixed Duration") - { - - } - else - { - - - + + } -
-
-
- - - Other Options - - - - - @code { diff --git a/ShockOsc/Ui/Components/Tabs/DebugTab.razor b/ShockOsc/Ui/Components/Tabs/DebugTab.razor index 26875b9..042758e 100644 --- a/ShockOsc/Ui/Components/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DebugTab.razor @@ -18,7 +18,7 @@ } else { - @foreach (var param in ShockOsc.ParamsInUse) + @foreach (var param in ShockOsc.ShockOscParams) { } diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Components/UpdateLogout.razor index 6aa87d1..c8fc3f9 100644 --- a/ShockOsc/Ui/Components/UpdateLogout.razor +++ b/ShockOsc/Ui/Components/UpdateLogout.razor @@ -22,7 +22,7 @@ } - @if (true || Updater.UpdateAvailable) + @if (Updater.UpdateAvailable) { From 3354cf6289506557009966974c4e5e2839fd486b Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 8 Apr 2024 09:36:13 +0200 Subject: [PATCH 25/95] Rework all of the config stuff and parts of the osc query server --- .gitignore | 3 +- ShockOsc/Backend/BackendLiveApiManager.cs | 11 +- ShockOsc/Backend/OpenShockApi.cs | 21 +- ShockOsc/Config/BehaviourConf.cs | 24 ++ ShockOsc/Config/ChatboxConf.cs | 72 +++++ ShockOsc/Config/ConfigManager.cs | 93 ++++++ ShockOsc/{Models => Config}/JsonRange.cs | 2 +- ShockOsc/Config/OpenShockConf.cs | 13 + ShockOsc/Config/OscConf.cs | 12 + ShockOsc/Config/ShockOscConfig.cs | 17 ++ ShockOsc/MauiProgram.cs | 64 ++--- ShockOsc/OscClient.cs | 11 +- ShockOsc/OscHandler.cs | 2 +- ShockOsc/OscQueryLibrary/OscQueryServer.cs | 87 +++--- ShockOsc/ShockOsc.cs | 112 ++++---- ShockOsc/ShockOsc.csproj | 1 + ShockOsc/ShockOscConfigManager.cs | 264 ------------------ ShockOsc/Ui/Components/MainLayout.razor | 72 ++--- ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 170 ++++++----- ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 27 +- ShockOsc/Ui/Components/Tabs/ShockersTab.razor | 9 +- ShockOsc/Ui/Components/UpdateDialog.razor | 13 +- ShockOsc/Ui/Components/UpdateLogout.razor | 14 +- .../Pages/Authentication/Authenticate.razor | 20 +- ShockOsc/UnderscoreConfig.cs | 55 ++-- ShockOsc/Updater.cs | 77 ++--- .../Utils/AsynchronousEventExtensions.cs | 5 +- ShockOsc/Utils/MathUtils.cs | 9 + 28 files changed, 667 insertions(+), 613 deletions(-) create mode 100644 ShockOsc/Config/BehaviourConf.cs create mode 100644 ShockOsc/Config/ChatboxConf.cs create mode 100644 ShockOsc/Config/ConfigManager.cs rename ShockOsc/{Models => Config}/JsonRange.cs (82%) create mode 100644 ShockOsc/Config/OpenShockConf.cs create mode 100644 ShockOsc/Config/OscConf.cs create mode 100644 ShockOsc/Config/ShockOscConfig.cs delete mode 100644 ShockOsc/ShockOscConfigManager.cs rename ShockOsc/{Ui => }/Utils/AsynchronousEventExtensions.cs (77%) create mode 100644 ShockOsc/Utils/MathUtils.cs diff --git a/.gitignore b/.gitignore index 07117fd..5c97cf8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ bin/ obj/ .idea *.DotSettings.user -.vs \ No newline at end of file +.vs +ShockOsc_Setup.exe \ No newline at end of file diff --git a/ShockOsc/Backend/BackendLiveApiManager.cs b/ShockOsc/Backend/BackendLiveApiManager.cs index 00df616..2c02a77 100644 --- a/ShockOsc/Backend/BackendLiveApiManager.cs +++ b/ShockOsc/Backend/BackendLiveApiManager.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using OpenShock.SDK.CSharp.Live; +using OpenShock.ShockOsc.Config; using Serilog; namespace OpenShock.ShockOsc.Backend; @@ -7,13 +8,13 @@ namespace OpenShock.ShockOsc.Backend; public sealed class BackendLiveApiManager { private readonly ILogger _logger; - private readonly ShockOscConfigManager.ShockOscConfig _config; + private readonly ConfigManager _configManager; private readonly OpenShockApiLiveClient _openShockApiLiveClient; - public BackendLiveApiManager(ILogger logger, ShockOscConfigManager.ShockOscConfig config, OpenShockApiLiveClient openShockApiLiveClient) + public BackendLiveApiManager(ILogger logger, ConfigManager configManager, OpenShockApiLiveClient openShockApiLiveClient) { _logger = logger; - _config = config; + _configManager = configManager; _openShockApiLiveClient = openShockApiLiveClient; } @@ -22,8 +23,8 @@ public async Task SetupLiveClient() { await _openShockApiLiveClient.Setup(new ApiLiveClientOptions() { - Token = _config.OpenShock.Token, - Server = _config.OpenShock.Backend, + Token = _configManager.Config.OpenShock.Token, + Server = _configManager.Config.OpenShock.Backend, ConfigureLogging = builder => { builder.ClearProviders(); diff --git a/ShockOsc/Backend/OpenShockApi.cs b/ShockOsc/Backend/OpenShockApi.cs index f1a3707..320d213 100644 --- a/ShockOsc/Backend/OpenShockApi.cs +++ b/ShockOsc/Backend/OpenShockApi.cs @@ -2,19 +2,20 @@ using OpenShock.SDK.CSharp; using OpenShock.SDK.CSharp.Live.Utils; using OpenShock.SDK.CSharp.Models; +using OpenShock.ShockOsc.Config; namespace OpenShock.ShockOsc.Backend; public sealed class OpenShockApi { private readonly ILogger _logger; - private readonly ShockOscConfigManager.ShockOscConfig _config; + private readonly ConfigManager _configManager; private OpenShockApiClient _client; - public OpenShockApi(ILogger logger, ShockOscConfigManager.ShockOscConfig config) + public OpenShockApi(ILogger logger, ConfigManager configManager) { _logger = logger; - _config = config; + _configManager = configManager; SetupApiClient(); } @@ -22,8 +23,8 @@ public void SetupApiClient() { _client = new OpenShockApiClient(new ApiClientOptions { - Server = _config.OpenShock.Backend, - Token = _config.OpenShock.Token + Server = _configManager.Config.OpenShock.Backend, + Token = _configManager.Config.OpenShock.Token }); } @@ -40,23 +41,23 @@ public async Task RefreshShockers() Shockers = success.Value.SelectMany(x => x.Shockers).ToArray(); // re-populate config with previous data if present, this also deletes any shockers that are no longer present - var shockerList = new Dictionary(); + var shockerList = new Dictionary(); foreach (var shocker in Shockers) { var enabled = true; - if (ShockOscConfigManager.ConfigInstance.OpenShock.Shockers.TryGetValue(shocker.Id, out var confShocker)) + if (_configManager.Config.OpenShock.Shockers.TryGetValue(shocker.Id, out var confShocker)) { enabled = confShocker.Enabled; } - shockerList.Add(shocker.Id, new ShockOscConfigManager.ShockOscConfig.ShockerConf + shockerList.Add(shocker.Id, new OpenShockConf.ShockerConf { Enabled = enabled }); } - ShockOscConfigManager.ConfigInstance.OpenShock.Shockers = shockerList; - ShockOscConfigManager.Save(); + _configManager.Config.OpenShock.Shockers = shockerList; + _configManager.Save(); OnShockersUpdated.Raise(Shockers); }, error => diff --git a/ShockOsc/Config/BehaviourConf.cs b/ShockOsc/Config/BehaviourConf.cs new file mode 100644 index 0000000..4bd9499 --- /dev/null +++ b/ShockOsc/Config/BehaviourConf.cs @@ -0,0 +1,24 @@ +namespace OpenShock.ShockOsc.Config; + +public sealed class BehaviourConf +{ + public bool RandomIntensity { get; set; } + public bool RandomDuration { get; set; } + public uint RandomDurationStep { get; set; } = 1000; + public JsonRange DurationRange { get; set; } = new JsonRange { Min = 1000, Max = 5000 }; + public JsonRange IntensityRange { get; set; } = new JsonRange { Min = 1, Max = 30 }; + public byte FixedIntensity { get; set; } = 50; + public uint FixedDuration { get; set; } = 2000; + public uint HoldTime { get; set; } = 250; + public uint CooldownTime { get; set; } = 5000; + public BoneHeldAction WhileBoneHeld { get; set; } = BoneHeldAction.Vibrate; + public bool DisableWhileAfk { get; set; } = true; + public bool ForceUnmute { get; set; } + + public enum BoneHeldAction + { + Vibrate = 0, + Shock = 1, + None = 2 + } +} \ No newline at end of file diff --git a/ShockOsc/Config/ChatboxConf.cs b/ShockOsc/Config/ChatboxConf.cs new file mode 100644 index 0000000..adb21cd --- /dev/null +++ b/ShockOsc/Config/ChatboxConf.cs @@ -0,0 +1,72 @@ +using OpenShock.SDK.CSharp.Models; + +namespace OpenShock.ShockOsc.Config; + +public sealed class ChatboxConf +{ + public string Prefix { get; set; } = "[ShockOsc] "; + public bool DisplayRemoteControl { get; set; } = true; + + public HoscyMessageType HoscyType { get; set; } = HoscyMessageType.Message; + + public string IgnoredKillSwitchActive { get; set; } = "Ignoring Shock, kill switch is active"; + public string IgnoredAfk { get; set; } = "Ignoring Shock, user is afk"; + + public IDictionary Types { get; set; } = + new Dictionary + { + { + ControlType.Stop, new ControlTypeConf + { + Enabled = true, + Local = "⏸ '{ShockerName}'", + Remote = "⏸ '{ShockerName}' by {Name}", + RemoteWithCustomName = "⏸ '{ShockerName}' by {CustomName} [{Name}]" + } + }, + { + ControlType.Shock, new ControlTypeConf + { + Enabled = true, + Local = "⚡ '{ShockerName}' {Intensity}%:{DurationSeconds}s", + Remote = "⚡ '{ShockerName}' {Intensity}%:{DurationSeconds}s by {Name}", + RemoteWithCustomName = + "⚡ '{ShockerName}' {Intensity}%:{DurationSeconds}s by {CustomName} [{Name}]" + } + }, + { + ControlType.Vibrate, new ControlTypeConf + { + Enabled = true, + Local = "〜 '{ShockerName}' {Intensity}%:{DurationSeconds}s", + Remote = "〜 '{ShockerName}' {Intensity}%:{DurationSeconds}s by {Name}", + RemoteWithCustomName = + "〜 '{ShockerName}' {Intensity}%:{DurationSeconds}s by {CustomName} [{Name}]" + } + }, + { + ControlType.Sound, new ControlTypeConf + { + Enabled = true, + Local = "🔈 '{ShockerName}' {Intensity}%:{DurationSeconds}s", + Remote = "🔈 '{ShockerName}' {Intensity}%:{DurationSeconds}s by {Name}", + RemoteWithCustomName = + "🔈 '{ShockerName}' {Intensity}%:{DurationSeconds}s by {CustomName} [{Name}]" + } + } + }; + + public sealed class ControlTypeConf + { + public required bool Enabled { get; set; } + public required string Local { get; set; } + public required string Remote { get; set; } + public required string RemoteWithCustomName { get; set; } + } + + public enum HoscyMessageType + { + Message, + Notification + } +} \ No newline at end of file diff --git a/ShockOsc/Config/ConfigManager.cs b/ShockOsc/Config/ConfigManager.cs new file mode 100644 index 0000000..724e1eb --- /dev/null +++ b/ShockOsc/Config/ConfigManager.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using OpenShock.ShockOsc.Utils; + +namespace OpenShock.ShockOsc.Config; + +public sealed class ConfigManager +{ + private static readonly string Path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\ShockOSC\config.json"; + + private readonly ILogger _logger; + public ShockOscConfig Config { get; } + + public ConfigManager(ILogger logger) + { + _logger = logger; + + // Load config + ShockOscConfig? config = null; + + _logger.LogInformation("Config file found, trying to load config from {Path}", Path); + if (File.Exists(Path)) + { + _logger.LogTrace("Config file exists"); + var json = File.ReadAllText(Path); + if (!string.IsNullOrWhiteSpace(json)) + { + _logger.LogTrace("Config file is not empty"); + try + { + config = JsonSerializer.Deserialize(json, Options); + } + catch (JsonException e) + { + _logger.LogCritical(e, "Error during deserialization/loading of config"); + _logger.LogWarning("Attempting to move old config and generate a new one"); + File.Move(Path, Path + ".old"); + } + } + } + + if (config != null) + { + Config = config; + _logger.LogInformation("Successfully loaded config"); + return; + } + _logger.LogInformation("No config file found (does not exist or empty or invalid), generating new one at {Path}", Path); + Config = new ShockOscConfig(); + SaveAsync().Wait(); + _logger.LogInformation("New configuration file generated!"); + } + + private static readonly JsonSerializerOptions Options = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() } + }; + + private readonly SemaphoreSlim _saveLock = new(1, 1); + + public async Task SaveAsync() + { + await _saveLock.WaitAsync().ConfigureAwait(false); + try + { + _logger.LogTrace("Saving config"); + var directory = System.IO.Path.GetDirectoryName(Path); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + await File.WriteAllTextAsync(Path, JsonSerializer.Serialize(Config, Options)).ConfigureAwait(false); + _logger.LogInformation("Config saved"); + } + catch (Exception e) + { + _logger.LogError(e, "Error occurred while saving new config file"); + } + finally + { + _saveLock.Release(); + } + } + + public void Save() + { + SaveAsync().Wait(); + } + + public void SaveFnf() => OsTask.Run(SaveAsync); + +} \ No newline at end of file diff --git a/ShockOsc/Models/JsonRange.cs b/ShockOsc/Config/JsonRange.cs similarity index 82% rename from ShockOsc/Models/JsonRange.cs rename to ShockOsc/Config/JsonRange.cs index 67b1dec..d2d5fba 100644 --- a/ShockOsc/Models/JsonRange.cs +++ b/ShockOsc/Config/JsonRange.cs @@ -1,5 +1,5 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace OpenShock.ShockOsc.Models; +namespace OpenShock.ShockOsc.Config; public class JsonRange { diff --git a/ShockOsc/Config/OpenShockConf.cs b/ShockOsc/Config/OpenShockConf.cs new file mode 100644 index 0000000..74670d2 --- /dev/null +++ b/ShockOsc/Config/OpenShockConf.cs @@ -0,0 +1,13 @@ +namespace OpenShock.ShockOsc.Config; + +public sealed class OpenShockConf +{ + public Uri Backend { get; set; } = new("https://api.shocklink.net"); + public string Token { get; set; } = ""; + public IReadOnlyDictionary Shockers { get; set; } = new Dictionary(); + + public sealed class ShockerConf + { + public bool Enabled { get; set; } = true; + } +} \ No newline at end of file diff --git a/ShockOsc/Config/OscConf.cs b/ShockOsc/Config/OscConf.cs new file mode 100644 index 0000000..895ff80 --- /dev/null +++ b/ShockOsc/Config/OscConf.cs @@ -0,0 +1,12 @@ +namespace OpenShock.ShockOsc.Config; + +public sealed class OscConf +{ + public bool Chatbox { get; set; } = true; + public bool Hoscy { get; set; } = false; + public ushort HoscySendPort { get; set; } = 9001; + public bool QuestSupport { get; set; } = false; + public bool OscQuery { get; set; } = true; + public ushort OscSendPort { get; set; } = 9000; + public ushort OscReceivePort { get; set; } = 9001; +} \ No newline at end of file diff --git a/ShockOsc/Config/ShockOscConfig.cs b/ShockOsc/Config/ShockOscConfig.cs new file mode 100644 index 0000000..91b48ee --- /dev/null +++ b/ShockOsc/Config/ShockOscConfig.cs @@ -0,0 +1,17 @@ +namespace OpenShock.ShockOsc.Config; + +public sealed class ShockOscConfig +{ + public OscConf Osc { get; set; } = new(); + public BehaviourConf Behaviour { get; set; } = new(); + public OpenShockConf OpenShock { get; set; } = new(); + public ChatboxConf Chatbox { get; set; } = new(); + public IDictionary Groups { get; set; } = new Dictionary(); + public Version? LastIgnoredVersion { get; set; } = null; + + public sealed class Group + { + public required string Name { get; set; } + public IList Shockers { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index e1cd5e2..4984b43 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -3,6 +3,7 @@ using MudBlazor.Services; using OpenShock.SDK.CSharp.Live; using OpenShock.ShockOsc.Backend; +using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Ui; @@ -15,9 +16,9 @@ public static class MauiProgram public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); - + // <---- Services ----> - + var loggerConfiguration = new LoggerConfiguration() .MinimumLevel.Information() .Filter.ByExcluding(ev => @@ -29,68 +30,65 @@ public static MauiApp CreateMauiApp() outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"); // ReSharper disable once RedundantAssignment - var isDebug = Environment.GetCommandLineArgs().Any(x => x.Equals("--debug", StringComparison.InvariantCultureIgnoreCase)); - + var isDebug = Environment.GetCommandLineArgs() + .Any(x => x.Equals("--debug", StringComparison.InvariantCultureIgnoreCase)); + #if DEBUG isDebug = true; #endif if (isDebug) { - loggerConfiguration.MinimumLevel.Debug(); + Console.WriteLine("Debug mode enabled"); + loggerConfiguration.MinimumLevel.Verbose(); } Log.Logger = loggerConfiguration.CreateLogger(); - + builder.Services.AddSerilog(Log.Logger); - builder.Services.AddSingleton(ShockOscConfigManager.ConfigInstance); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); builder.Services.AddSingleton(); - + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - + builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - var listenAddress = ShockOscConfigManager.ConfigInstance.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; - if (ShockOscConfigManager.ConfigInstance.Osc.OscQuery) + builder.Services.AddSingleton(provider => { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(provider => - { - var shockOsc = provider.GetRequiredService(); - return new OscQueryServer("ShockOsc", listenAddress, shockOsc.FoundVrcClient, shockOsc.OnAvatarChange); - }); - } + var config = provider.GetRequiredService(); + var listenAddress = config.Config.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; + return new OscQueryServer("ShockOsc", listenAddress, config); + }); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddMudServices(); builder.Services.AddMauiBlazorWebView(); - + // <---- App ----> - + builder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); - + #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); #endif - - var app = builder.Build(); - - if (ShockOscConfigManager.ConfigInstance.Osc.OscQuery) - { - // Warmup - app.Services.GetRequiredService(); - } + var app = builder.Build(); + + // <---- Warmup ----> + app.Services.GetRequiredService(); + app.Services.GetRequiredService().Start(); + return app; } } \ No newline at end of file diff --git a/ShockOsc/OscClient.cs b/ShockOsc/OscClient.cs index fbc2175..8f99a88 100644 --- a/ShockOsc/OscClient.cs +++ b/ShockOsc/OscClient.cs @@ -2,18 +2,23 @@ using System.Threading.Channels; using LucHeart.CoreOSC; using Microsoft.Extensions.Logging; +using OpenShock.ShockOsc.Config; namespace OpenShock.ShockOsc; public sealed class OscClient { private readonly ILogger _logger; + private readonly ConfigManager _configManager; private OscDuplex? _gameConnection; - private readonly OscSender _hoscySenderClient = new(new IPEndPoint(IPAddress.Loopback, ShockOscConfigManager.ConfigInstance.Osc.HoscySendPort)); + private readonly OscSender _hoscySenderClient; - public OscClient(ILogger logger) + public OscClient(ILogger logger, ConfigManager configManager) { _logger = logger; + _configManager = configManager; + _hoscySenderClient = new OscSender(new IPEndPoint(IPAddress.Loopback, _configManager.Config.Osc.HoscySendPort)); + Task.Run(GameSenderLoop); Task.Run(HoscySenderLoop); } @@ -44,7 +49,7 @@ public ValueTask SendGameMessage(string address, params object?[]?arguments) public ValueTask SendChatboxMessage(string message) { - if (ShockOscConfigManager.ConfigInstance.Osc.Hoscy) return _hoscySenderChannel.Writer.WriteAsync(new OscMessage("/hoscy/message", message)); + if (_configManager.Config.Osc.Hoscy) return _hoscySenderChannel.Writer.WriteAsync(new OscMessage("/hoscy/message", message)); return _gameSenderChannel.Writer.WriteAsync(new OscMessage("/chatbox/input", message, true)); } diff --git a/ShockOsc/OscHandler.cs b/ShockOsc/OscHandler.cs index 224bdb1..eb0cffc 100644 --- a/ShockOsc/OscHandler.cs +++ b/ShockOsc/OscHandler.cs @@ -8,6 +8,6 @@ public sealed class OscHandler public OscHandler(ILogger logger) { - + logger.LogInformation("YES STARTED"); } } \ No newline at end of file diff --git a/ShockOsc/OscQueryLibrary/OscQueryServer.cs b/ShockOsc/OscQueryLibrary/OscQueryServer.cs index d31dd6f..9acb469 100644 --- a/ShockOsc/OscQueryLibrary/OscQueryServer.cs +++ b/ShockOsc/OscQueryLibrary/OscQueryServer.cs @@ -7,6 +7,10 @@ using Serilog; using EmbedIO; using EmbedIO.Actions; +using Microsoft.Extensions.Hosting; +using OpenShock.SDK.CSharp.Live.Utils; +using OpenShock.ShockOsc.Config; +using OpenShock.ShockOsc.Utils; namespace OpenShock.ShockOsc.OscQueryLibrary; @@ -18,9 +22,8 @@ public class OscQueryServer : IDisposable private readonly ushort _httpPort; private readonly IPAddress _ipAddress; - public static string OscIpAddress; - public static ushort OscReceivePort; - public static ushort OscSendPort; + private readonly ConfigManager _configManager; + public readonly ushort ShockOscReceivePort; private const string OscHttpServiceName = "_oscjson._tcp"; private const string OscUdpServiceName = "_osc._udp"; private readonly MulticastService _multicastService; @@ -29,32 +32,35 @@ public class OscQueryServer : IDisposable private OscQueryModels.HostInfo? _hostInfo; private object? _queryData; - private static readonly HashSet FoundServices = new(); - private static IPEndPoint? _lastVrcHttpServer; - private static event Action? FoundVrcClient; - private static event Action, string>? ParameterUpdate; - private static readonly Dictionary ParameterList = new(); + private readonly HashSet FoundServices = new(); + private IPEndPoint? _lastVrcHttpServer; + + + public event Func? FoundVrcClient; + private event Func, string, Task>? ParameterUpdate; + + private readonly Dictionary ParameterList = new(); + + private readonly WebServer _httpServer; + private readonly string _httpServerUrl; - public OscQueryServer(string serviceName, IPAddress ipAddress, - Action? foundVrcClient = null, - Action, string>? parameterUpdate = null) + public OscQueryServer(string serviceName, IPAddress ipAddress, ConfigManager configManager) { Swan.Logging.Logger.NoLogging(); _serviceName = serviceName; _ipAddress = ipAddress; - OscReceivePort = FindAvailableUdpPort(); + _configManager = configManager; + ShockOscReceivePort = FindAvailableUdpPort(); _httpPort = FindAvailableTcpPort(); - FoundVrcClient = foundVrcClient; - ParameterUpdate = parameterUpdate; SetupJsonObjects(); // ignore our own service FoundServices.Add($"{_serviceName.ToLower()}.{OscHttpServiceName}.local:{_httpPort}"); // HTTP Server - var url = $"http://{_ipAddress}:{_httpPort}/"; - var server = new WebServer(o => o - .WithUrlPrefix(url) + _httpServerUrl = $"http://{_ipAddress}:{_httpPort}/"; + _httpServer = new WebServer(o => o + .WithUrlPrefix(_httpServerUrl) .WithMode(HttpListenerMode.EmbedIO)) .WithModule(new ActionModule("/", HttpVerbs.Get, ctx => ctx.SendStringAsync( @@ -62,9 +68,6 @@ public OscQueryServer(string serviceName, IPAddress ipAddress, ? JsonSerializer.Serialize(_hostInfo) : JsonSerializer.Serialize(_queryData), "application/json", Encoding.UTF8))); - server.RunAsync(); - Logger.Debug("OSCQueryHttpServer: Listening at {Prefix}", url); - // mDNS _multicastService = new MulticastService { @@ -72,18 +75,30 @@ public OscQueryServer(string serviceName, IPAddress ipAddress, IgnoreDuplicateMessages = true }; _serviceDiscovery = new ServiceDiscovery(_multicastService); + } + + public void Start() + { + if (!_configManager.Config.Osc.OscQuery) + { + Logger.Debug("OSCQuery: Disabled"); + return; + } + OsTask.Run(() => _httpServer.RunAsync()); + Logger.Debug("OSCQueryHttpServer: Listening at {Prefix}", _httpServerUrl); + ListenForServices(); _multicastService.Start(); AdvertiseOscQueryServer(); } - + private void AdvertiseOscQueryServer() { var httpProfile = new ServiceProfile(_serviceName, OscHttpServiceName, _httpPort, new[] { _ipAddress }); var oscProfile = - new ServiceProfile(_serviceName, OscUdpServiceName, OscReceivePort, + new ServiceProfile(_serviceName, OscUdpServiceName, ShockOscReceivePort, new[] { _ipAddress }); _serviceDiscovery.Advertise(httpProfile); _serviceDiscovery.Advertise(oscProfile); @@ -146,12 +161,13 @@ private void OnAnswerReceived(object? sender, MessageEventArgs args) private async Task FoundNewVrcClient(IPAddress ipAddress, int port) { _lastVrcHttpServer = new IPEndPoint(ipAddress, port); - await FetchOscSendPortFromVrc(ipAddress, port); - FoundVrcClient?.Invoke(); + var oscEndpoint = await FetchOscSendPortFromVrc(ipAddress, port); + if(oscEndpoint == null) return; + FoundVrcClient?.Raise(oscEndpoint); await FetchJsonFromVrc(ipAddress, port); } - private async Task FetchOscSendPortFromVrc(IPAddress ipAddress, int port) + private async Task FetchOscSendPortFromVrc(IPAddress ipAddress, int port) { var url = $"http://{ipAddress}:{port}?HOST_INFO"; Logger.Debug("OSCQueryHttpClient: Fetching OSC send port from {Url}", url); @@ -164,11 +180,10 @@ private async Task FetchOscSendPortFromVrc(IPAddress ipAddress, int port) if (rootNode?.OSC_PORT == null) { Logger.Error("OSCQueryHttpClient: Error no OSC port found"); - return; + return null; } - - OscSendPort = (ushort)rootNode.OSC_PORT; - OscIpAddress = rootNode.OSC_IP; + + return new IPEndPoint(IPAddress.Parse(rootNode.OSC_IP), (ushort)rootNode.OSC_PORT); } catch (HttpRequestException ex) { @@ -178,11 +193,13 @@ private async Task FetchOscSendPortFromVrc(IPAddress ipAddress, int port) { Logger.Error("OSCQueryHttpClient: Error {ExMessage}\\n{Response}", ex.Message, response); } + + return null; } private static bool _fetchInProgress; - private static async Task FetchJsonFromVrc(IPAddress ipAddress, int port) + private async Task FetchJsonFromVrc(IPAddress ipAddress, int port) { if (_fetchInProgress) return; _fetchInProgress = true; @@ -207,13 +224,13 @@ private static async Task FetchJsonFromVrc(IPAddress ipAddress, int port) } avatarId = rootNode.CONTENTS.avatar.CONTENTS.change.VALUE?[0]?.ToString() ?? string.Empty; - ParameterUpdate?.Invoke(ParameterList, avatarId); + ParameterUpdate?.Raise(ParameterList, avatarId); } catch (HttpRequestException ex) { _lastVrcHttpServer = null; ParameterList.Clear(); - ParameterUpdate?.Invoke(ParameterList, avatarId); + ParameterUpdate?.Raise(ParameterList, avatarId); Logger.Error("OSCQueryHttpClient: Error {ExMessage}", ex.Message); } catch (Exception ex) @@ -226,7 +243,7 @@ private static async Task FetchJsonFromVrc(IPAddress ipAddress, int port) } } - private static void RecursiveParameterLookup(OscQueryModels.Node node) + private void RecursiveParameterLookup(OscQueryModels.Node node) { if (node.CONTENTS == null) { @@ -240,7 +257,7 @@ private static void RecursiveParameterLookup(OscQueryModels.Node node) } } - public static async Task GetParameters() + public async Task GetParameters() { if (_lastVrcHttpServer == null) return; @@ -288,7 +305,7 @@ private void SetupJsonObjects() _hostInfo = new OscQueryModels.HostInfo { NAME = _serviceName, - OSC_PORT = OscReceivePort, + OSC_PORT = ShockOscReceivePort, OSC_IP = _ipAddress.ToString(), OSC_TRANSPORT = "UDP", EXTENSIONS = new OscQueryModels.Extensions diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 46617c7..391cd40 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -7,10 +7,10 @@ using OpenShock.SDK.CSharp.Live.Models; using OpenShock.SDK.CSharp.Models; using OpenShock.ShockOsc.Backend; +using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.OscChangeTracker; using OpenShock.ShockOsc.OscQueryLibrary; -using OpenShock.ShockOsc.Ui.Utils; using OpenShock.ShockOsc.Utils; using SmartFormat; @@ -24,7 +24,8 @@ public sealed class ShockOsc private readonly OscClient _oscClient; private readonly OpenShockApiLiveClient _liveClient; private readonly UnderscoreConfig _underscoreConfig; - private readonly ShockOscConfigManager.ShockOscConfig _config; + private readonly ConfigManager _configManager; + private readonly OscQueryServer _oscQueryServer; private static bool _oscServerActive; private static bool _isAfk; @@ -62,13 +63,14 @@ public sealed class ShockOsc public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi openShockApi, OpenShockApiLiveClient liveClient, UnderscoreConfig underscoreConfig, - ShockOscConfigManager.ShockOscConfig config) + ConfigManager configManager, OscQueryServer oscQueryServer) { _logger = logger; _oscClient = oscClient; _liveClient = liveClient; _underscoreConfig = underscoreConfig; - _config = config; + _configManager = configManager; + _oscQueryServer = oscQueryServer; _paramAnyActive = new ChangeTrackedOscParam("_Any", "_Active", false, _oscClient); _paramAnyCooldown = new ChangeTrackedOscParam("_Any", "_Cooldown", false, _oscClient); @@ -85,19 +87,23 @@ public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi open OnGroupsChanged += SetupGroups; + oscQueryServer.FoundVrcClient += FoundVrcClient; + SetupGroups().Wait(); - if (!ShockOscConfigManager.ConfigInstance.Osc.OscQuery) + if (!_configManager.Config.Osc.OscQuery) { - FoundVrcClient(); + FoundVrcClient(null); } + + _logger.LogInformation("Started ShockOsc.cs"); } private async Task SetupGroups() { ProgramGroups.Clear(); ProgramGroups[Guid.Empty] = new ProgramGroup(Guid.Empty, "_All", _oscClient); - foreach (var (id, group) in _config.Groups) ProgramGroups[id] = new ProgramGroup(id, group.Name, _oscClient); + foreach (var (id, group) in _configManager.Config.Groups) ProgramGroups[id] = new ProgramGroup(id, group.Name, _oscClient); } public Task RaiseOnGroupsChanged() => OnGroupsChanged.Raise(); @@ -107,22 +113,21 @@ private static void OnParamChange(bool shockOscParam) OnParamsChange?.Invoke(shockOscParam); } - public void FoundVrcClient() + public async Task FoundVrcClient(IPEndPoint? oscClient) { _logger.LogInformation("Found VRC client"); // stop tasks _oscServerActive = false; Task.Delay(1000).Wait(); // wait for tasks to stop - if (ShockOscConfigManager.ConfigInstance.Osc.OscQuery) + if (oscClient != null) { - _oscClient.CreateGameConnection(IPAddress.Parse(OscQueryServer.OscIpAddress), OscQueryServer.OscReceivePort, - OscQueryServer.OscSendPort); + _oscClient.CreateGameConnection(oscClient.Address, _oscQueryServer.ShockOscReceivePort, (ushort)oscClient.Port); } else { - _oscClient.CreateGameConnection(IPAddress.Loopback, ShockOscConfigManager.ConfigInstance.Osc.OscReceivePort, - ShockOscConfigManager.ConfigInstance.Osc.OscSendPort); + _oscClient.CreateGameConnection(IPAddress.Loopback, _configManager.Config.Osc.OscReceivePort, + _configManager.Config.Osc.OscSendPort); } _logger.LogInformation("Connecting UDP Clients..."); @@ -247,7 +252,7 @@ private async Task ReceiveLogic() case "/avatar/change": var avatarId = received.Arguments.ElementAtOrDefault(0); _logger.LogDebug("Avatar changed: {AvatarId}", avatarId); - OsTask.Run(OscQueryServer.GetParameters); + OsTask.Run(_oscQueryServer.GetParameters); OsTask.Run(_underscoreConfig.SendUpdateForAll); return; case "/avatar/parameters/AFK": @@ -315,7 +320,7 @@ private async Task ReceiveLogic() return; } - if (_isAfk && ShockOscConfigManager.ConfigInstance.Behaviour.DisableWhileAfk) + if (_isAfk && _configManager.Config.Behaviour.DisableWhileAfk) { programGroup.TriggerMethod = TriggerMethod.None; await LogIgnoredAfk(); @@ -339,8 +344,8 @@ private async Task ReceiveLogic() programGroup.TriggerMethod = TriggerMethod.PhysBoneRelease; programGroup.LastActive = DateTime.UtcNow; } - else if (ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld != - ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None) + else if (_configManager.Config.Behaviour.WhileBoneHeld != + BehaviourConf.BoneHeldAction.None) { await CancelAction(programGroup); } @@ -367,21 +372,21 @@ private async Task ReceiveLogic() private ValueTask LogIgnoredKillSwitchActive() { _logger.LogInformation("Ignoring shock, kill switch is active"); - if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive)) + if (string.IsNullOrEmpty(_configManager.Config.Chatbox.IgnoredKillSwitchActive)) return ValueTask.CompletedTask; return _oscClient.SendChatboxMessage( - $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredKillSwitchActive}"); + $"{_configManager.Config.Chatbox.Prefix}{_configManager.Config.Chatbox.IgnoredKillSwitchActive}"); } private ValueTask LogIgnoredAfk() { _logger.LogInformation("Ignoring shock, user is AFK"); - if (string.IsNullOrEmpty(ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk)) + if (string.IsNullOrEmpty(_configManager.Config.Chatbox.IgnoredAfk)) return ValueTask.CompletedTask; return _oscClient.SendChatboxMessage( - $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{ShockOscConfigManager.ConfigInstance.Chatbox.IgnoredAfk}"); + $"{_configManager.Config.Chatbox.Prefix}{_configManager.Config.Chatbox.IgnoredAfk}"); } private async Task SenderLoopAsync() @@ -397,7 +402,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i { programGroup.LastExecuted = DateTime.UtcNow; programGroup.LastDuration = duration; - var intensityPercentage = Math.Round(ClampFloat(intensity) * 100f); + var intensityPercentage = Math.Round(MathUtils.ClampFloat(intensity) * 100f); programGroup.LastIntensity = intensity; ForceUnmute(); @@ -411,7 +416,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i await ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock); - if (!ShockOscConfigManager.ConfigInstance.Osc.Chatbox) return; + if (!_configManager.Config.Osc.Chatbox) return; // Chatbox message local var dat = new { @@ -421,8 +426,8 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i Duration = duration, DurationSeconds = inSeconds }; - var template = ShockOscConfigManager.ConfigInstance.Chatbox.Types[ControlType.Shock]; - var msg = $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{Smart.Format(template.Local, dat)}"; + var template = _configManager.Config.Chatbox.Types[ControlType.Shock]; + var msg = $"{_configManager.Config.Chatbox.Prefix}{Smart.Format(template.Local, dat)}"; await _oscClient.SendChatboxMessage(msg); } @@ -432,7 +437,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i // /// // /// // private static float GetFloatScaled(byte intensity) => - // ClampFloat((float)intensity / ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max); + // ClampFloat((float)intensity / _configManager.ConfigInstance.Behaviour.IntensityRange.Max); private async Task SendParams() { @@ -446,7 +451,7 @@ private async Task SendParams() { var isActive = shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; var isActiveOrOnCooldown = - shocker.LastExecuted.AddMilliseconds(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime) + shocker.LastExecuted.AddMilliseconds(_configManager.Config.Behaviour.CooldownTime) .AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; if (!isActiveOrOnCooldown && shocker.LastIntensity > 0) shocker.LastIntensity = 0; @@ -455,21 +460,21 @@ private async Task SendParams() var cooldownPercentage = 0f; if (onCoolDown) - cooldownPercentage = ClampFloat(1 - - (float)(DateTime.UtcNow - - shocker.LastExecuted.AddMilliseconds(shocker.LastDuration)) - .TotalMilliseconds / - ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime); + cooldownPercentage = MathUtils.ClampFloat(1 - + (float)(DateTime.UtcNow - + shocker.LastExecuted.AddMilliseconds(shocker.LastDuration)) + .TotalMilliseconds / + _configManager.Config.Behaviour.CooldownTime); await shocker.ParamActive.SetValue(isActive); await shocker.ParamCooldown.SetValue(onCoolDown); await shocker.ParamCooldownPercentage.SetValue(cooldownPercentage); - await shocker.ParamIntensity.SetValue(ClampFloat(shocker.LastIntensity)); + await shocker.ParamIntensity.SetValue(MathUtils.ClampFloat(shocker.LastIntensity)); if (isActive) anyActive = true; if (onCoolDown) anyCooldown = true; anyCooldownPercentage = Math.Max(anyCooldownPercentage, cooldownPercentage); - anyIntensity = Math.Max(anyIntensity, ClampFloat(shocker.LastIntensity)); + anyIntensity = Math.Max(anyIntensity, MathUtils.ClampFloat(shocker.LastIntensity)); } await _paramAnyActive.SetValue(anyActive); @@ -497,7 +502,7 @@ private async Task CheckLoop() private byte GetIntensity() { - var config = ShockOscConfigManager.ConfigInstance.Behaviour; + var config = _configManager.Config.Behaviour; if (!config.RandomIntensity) return config.FixedIntensity; var rir = config.IntensityRange; @@ -507,16 +512,16 @@ private byte GetIntensity() private async Task CheckLogic() { - var config = ShockOscConfigManager.ConfigInstance.Behaviour; + var config = _configManager.Config.Behaviour; foreach (var (pos, programGroup) in ProgramGroups) { var isActiveOrOnCooldown = - programGroup.LastExecuted.AddMilliseconds(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime) + programGroup.LastExecuted.AddMilliseconds(_configManager.Config.Behaviour.CooldownTime) .AddMilliseconds(programGroup.LastDuration) > DateTime.UtcNow; if (programGroup.TriggerMethod == TriggerMethod.None && - ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld != - ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.None && + _configManager.Config.Behaviour.WhileBoneHeld != + BehaviourConf.BoneHeldAction.None && !isActiveOrOnCooldown && programGroup.IsGrabbed && programGroup.LastVibration < DateTime.UtcNow.Subtract(TimeSpan.FromMilliseconds(300))) @@ -528,8 +533,8 @@ private async Task CheckLogic() _logger.LogDebug("Vibrating {Shocker} at {Intensity}", pos, vibrationIntensity); await ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, - ShockOscConfigManager.ConfigInstance.Behaviour.WhileBoneHeld == - ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction.Shock + _configManager.Config.Behaviour.WhileBoneHeld == + BehaviourConf.BoneHeldAction.Shock ? ControlType.Shock : ControlType.Vibrate); } @@ -566,7 +571,7 @@ await ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, if (programGroup.TriggerMethod == TriggerMethod.PhysBoneRelease) { - intensity = (byte)LerpFloat(config.IntensityRange.Min, config.IntensityRange.Max, + intensity = (byte)MathUtils.LerpFloat(config.IntensityRange.Min, config.IntensityRange.Max, programGroup.LastStretchValue); programGroup.LastStretchValue = 0; } @@ -578,7 +583,7 @@ await ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, private uint GetDuration() { - var config = ShockOscConfigManager.ConfigInstance.Behaviour; + var config = _configManager.Config.Behaviour; if (!config.RandomDuration) return config.FixedDuration; var rdr = config.DurationRange; @@ -590,7 +595,7 @@ private async Task ControlGroup(Guid groupId, uint duration, byte intensit { if(groupId == Guid.Empty) { - var controlCommandsAll = _config.OpenShock.Shockers + var controlCommandsAll = _configManager.Config.OpenShock.Shockers .Where(x => x.Value.Enabled) .Select(x => new Control { @@ -604,7 +609,7 @@ private async Task ControlGroup(Guid groupId, uint duration, byte intensit } - if (!ShockOscConfigManager.ConfigInstance.Groups.TryGetValue(groupId, out var group)) return false; + if (!_configManager.Config.Groups.TryGetValue(groupId, out var group)) return false; var controlCommands = group.Shockers.Select(x => new Control { @@ -644,9 +649,9 @@ private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log "Received remote {Type} for \"{ShockerName}\" at {Intensity}%:{Duration}s by {SenderCustomName} [{Sender}]", log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.CustomName, sender.Name); - var template = ShockOscConfigManager.ConfigInstance.Chatbox.Types[log.Type]; - if (ShockOscConfigManager.ConfigInstance.Osc.Chatbox && - ShockOscConfigManager.ConfigInstance.Chatbox.DisplayRemoteControl && template.Enabled) + var template = _configManager.Config.Chatbox.Types[log.Type]; + if (_configManager.Config.Osc.Chatbox && + _configManager.Config.Chatbox.DisplayRemoteControl && template.Enabled) { // Chatbox message remote var dat = new @@ -660,11 +665,11 @@ private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log }; var msg = - $"{ShockOscConfigManager.ConfigInstance.Chatbox.Prefix}{Smart.Format(sender.CustomName == null ? template.Remote : template.RemoteWithCustomName, dat)}"; + $"{_configManager.Config.Chatbox.Prefix}{Smart.Format(sender.CustomName == null ? template.Remote : template.RemoteWithCustomName, dat)}"; await _oscClient.SendChatboxMessage(msg); } - var configGroupsAffected = _config.Groups.Where(s => s.Value.Shockers.Any(x => x == log.Shocker.Id)).Select(x => x.Key).ToArray(); + var configGroupsAffected = _configManager.Config.Groups.Where(s => s.Value.Shockers.Any(x => x == log.Shocker.Id)).Select(x => x.Key).ToArray(); var programGroupsAffected = ProgramGroups.Where(x => configGroupsAffected.Contains(x.Key)).Select(x => x.Value); var oneShock = false; @@ -706,7 +711,7 @@ private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log private async Task ForceUnmute() { - if (!ShockOscConfigManager.ConfigInstance.Behaviour.ForceUnmute || !_isMuted) return; + if (!_configManager.Config.Behaviour.ForceUnmute || !_isMuted) return; _logger.LogDebug("Force unmuting..."); await _oscClient.SendGameMessage("/input/Voice", false); await Task.Delay(50); @@ -721,8 +726,5 @@ private Task CancelAction(ProgramGroup programGroup) return ControlGroup(programGroup.Id, 0, 0, ControlType.Stop); } - private static float LerpFloat(float min, float max, float t) => min + (max - min) * t; - public static float ClampFloat(float value) => value < 0 ? 0 : value > 1 ? 1 : value; - public static uint LerpUint(uint min, uint max, float t) => (uint)(min + (max - min) * t); - public static uint ClampUint(uint value, uint min, uint max) => value < min ? min : value > max ? max : value; + } \ No newline at end of file diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index a85e441..a5d079a 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -64,6 +64,7 @@ + diff --git a/ShockOsc/ShockOscConfigManager.cs b/ShockOsc/ShockOscConfigManager.cs deleted file mode 100644 index 53ca1db..0000000 --- a/ShockOsc/ShockOscConfigManager.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using OpenShock.SDK.CSharp.Models; -using OpenShock.ShockOsc.Models; -using Serilog; - -namespace OpenShock.ShockOsc; - -public static class ShockOscConfigManager -{ - private static readonly ILogger Logger = Log.ForContext(typeof(ShockOscConfigManager)); - private static ShockOscConfig? _internalConfig; - private static readonly string Path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\ShockOSC\config.json"; - - public static ShockOscConfig ConfigInstance => _internalConfig!; - - static ShockOscConfigManager() - { - TryLoad(); - } - - private static void TryLoad() - { - if (_internalConfig != null) return; - Logger.Information("Config file found, trying to load config from {Path}", Path); - if (File.Exists(Path)) - { - Logger.Verbose("Config file exists"); - var json = File.ReadAllText(Path); - if (!string.IsNullOrWhiteSpace(json)) - { - Logger.Verbose("Config file is not empty"); - try - { - _internalConfig = JsonSerializer.Deserialize(json, Options); - Logger.Information("Successfully loaded config"); - } - catch (JsonException e) - { - Logger.Fatal(e, "Error during deserialization/loading of config"); - Logger.Warning("Attempting to move old config and generate a new one"); - File.Move(Path, Path + ".old"); - Save(); - json = File.ReadAllText(Path); - _internalConfig = JsonSerializer.Deserialize(json, Options); - Logger.Information("Successfully loaded config"); - return; - } - } - } - - if (_internalConfig != null) return; - Logger.Information("No config file found (does not exist or empty), generating new one at {Path}", Path); - _internalConfig = GetDefaultConfig(); - Save(); - var jsonNew = File.ReadAllText(Path); - _internalConfig = JsonSerializer.Deserialize(jsonNew, Options); - Logger.Information("New configuration file generated! Please configure it!"); - } - - private static readonly JsonSerializerOptions Options = new() - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter() } - }; - - public static void Save() - { - Logger.Information("Saving config"); - try - { - var directory = System.IO.Path.GetDirectoryName(Path); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - Directory.CreateDirectory(directory); - - File.WriteAllText(Path, JsonSerializer.Serialize(_internalConfig, Options)); - } - catch (Exception e) - { - Logger.Error(e, "Error occurred while saving new config file"); - } - } - - private static ShockOscConfig GetDefaultConfig() => new() - { - Osc = new ShockOscConfig.OscConf - { - Chatbox = true, - Hoscy = false, - HoscySendPort = 9001, - QuestSupport = false, - OscQuery = true, - OscSendPort = 9000, - OscReceivePort = 9001 - }, - Chatbox = new ShockOscConfig.ChatboxConf - { - DisplayRemoteControl = true, - HoscyType = ShockOscConfig.ChatboxConf.HoscyMessageType.Message, - Prefix = null!, - Types = null!, - IgnoredKillSwitchActive = null!, - IgnoredAfk = null! - }, - Behaviour = new ShockOscConfig.BehaviourConf - { - RandomDuration = false, - RandomIntensity = false, - RandomDurationStep = 1000, - DurationRange = new JsonRange { Min = 1000, Max = 5000 }, - IntensityRange = new JsonRange { Min = 1, Max = 30 }, - FixedDuration = 2000, - FixedIntensity = 50, - CooldownTime = 5000, - HoldTime = 250, - WhileBoneHeld = ShockOscConfig.BehaviourConf.BoneHeldAction.Vibrate, - DisableWhileAfk = true, - ForceUnmute = false - }, - OpenShock = new ShockOscConfig.OpenShockConf - { - Shockers = new Dictionary(), - Token = "", - }, - Groups = new Dictionary() - }; - - public class ShockOscConfig - { - public required OscConf Osc { get; set; } - public required BehaviourConf Behaviour { get; set; } - public required OpenShockConf OpenShock { get; set; } - public required ChatboxConf Chatbox { get; set; } - - public IDictionary Groups { get; set; } = new Dictionary(); - - public Version? LastIgnoredVersion { get; set; } - - - public sealed class Group - { - public required string Name { get; set; } - public IList Shockers { get; set; } = new List(); - } - - - public class ChatboxConf - { - public string Prefix { get; set; } = "[ShockOsc] "; - public bool DisplayRemoteControl { get; set; } = true; - - [JsonConverter(typeof(JsonStringEnumConverter))] - public HoscyMessageType HoscyType { get; set; } = HoscyMessageType.Message; - - public string IgnoredKillSwitchActive { get; set; } = "Ignoring Shock, kill switch is active"; - public string IgnoredAfk { get; set; } = "Ignoring Shock, user is afk"; - - public IDictionary Types { get; set; } = - new Dictionary - { - { - ControlType.Stop, new ControlTypeConf - { - Enabled = true, - Local = "⏸ '{ShockerName}'", - Remote = "⏸ '{ShockerName}' by {Name}", - RemoteWithCustomName = "⏸ '{ShockerName}' by {CustomName} [{Name}]" - } - }, - { - ControlType.Shock, new ControlTypeConf - { - Enabled = true, - Local = "⚡ '{ShockerName}' {Intensity}%:{DurationSeconds}s", - Remote = "⚡ '{ShockerName}' {Intensity}%:{DurationSeconds}s by {Name}", - RemoteWithCustomName = - "⚡ '{ShockerName}' {Intensity}%:{DurationSeconds}s by {CustomName} [{Name}]" - } - }, - { - ControlType.Vibrate, new ControlTypeConf - { - Enabled = true, - Local = "〜 '{ShockerName}' {Intensity}%:{DurationSeconds}s", - Remote = "〜 '{ShockerName}' {Intensity}%:{DurationSeconds}s by {Name}", - RemoteWithCustomName = - "〜 '{ShockerName}' {Intensity}%:{DurationSeconds}s by {CustomName} [{Name}]" - } - }, - { - ControlType.Sound, new ControlTypeConf - { - Enabled = true, - Local = "🔈 '{ShockerName}' {Intensity}%:{DurationSeconds}s", - Remote = "🔈 '{ShockerName}' {Intensity}%:{DurationSeconds}s by {Name}", - RemoteWithCustomName = - "🔈 '{ShockerName}' {Intensity}%:{DurationSeconds}s by {CustomName} [{Name}]" - } - } - }; - - public class ControlTypeConf - { - public required bool Enabled { get; set; } - public required string Local { get; set; } - public required string Remote { get; set; } - public required string RemoteWithCustomName { get; set; } - } - - public enum HoscyMessageType - { - Message, - Notification - } - } - - public class OscConf - { - public required bool Chatbox { get; set; } - public required bool Hoscy { get; set; } - public ushort HoscySendPort { get; set; } = 9001; - public required bool QuestSupport { get; set; } - public required bool OscQuery { get; set; } - public ushort OscSendPort { get; set; } = 9000; - public ushort OscReceivePort { get; set; } = 9001; - } - - public class BehaviourConf - { - public required bool RandomIntensity { get; set; } - public required bool RandomDuration { get; set; } - public required uint RandomDurationStep { get; set; } = 1000; - public required JsonRange DurationRange { get; set; } - public required JsonRange IntensityRange { get; set; } - public required byte FixedIntensity { get; set; } - public required uint FixedDuration { get; set; } - public required uint HoldTime { get; set; } - public required uint CooldownTime { get; set; } - public BoneHeldAction WhileBoneHeld { get; set; } = BoneHeldAction.Vibrate; - public bool DisableWhileAfk { get; set; } = true; - public bool ForceUnmute { get; set; } = false; - - public enum BoneHeldAction - { - Vibrate = 0, - Shock = 1, - None = 2 - } - } - - public class OpenShockConf - { - public Uri Backend { get; set; } = new("https://api.shocklink.net"); - public required string Token { get; set; } - public required IReadOnlyDictionary Shockers { get; set; } - } - - public class ShockerConf - { - public required bool Enabled { get; set; } = true; - } - } -} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index bec758f..34f2e52 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,11 +1,10 @@ @using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Layout @using OpenShock.ShockOsc.Ui.Components.Tabs -@using Serilog @using OpenShock.ShockOsc.Ui.Utils +@using OpenShock.ShockOsc.Config @inject ISnackbar Snackbar @inherits LayoutComponentBase -@inject ShockOscConfigManager.ShockOscConfig Config @page "/main" @@ -13,42 +12,37 @@ -@code { - private static readonly ILogger Logger = Log.ForContext(typeof(MainLayout)); - - -} - - - - - @* - *@ - - + + @* *@ + @* *@ + @* Groups *@ + @* *@ + @* *@ + @* *@ + @* @switch(_currentTab) *@ + @* { *@ + @* case Tab.Groups: *@ + @* *@ + @* break; *@ + @* case Tab.Config: *@ + @* *@ + @* break; *@ + @* case Tab.Shockers: *@ + @* *@ + @* break; *@ + @* case Tab.Debug: *@ + @* *@ + @* break; *@ + @* case Tab.Logs: *@ + @* *@ + @* break; *@ + @* } *@ + - + @@ -70,7 +64,15 @@ @code { - private int activePageIndex = 0; + + public enum Tab { Groups, Config, Shockers, Debug, Logs } + + private Tab _currentTab = Tab.Groups; + + private void NavigateTab(Tab tab) + { + _currentTab = tab; + } private void MsgNoty(string msg, Severity severity) { diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor index 59cadaf..e5c41a2 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -1,14 +1,18 @@ -@inject ShockOscConfigManager.ShockOscConfig Config -@using OpenShock.SDK.CSharp.Models +@using OpenShock.SDK.CSharp.Models +@using System.Reactive.Subjects +@using OpenShock.ShockOsc.Utils +@using System.Reactive.Linq +@using OpenShock.ShockOsc.Config @implements IDisposable @inject UnderscoreConfig underscoreConfig +@inject ConfigManager ConfigManager Shocker Options - - @foreach (ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.BehaviourConf.BoneHeldAction))) + + @foreach (BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(BehaviourConf.BoneHeldAction))) { @boneHeldAction } @@ -16,8 +20,8 @@

- - + +
@@ -26,13 +30,13 @@
@if (intensity == "Fixed Intensity") { - Intensity: @Config.Behaviour.FixedIntensity.ToString()% + Intensity: @FixedIntensity.ToString()% } else { - Intensity Min: @Config.Behaviour.IntensityRange.Min.ToString()% + Intensity Min: @IntensityMin.ToString()%
- Intensity Max: @Config.Behaviour.IntensityRange.Max.ToString()% + Intensity Max: @IntensityMax.ToString()% } @@ -41,13 +45,13 @@
@if (duration == "Fixed Duration") { - + } else { - - - + + + }

@@ -57,22 +61,22 @@ Game Options - - + +
Chatbox Options - - + +

- - + + - - @foreach (ShockOscConfigManager.ShockOscConfig.ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(ShockOscConfigManager.ShockOscConfig.ChatboxConf.HoscyMessageType))) + + @foreach (ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(ChatboxConf.HoscyMessageType))) { @hoscyMessageType } @@ -91,18 +95,18 @@ } - - - + + +
@foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) { - - - - + + + + } @@ -113,82 +117,102 @@ OSC Options - - - @if (!Config.Osc.OscQuery) + + + @if (!ConfigManager.Config.Osc.OscQuery) {
- - + + }
@code { private bool _advancedSettingsExpanded = false; - - private string intensity = "Fixed Intensity"; - private string duration = "Fixed Duration"; - private DateTime _lastSettingsSave = DateTime.Now; - private CancellationTokenSource _cts = new CancellationTokenSource(); - - private void OnAdvancedSettingsClick() + private BehaviorSubject _fixedIntensitySubject = null!; + + private byte FixedIntensity { - _advancedSettingsExpanded = !_advancedSettingsExpanded; + get => _fixedIntensitySubject.Value; + set => _fixedIntensitySubject.OnNext(value); } - private async Task OnSliderSettingsValueChange() + private BehaviorSubject _intensityMinSubject = null!; + + private byte IntensityMin { - if ((DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) + get => _intensityMinSubject.Value; + set { - await OnSettingsValueChange(); - return; + if (value > IntensityMax) IntensityMax = value; + _intensityMinSubject.OnNext(value); } + } - _cts.Cancel(); - _cts = new CancellationTokenSource(); - - try - { - await Task.Delay(10, _cts.Token); + private BehaviorSubject _intensityMaxSubject = null!; - if (!_cts.Token.IsCancellationRequested && - (DateTime.Now - _lastSettingsSave).TotalMilliseconds >= 10) - { - await OnSettingsValueChange(); - } - } - catch (TaskCanceledException) + private byte IntensityMax + { + get => _intensityMaxSubject.Value; + set { - // Task was cancelled, which is expected + if (value < IntensityMin) IntensityMin = value; + _intensityMaxSubject.OnNext(value); } } + + private string intensity = "Fixed Intensity"; + private string duration = "Fixed Duration"; + + private DateTime _lastSettingsSave = DateTime.Now; + private CancellationTokenSource _cts = new CancellationTokenSource(); + + private void OnAdvancedSettingsClick() + { + _advancedSettingsExpanded = !_advancedSettingsExpanded; + } + private async Task OnSettingsValueChange() { _lastSettingsSave = DateTime.Now; - Config.Behaviour.RandomIntensity = intensity == "Random Intensity"; - Config.Behaviour.RandomDuration = duration == "Random Duration"; - ValidateSettings(); + ConfigManager.Config.Behaviour.RandomIntensity = intensity == "Random Intensity"; + ConfigManager.Config.Behaviour.RandomDuration = duration == "Random Duration"; - ShockOscConfigManager.Save(); + await ConfigManager.SaveAsync(); await underscoreConfig.SendUpdateForAll(); } - - private void ValidateSettings() - { - if (Config.Behaviour.IntensityRange.Min > Config.Behaviour.IntensityRange.Max) Config.Behaviour.IntensityRange.Max = Config.Behaviour.IntensityRange.Min; - } - + protected override void OnInitialized() { - ShockOsc.OnConfigUpdate = OnConfigUpdate; + + intensity = ConfigManager.Config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; + duration = ConfigManager.Config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; + + _fixedIntensitySubject = new BehaviorSubject(ConfigManager.Config.Behaviour.FixedIntensity); + _fixedIntensitySubject.Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(b => + { + ConfigManager.Config.Behaviour.FixedIntensity = b; + OsTask.Run(OnSettingsValueChange); + }); - intensity = Config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; - duration = Config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; + _intensityMinSubject = new BehaviorSubject((byte)ConfigManager.Config.Behaviour.IntensityRange.Min); + _intensityMinSubject.Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(b => + { + ConfigManager.Config.Behaviour.IntensityRange.Min = b; + OsTask.Run(OnSettingsValueChange); + }); + + _intensityMaxSubject = new BehaviorSubject((byte)ConfigManager.Config.Behaviour.IntensityRange.Max); + _intensityMaxSubject.Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(b => + { + ConfigManager.Config.Behaviour.IntensityRange.Max = b; + OsTask.Run(OnSettingsValueChange); + }); } - + private void OnConfigUpdate() { InvokeAsync(StateHasChanged); diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index 399af0b..ae8bc24 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -1,7 +1,9 @@ @using System.Text.RegularExpressions @using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Config @inject OpenShockApi OpenShockApi @inject ShockOsc ShockOsc +@inject ConfigManager ConfigManager @code { @@ -11,11 +13,11 @@ public async Task AddGroup() { var groupId = Guid.NewGuid(); - ShockOscConfigManager.ConfigInstance.Groups.Add(groupId, new ShockOscConfigManager.ShockOscConfig.Group { Name = "New Group" }); + ConfigManager.Config.Groups.Add(groupId, new ShockOscConfig.Group { Name = "New Group" }); Group = groupId; await InvokeAsync(StateHasChanged); - ShockOscConfigManager.Save(); + await ConfigManager.SaveAsync(); await ShockOsc.RaiseOnGroupsChanged(); } @@ -23,39 +25,36 @@ { if (Group == null) return; - ShockOscConfigManager.ConfigInstance.Groups.Remove(Group.Value); + ConfigManager.Config.Groups.Remove(Group.Value); Group = null; await InvokeAsync(StateHasChanged); - ShockOscConfigManager.Save(); + await ConfigManager.SaveAsync(); await ShockOsc.RaiseOnGroupsChanged(); } private async Task OnSettingsValueChange() { - ShockOscConfigManager.Save(); + await ConfigManager.SaveAsync(); await ShockOsc.RaiseOnGroupsChanged(); } - private Task OnGroupSelect() + private async Task OnGroupSelect() { if (CurrentGroup != null) _selectedShockers = [..CurrentGroup.Shockers]; - InvokeAsync(StateHasChanged); - return Task.CompletedTask; + await InvokeAsync(StateHasChanged); } - private Task OnSelectedShockersUpdate() + private async Task OnSelectedShockersUpdate() { if (CurrentGroup != null) { CurrentGroup.Shockers = _selectedShockers.ToList(); - ShockOscConfigManager.Save(); + await ConfigManager.SaveAsync(); } - - return Task.CompletedTask; } - private ShockOscConfigManager.ShockOscConfig.Group? CurrentGroup => Group == null ? null : ShockOscConfigManager.ConfigInstance.Groups.TryGetValue(Group.Value, out var group) ? group : null; + private ShockOscConfig.Group? CurrentGroup => Group == null ? null : ConfigManager.Config.Groups.TryGetValue(Group.Value, out var group) ? group : null; private static Regex _nameRegex = new Regex(@"^[a-zA-Z0-9\/\\ -]+$", RegexOptions.Compiled); @@ -74,7 +73,7 @@ Delete Group

- @foreach (var group in ShockOscConfigManager.ConfigInstance.Groups) + @foreach (var group in ConfigManager.Config.Groups) { @group.Value.Name } diff --git a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor b/ShockOsc/Ui/Components/Tabs/ShockersTab.razor index de141af..4d46260 100644 --- a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ShockersTab.razor @@ -1,6 +1,7 @@ @using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Config @inject OpenShockApi OpenShockApi -@inject ShockOscConfigManager.ShockOscConfig Config +@inject ConfigManager ConfigManager Refresh
@@ -13,7 +14,7 @@ - + @context.Name @context.Id @@ -21,8 +22,8 @@ @code { - private void OnShockerConfigUpdate() + private Task OnShockerConfigUpdate() { - ShockOscConfigManager.Save(); + return ConfigManager.SaveAsync(); } } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/UpdateDialog.razor b/ShockOsc/Ui/Components/UpdateDialog.razor index c1b4dfb..3b9e7f6 100644 --- a/ShockOsc/Ui/Components/UpdateDialog.razor +++ b/ShockOsc/Ui/Components/UpdateDialog.razor @@ -1,20 +1,25 @@ +@using OpenShock.ShockOsc.Config +@using OpenShock.ShockOsc.Utils +@inject ConfigManager ConfigManager +@inject Updater Updater + @code { [CascadingParameter] MudDialogInstance MudDialog { get; set; } private bool _isDownloading = false; - private void Skip() + private async Task Skip() { - ShockOscConfigManager.ConfigInstance.LastIgnoredVersion = Updater.LatestVersion; - ShockOscConfigManager.Save(); + ConfigManager.Config.LastIgnoredVersion = Updater.LatestVersion; + await ConfigManager.SaveAsync(); MudDialog.Close(DialogResult.Ok(true)); } private void DownloadUpdate() { _isDownloading = true; - Updater.DoUpdate(); + OsTask.Run(Updater.DoUpdate); } } diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Components/UpdateLogout.razor index c8fc3f9..2f75e8d 100644 --- a/ShockOsc/Ui/Components/UpdateLogout.razor +++ b/ShockOsc/Ui/Components/UpdateLogout.razor @@ -1,23 +1,23 @@ +@using OpenShock.ShockOsc.Config @inject IDialogService Dialog @inject IDialogService DialogService +@inject ConfigManager ConfigManager +@inject Updater Updater @code { [Parameter] public bool Authenticated { get; set; } - private DialogOptions dialogOptions = new DialogOptions() { NoHeader = true, DisableBackdropClick = true }; + private readonly DialogOptions _dialogOptions = new() { NoHeader = true, DisableBackdropClick = true }; private void OpenUpdateDialog() { - DialogService.Show("Update", dialogOptions); + DialogService.Show("Update", _dialogOptions); } protected override async Task OnInitializedAsync() { - if (await Updater.CheckUpdate()) - { - OpenUpdateDialog(); - } + if (await Updater.CheckUpdate()) OpenUpdateDialog(); } } @@ -33,7 +33,7 @@ @if (Authenticated) { - + diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index 5527776..a5c5026 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -1,14 +1,17 @@ @using OpenShock.ShockOsc.Ui.Utils @using OpenShock.SDK.CSharp.Live @using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Config @using OpenShock.ShockOsc.Ui.Components.Layout +@using Microsoft.Extensions.Logging @inherits LayoutComponentBase -@inject ShockOscConfigManager.ShockOscConfig ShockOscConfig +@inject ConfigManager ConfigManager @inject NavigationManager NavigationManager @inject BackendLiveApiManager LiveApiManager @inject OpenShockApiLiveClient LiveApiClient @inject OpenShockApi ApiClient +@inject ILogger Logger @page "/" @@ -24,7 +27,7 @@
@if (!Loading) { - +
Continue
@@ -41,27 +44,30 @@ protected override async Task OnInitializedAsync() { - if(string.IsNullOrEmpty(ShockOscConfig.OpenShock.Token)) return; + if(string.IsNullOrEmpty(ConfigManager.Config.OpenShock.Token)) return; await ProceedAuthenticated(); } public async Task Login() { - ShockOscConfigManager.Save(); + await ConfigManager.SaveAsync(); ApiClient.SetupApiClient(); - await LiveApiManager.SetupLiveClient(); await ProceedAuthenticated(); } private async Task ProceedAuthenticated() { Loading = true; - + + Logger.LogInformation("Setting up live client"); await LiveApiManager.SetupLiveClient(); + Logger.LogInformation("Starting live client"); await LiveApiClient.StartAsync(); - await ApiClient.RefreshShockers(); + Logger.LogInformation("Refreshing shockers"); + await ApiClient.RefreshShockers(); + NavigationManager.NavigateTo("main"); } } \ No newline at end of file diff --git a/ShockOsc/UnderscoreConfig.cs b/ShockOsc/UnderscoreConfig.cs index 8e85bda..09a3c55 100644 --- a/ShockOsc/UnderscoreConfig.cs +++ b/ShockOsc/UnderscoreConfig.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; -using Serilog; +using OpenShock.ShockOsc.Config; +using OpenShock.ShockOsc.Utils; namespace OpenShock.ShockOsc; @@ -7,11 +8,13 @@ public sealed class UnderscoreConfig { private readonly ILogger _logger; private readonly OscClient _oscClient; + private readonly ConfigManager _configManager; - public UnderscoreConfig(ILogger logger, OscClient oscClient) + public UnderscoreConfig(ILogger logger, OscClient oscClient, ConfigManager configManager) { _logger = logger; _oscClient = oscClient; + _configManager = configManager; } public bool KillSwitch { get; set; } = false; @@ -49,12 +52,12 @@ public void HandleCommand(string parameterName, object?[] arguments) // 0..100% if (value is float minIntensityFloat) { - var currentMinIntensity = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min / 100f); + var currentMinIntensity = MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f); if (minIntensityFloat == currentMinIntensity) return; - ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min = ShockOsc.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); + _configManager.Config.Behaviour.IntensityRange.Min = MathUtils.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); ValidateSettings(); - ShockOscConfigManager.Save(); + _configManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -63,12 +66,12 @@ public void HandleCommand(string parameterName, object?[] arguments) // 0..100% if (value is float maxIntensityFloat) { - var currentMaxIntensity = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max / 100f); + var currentMaxIntensity = MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f); if (maxIntensityFloat == currentMaxIntensity) return; - ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max = ShockOsc.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); + _configManager.Config.Behaviour.IntensityRange.Max = MathUtils.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); ValidateSettings(); - ShockOscConfigManager.Save(); + _configManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -77,12 +80,12 @@ public void HandleCommand(string parameterName, object?[] arguments) // 0..10sec if (value is float durationFloat) { - var currentDuration = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.FixedDuration / 10000f); + var currentDuration = MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f); if (durationFloat == currentDuration) return; - ShockOscConfigManager.ConfigInstance.Behaviour.FixedDuration = ShockOsc.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); + _configManager.Config.Behaviour.FixedDuration = MathUtils.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); ValidateSettings(); - ShockOscConfigManager.Save(); + _configManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -91,12 +94,12 @@ public void HandleCommand(string parameterName, object?[] arguments) // 0..100sec if (value is float cooldownTimeFloat) { - var currentCooldownTime = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime / 100000f); + var currentCooldownTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f); if (cooldownTimeFloat == currentCooldownTime) return; - ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime = ShockOsc.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); + _configManager.Config.Behaviour.CooldownTime = MathUtils.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); ValidateSettings(); - ShockOscConfigManager.Save(); + _configManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -106,12 +109,12 @@ public void HandleCommand(string parameterName, object?[] arguments) // 0..1sec if (value is float holdTimeFloat) { - var currentHoldTime = ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.HoldTime / 1000f); + var currentHoldTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f); if (holdTimeFloat == currentHoldTime) return; - ShockOscConfigManager.ConfigInstance.Behaviour.HoldTime = ShockOsc.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); + _configManager.Config.Behaviour.HoldTime = MathUtils.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); ValidateSettings(); - ShockOscConfigManager.Save(); + _configManager.Save(); ShockOsc.OnConfigUpdate?.Invoke(); // update Ui } break; @@ -134,19 +137,19 @@ public void HandleCommand(string parameterName, object?[] arguments) private void ValidateSettings() { - if (ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min > ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max) - { - ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max = ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min; - } + var intensityRange = _configManager.Config.Behaviour.IntensityRange; + if (intensityRange.Min > intensityRange.Max) intensityRange.Max = intensityRange.Min; + if(intensityRange.Max < intensityRange.Min) intensityRange.Min = intensityRange.Max; + } public async Task SendUpdateForAll() { await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/Paused", KillSwitch); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Min / 100f)); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.IntensityRange.Max / 100f)); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.FixedDuration / 10000f)); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.CooldownTime / 100000f)); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", ShockOsc.ClampFloat(ShockOscConfigManager.ConfigInstance.Behaviour.HoldTime / 1000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f)); } } \ No newline at end of file diff --git a/ShockOsc/Updater.cs b/ShockOsc/Updater.cs index 479ecf9..688d66c 100644 --- a/ShockOsc/Updater.cs +++ b/ShockOsc/Updater.cs @@ -1,27 +1,36 @@ using System.Diagnostics; using System.Reflection; using System.Text.Json; +using Microsoft.Extensions.Logging; +using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; using Serilog; namespace OpenShock.ShockOsc; -public static class Updater +public sealed class Updater { - private static readonly ILogger Logger = Log.ForContext(typeof(Updater)); - private static readonly HttpClient HttpClient = new(); private const string GithubLatest = "https://api.github.com/repos/OpenShock/ShockOsc/releases/latest"; private const string SetupFileName = "ShockOSC_Setup.exe"; // OpenShock.ShockOsc.exe - private static readonly string SetupFilePath = Path.Combine(Environment.CurrentDirectory, SetupFileName); - private static readonly Version CurrentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? throw new Exception("Could not determine ShockOsc version"); + + private static readonly HttpClient HttpClient = new(); + + private readonly string _setupFilePath = Path.Combine(Environment.CurrentDirectory, SetupFileName); + private readonly Version _currentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? throw new Exception("Could not determine ShockOsc version"); + private Uri? LatestDownloadUrl { get; set; } + + private readonly ILogger _logger; + private readonly ConfigManager _configManager; - public static bool UpdateAvailable { get; private set; } - public static Version? LatestVersion { get; private set; } - public static Uri? LatestDownloadUrl { get; private set; } + public bool UpdateAvailable { get; private set; } + public Version? LatestVersion { get; private set; } + - static Updater() + public Updater(ILogger logger, ConfigManager configManager) { - HttpClient.DefaultRequestHeaders.Add("User-Agent", $"ShockOsc/{CurrentVersion}"); + _logger = logger; + _configManager = configManager; + HttpClient.DefaultRequestHeaders.Add("User-Agent", $"ShockOsc/{_currentVersion}"); } private static bool TryDeleteFile(string fileName) @@ -38,36 +47,36 @@ private static bool TryDeleteFile(string fileName) } } - private static async Task<(Version, GithubReleaseResponse.Asset)?> GetLatestRelease() + private async Task<(Version, GithubReleaseResponse.Asset)?> GetLatestRelease() { - Logger.Information("Checking GitHub for updates..."); + _logger.LogInformation("Checking GitHub for updates..."); try { var res = await HttpClient.GetAsync(GithubLatest); if (!res.IsSuccessStatusCode) { - Logger.Warning("Failed to get latest version information from GitHub. {StatusCode}", res.StatusCode); + _logger.LogWarning("Failed to get latest version information from GitHub. {StatusCode}", res.StatusCode); return null; } var json = await JsonSerializer.DeserializeAsync(await res.Content.ReadAsStreamAsync()); if (json == null) { - Logger.Warning("Could not deserialize json"); + _logger.LogWarning("Could not deserialize json"); return null; } if (!Version.TryParse(json.TagName[1..], out var version)) { - Logger.Warning("Failed to parse version. Value: {Version}", json.TagName); + _logger.LogWarning("Failed to parse version. Value: {Version}", json.TagName); return null; } var asset = json.Assets.FirstOrDefault(x => x.Name == SetupFileName); if (asset == null) { - Logger.Warning("Could not find asset with {@SetupName}. Assets found: {@Assets}", SetupFileName, json.Assets); + _logger.LogWarning("Could not find asset with {@SetupName}. Assets found: {@Assets}", SetupFileName, json.Assets); return null; } @@ -75,18 +84,18 @@ private static bool TryDeleteFile(string fileName) } catch (Exception e) { - Logger.Warning(e, "Failed to get latest version information from GitHub"); + _logger.LogWarning(e, "Failed to get latest version information from GitHub"); return null; } } - public static async Task CheckUpdate() + public async Task CheckUpdate() { var latestVersion = await GetLatestRelease(); if (latestVersion is null) return false; - if (latestVersion.Value.Item1 <= CurrentVersion) + if (latestVersion.Value.Item1 <= _currentVersion) { - Logger.Information("ShockOsc is up to date ([{Version}] >= [{LatestVersion}])", CurrentVersion, latestVersion.Value.Item1); + _logger.LogInformation("ShockOsc is up to date ([{Version}] >= [{LatestVersion}])", _currentVersion, latestVersion.Value.Item1); UpdateAvailable = false; return false; } @@ -94,45 +103,45 @@ public static async Task CheckUpdate() UpdateAvailable = true; LatestVersion = latestVersion.Value.Item1; LatestDownloadUrl = latestVersion.Value.Item2.BrowserDownloadUrl; - if (ShockOscConfigManager.ConfigInstance.LastIgnoredVersion != null && - ShockOscConfigManager.ConfigInstance.LastIgnoredVersion >= latestVersion.Value.Item1) + if (_configManager.Config.LastIgnoredVersion != null && + _configManager.Config.LastIgnoredVersion >= latestVersion.Value.Item1) { - Logger.Information("ShockOsc is not up to date. Skipping update due to previous postpone. You can reenable the updater by setting 'LastIgnoredVersion' to null"); + _logger.LogInformation("ShockOsc is not up to date. Skipping update due to previous postpone. You can reenable the updater by setting 'LastIgnoredVersion' to null"); return false; } - Logger.Warning( + _logger.LogWarning( "ShockOsc is not up to date. Newest version is [{NewVersion}] but you are on [{CurrentVersion}]!", - latestVersion.Value.Item1, CurrentVersion); + latestVersion.Value.Item1, _currentVersion); return true; } - public static async Task DoUpdate() + public async Task DoUpdate() { - Logger.Information("Starting update..."); + _logger.LogInformation("Starting update..."); if (LatestVersion == null || LatestDownloadUrl == null) { - Logger.Error("LatestVersion or LatestDownloadUrl is null. Cannot update"); + _logger.LogError("LatestVersion or LatestDownloadUrl is null. Cannot update"); return; } - TryDeleteFile(SetupFilePath); + TryDeleteFile(_setupFilePath); - Logger.Debug("Downloading new release..."); + _logger.LogDebug("Downloading new release..."); var sp = Stopwatch.StartNew(); await using (var stream = await HttpClient.GetStreamAsync(LatestDownloadUrl)) { - await using var fStream = new FileStream(SetupFilePath, FileMode.OpenOrCreate); + await using var fStream = new FileStream(_setupFilePath, FileMode.OpenOrCreate); await stream.CopyToAsync(fStream); } - Logger.Debug("Downloaded file within {TimeTook}ms", sp.ElapsedMilliseconds); - Logger.Information("Download complete, now restarting to newer application in one second"); + _logger.LogDebug("Downloaded file within {TimeTook}ms", sp.ElapsedMilliseconds); + _logger.LogInformation("Download complete, now restarting to newer application in one second"); await Task.Delay(1000); var startInfo = new ProcessStartInfo { - FileName = SetupFilePath, + FileName = _setupFilePath, UseShellExecute = true }; Process.Start(startInfo); diff --git a/ShockOsc/Ui/Utils/AsynchronousEventExtensions.cs b/ShockOsc/Utils/AsynchronousEventExtensions.cs similarity index 77% rename from ShockOsc/Ui/Utils/AsynchronousEventExtensions.cs rename to ShockOsc/Utils/AsynchronousEventExtensions.cs index 8479ba7..387589f 100644 --- a/ShockOsc/Ui/Utils/AsynchronousEventExtensions.cs +++ b/ShockOsc/Utils/AsynchronousEventExtensions.cs @@ -1,4 +1,7 @@ -namespace OpenShock.ShockOsc.Ui.Utils; +using System.Net; +using OpenShock.ShockOsc.OscQueryLibrary; + +namespace OpenShock.ShockOsc.Utils; public static class AsynchronousEventExtensions { diff --git a/ShockOsc/Utils/MathUtils.cs b/ShockOsc/Utils/MathUtils.cs new file mode 100644 index 0000000..aa5f6fa --- /dev/null +++ b/ShockOsc/Utils/MathUtils.cs @@ -0,0 +1,9 @@ +namespace OpenShock.ShockOsc.Utils; + +public static class MathUtils +{ + public static float LerpFloat(float min, float max, float t) => min + (max - min) * t; + public static float ClampFloat(float value) => value < 0 ? 0 : value > 1 ? 1 : value; + public static uint LerpUint(uint min, uint max, float t) => (uint)(min + (max - min) * t); + public static uint ClampUint(uint value, uint min, uint max) => value < min ? min : value > max ? max : value; +} \ No newline at end of file From 5d93214222441daa4290c09b829cc615e582a0c1 Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 9 Apr 2024 02:38:40 +0200 Subject: [PATCH 26/95] Fix avatar change not being handled properly --- ShockOsc/MauiProgram.cs | 7 +-- ShockOsc/OscQueryLibrary/OscQueryServer.cs | 4 +- ShockOsc/ShockOsc.cs | 60 ++++++++++----------- ShockOsc/ShockOscData.cs | 9 ++++ ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 6 ++- ShockOsc/Ui/Components/Tabs/DebugTab.razor | 1 + ShockOsc/UnderscoreConfig.cs | 20 ++++--- 7 files changed, 59 insertions(+), 48 deletions(-) create mode 100644 ShockOsc/ShockOscData.cs diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 4984b43..3f016c0 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -45,8 +45,9 @@ public static MauiApp CreateMauiApp() Log.Logger = loggerConfiguration.CreateLogger(); builder.Services.AddSerilog(Log.Logger); - - + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -66,8 +67,8 @@ public static MauiApp CreateMauiApp() return new OscQueryServer("ShockOsc", listenAddress, config); }); - builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddMudServices(); builder.Services.AddMauiBlazorWebView(); diff --git a/ShockOsc/OscQueryLibrary/OscQueryServer.cs b/ShockOsc/OscQueryLibrary/OscQueryServer.cs index 9acb469..1757f5a 100644 --- a/ShockOsc/OscQueryLibrary/OscQueryServer.cs +++ b/ShockOsc/OscQueryLibrary/OscQueryServer.cs @@ -37,7 +37,7 @@ public class OscQueryServer : IDisposable public event Func? FoundVrcClient; - private event Func, string, Task>? ParameterUpdate; + public event Func, string, Task>? ParameterUpdate; private readonly Dictionary ParameterList = new(); @@ -224,7 +224,7 @@ private async Task FetchJsonFromVrc(IPAddress ipAddress, int port) } avatarId = rootNode.CONTENTS.avatar.CONTENTS.change.VALUE?[0]?.ToString() ?? string.Empty; - ParameterUpdate?.Raise(ParameterList, avatarId); + if(ParameterUpdate != null) await ParameterUpdate.Raise(ParameterList, avatarId); } catch (HttpRequestException ex) { diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index 391cd40..fbaf904 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -26,13 +26,13 @@ public sealed class ShockOsc private readonly UnderscoreConfig _underscoreConfig; private readonly ConfigManager _configManager; private readonly OscQueryServer _oscQueryServer; + private readonly ShockOscData _dataLayer; - private static bool _oscServerActive; - private static bool _isAfk; - private static bool _isMuted; - public static string AvatarId = string.Empty; - private static readonly Random Random = new(); - public static readonly ConcurrentDictionary ProgramGroups = new(); + private bool _oscServerActive; + private bool _isAfk; + private bool _isMuted; + public string AvatarId = string.Empty; + private readonly Random Random = new(); public event Func? OnGroupsChanged; @@ -48,22 +48,21 @@ public sealed class ShockOsc "IShock" }; - public static Dictionary ShockOscParams = new(); - public static Dictionary AllAvatarParams = new(); + public readonly Dictionary ShockOscParams = new(); + public readonly Dictionary AllAvatarParams = new(); - public static Action? OnParamsChange; - public static Action? OnConfigUpdate; + public Action? OnParamsChange; private readonly ChangeTrackedOscParam _paramAnyActive; private readonly ChangeTrackedOscParam _paramAnyCooldown; private readonly ChangeTrackedOscParam _paramAnyCooldownPercentage; private readonly ChangeTrackedOscParam _paramAnyIntensity; - private string connectionId = string.Empty; + private string _liveConnectionId = string.Empty; public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi openShockApi, OpenShockApiLiveClient liveClient, UnderscoreConfig underscoreConfig, - ConfigManager configManager, OscQueryServer oscQueryServer) + ConfigManager configManager, OscQueryServer oscQueryServer, ShockOscData dataLayer) { _logger = logger; _oscClient = oscClient; @@ -71,6 +70,7 @@ public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi open _underscoreConfig = underscoreConfig; _configManager = configManager; _oscQueryServer = oscQueryServer; + _dataLayer = dataLayer; _paramAnyActive = new ChangeTrackedOscParam("_Any", "_Active", false, _oscClient); _paramAnyCooldown = new ChangeTrackedOscParam("_Any", "_Cooldown", false, _oscClient); @@ -79,7 +79,7 @@ public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi open liveClient.OnWelcome += s => { - connectionId = s; + _liveConnectionId = s; return Task.CompletedTask; }; @@ -88,6 +88,7 @@ public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi open OnGroupsChanged += SetupGroups; oscQueryServer.FoundVrcClient += FoundVrcClient; + oscQueryServer.ParameterUpdate += OnAvatarChange; SetupGroups().Wait(); @@ -101,14 +102,14 @@ public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi open private async Task SetupGroups() { - ProgramGroups.Clear(); - ProgramGroups[Guid.Empty] = new ProgramGroup(Guid.Empty, "_All", _oscClient); - foreach (var (id, group) in _configManager.Config.Groups) ProgramGroups[id] = new ProgramGroup(id, group.Name, _oscClient); + _dataLayer.ProgramGroups.Clear(); + _dataLayer.ProgramGroups[Guid.Empty] = new ProgramGroup(Guid.Empty, "_All", _oscClient); + foreach (var (id, group) in _configManager.Config.Groups) _dataLayer.ProgramGroups[id] = new ProgramGroup(id, group.Name, _oscClient); } public Task RaiseOnGroupsChanged() => OnGroupsChanged.Raise(); - private static void OnParamChange(bool shockOscParam) + private void OnParamChange(bool shockOscParam) { OnParamsChange?.Invoke(shockOscParam); } @@ -142,24 +143,18 @@ public async Task FoundVrcClient(IPEndPoint? oscClient) OsTask.Run(_underscoreConfig.SendUpdateForAll); } - public void OnAvatarChange(Dictionary? parameters, string avatarId) + public async Task OnAvatarChange(Dictionary parameters, string avatarId) { AvatarId = avatarId; try { - foreach (var obj in ProgramGroups) + foreach (var obj in _dataLayer.ProgramGroups) { obj.Value.Reset(); } var parameterCount = 0; - if (parameters == null) - { - _logger.LogError("Failed to receive avatar parameters"); - return; - } - ShockOscParams.Clear(); AllAvatarParams.Clear(); @@ -184,7 +179,7 @@ public void OnAvatarChange(Dictionary? parameters, string avata parameterCount++; ShockOscParams.TryAdd(param[28..], parameters[param]); - if (!ProgramGroups.Any(x => + if (!_dataLayer.ProgramGroups.Any(x => x.Value.Name.Equals(shockerName, StringComparison.InvariantCultureIgnoreCase)) && !shockerName.StartsWith('_')) { @@ -233,7 +228,6 @@ private async Task ReceiveLogic() } var addr = received.Address; - _logger.LogTrace("Received message: {Addr}", addr); if (addr.StartsWith("/avatar/parameters/")) { @@ -296,7 +290,7 @@ private async Task ReceiveLogic() if (!ShockerParams.Contains(action)) return; - if (!ProgramGroups.Any(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase))) + if (!_dataLayer.ProgramGroups.Any(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase))) { if (groupName == "_Any") return; _logger.LogWarning("Unknown group {GroupName}", groupName); @@ -304,7 +298,7 @@ private async Task ReceiveLogic() return; } - var programGroup = ProgramGroups + var programGroup = _dataLayer.ProgramGroups .First(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)).Value; var value = received.Arguments.ElementAtOrDefault(0); @@ -447,7 +441,7 @@ private async Task SendParams() var anyCooldownPercentage = 0f; var anyIntensity = 0f; - foreach (var shocker in ProgramGroups.Values) + foreach (var shocker in _dataLayer.ProgramGroups.Values) { var isActive = shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; var isActiveOrOnCooldown = @@ -513,7 +507,7 @@ private byte GetIntensity() private async Task CheckLogic() { var config = _configManager.Config.Behaviour; - foreach (var (pos, programGroup) in ProgramGroups) + foreach (var (pos, programGroup) in _dataLayer.ProgramGroups) { var isActiveOrOnCooldown = programGroup.LastExecuted.AddMilliseconds(_configManager.Config.Behaviour.CooldownTime) @@ -625,7 +619,7 @@ private async Task ControlGroup(Guid groupId, uint duration, byte intensit private async Task RemoteActivateShockers(ControlLogSender sender, ICollection logs) { - if (sender.ConnectionId == connectionId) + if (sender.ConnectionId == _liveConnectionId) { _logger.LogDebug("Ignoring remote command log cause it was the local connection"); return; @@ -670,7 +664,7 @@ private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log } var configGroupsAffected = _configManager.Config.Groups.Where(s => s.Value.Shockers.Any(x => x == log.Shocker.Id)).Select(x => x.Key).ToArray(); - var programGroupsAffected = ProgramGroups.Where(x => configGroupsAffected.Contains(x.Key)).Select(x => x.Value); + var programGroupsAffected = _dataLayer.ProgramGroups.Where(x => configGroupsAffected.Contains(x.Key)).Select(x => x.Value); var oneShock = false; foreach (var pain in programGroupsAffected) diff --git a/ShockOsc/ShockOscData.cs b/ShockOsc/ShockOscData.cs new file mode 100644 index 0000000..ad689c4 --- /dev/null +++ b/ShockOsc/ShockOscData.cs @@ -0,0 +1,9 @@ +using System.Collections.Concurrent; +using OpenShock.ShockOsc.Models; + +namespace OpenShock.ShockOsc; + +public sealed class ShockOscData +{ + public readonly ConcurrentDictionary ProgramGroups = new(); +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor index e5c41a2..62a07ab 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -6,6 +6,8 @@ @implements IDisposable @inject UnderscoreConfig underscoreConfig @inject ConfigManager ConfigManager +@inject ShockOsc ShockOsc +@inject UnderscoreConfig UnderscoreConfig Shocker Options @@ -186,7 +188,7 @@ protected override void OnInitialized() { - ShockOsc.OnConfigUpdate = OnConfigUpdate; + UnderscoreConfig.OnConfigUpdate += OnConfigUpdate; intensity = ConfigManager.Config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; duration = ConfigManager.Config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; @@ -220,7 +222,7 @@ public void Dispose() { - ShockOsc.OnConfigUpdate = null; + UnderscoreConfig.OnConfigUpdate -= OnConfigUpdate; } } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/DebugTab.razor b/ShockOsc/Ui/Components/Tabs/DebugTab.razor index 042758e..a03201a 100644 --- a/ShockOsc/Ui/Components/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DebugTab.razor @@ -1,6 +1,7 @@ @using OpenShock.ShockOsc.Utils @using Serilog.Sinks.SystemConsole.Themes @implements IAsyncDisposable +@inject ShockOsc ShockOsc List of ShockOSC prefixed parameters and their states
diff --git a/ShockOsc/UnderscoreConfig.cs b/ShockOsc/UnderscoreConfig.cs index 09a3c55..07ce543 100644 --- a/ShockOsc/UnderscoreConfig.cs +++ b/ShockOsc/UnderscoreConfig.cs @@ -9,12 +9,16 @@ public sealed class UnderscoreConfig private readonly ILogger _logger; private readonly OscClient _oscClient; private readonly ConfigManager _configManager; + private readonly ShockOscData _dataLayer; - public UnderscoreConfig(ILogger logger, OscClient oscClient, ConfigManager configManager) + public event Action? OnConfigUpdate; + + public UnderscoreConfig(ILogger logger, OscClient oscClient, ConfigManager configManager, ShockOscData dataLayer) { _logger = logger; _oscClient = oscClient; _configManager = configManager; + _dataLayer = dataLayer; } public bool KillSwitch { get; set; } = false; @@ -34,14 +38,14 @@ public void HandleCommand(string parameterName, object?[] arguments) { var groupName = settingPath[0]; var action = settingPath[1]; - if (!ShockOsc.ProgramGroups.Any(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)) && groupName != "_All") + if (!_dataLayer.ProgramGroups.Any(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)) && groupName != "_All") { _logger.LogWarning("Unknown shocker {Shocker}", groupName); _logger.LogDebug("Param: {Param}", action); return; } - var group = ShockOsc.ProgramGroups.First(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)); + var group = _dataLayer.ProgramGroups.First(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)); var value = arguments.ElementAtOrDefault(0); // TODO: support groups @@ -58,7 +62,7 @@ public void HandleCommand(string parameterName, object?[] arguments) _configManager.Config.Behaviour.IntensityRange.Min = MathUtils.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); ValidateSettings(); _configManager.Save(); - ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + OnConfigUpdate?.Invoke(); // update Ui } break; @@ -72,7 +76,7 @@ public void HandleCommand(string parameterName, object?[] arguments) _configManager.Config.Behaviour.IntensityRange.Max = MathUtils.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); ValidateSettings(); _configManager.Save(); - ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + OnConfigUpdate?.Invoke(); // update Ui } break; @@ -86,7 +90,7 @@ public void HandleCommand(string parameterName, object?[] arguments) _configManager.Config.Behaviour.FixedDuration = MathUtils.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); ValidateSettings(); _configManager.Save(); - ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + OnConfigUpdate?.Invoke(); // update Ui } break; @@ -100,7 +104,7 @@ public void HandleCommand(string parameterName, object?[] arguments) _configManager.Config.Behaviour.CooldownTime = MathUtils.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); ValidateSettings(); _configManager.Save(); - ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + OnConfigUpdate?.Invoke(); // update Ui } break; @@ -115,7 +119,7 @@ public void HandleCommand(string parameterName, object?[] arguments) _configManager.Config.Behaviour.HoldTime = MathUtils.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); ValidateSettings(); _configManager.Save(); - ShockOsc.OnConfigUpdate?.Invoke(); // update Ui + OnConfigUpdate?.Invoke(); // update Ui } break; } From 70a8fffde5917c006c89a3f742b73bff97f9b31f Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 9 Apr 2024 03:26:27 +0200 Subject: [PATCH 27/95] Split off more logic from ShockOsc.cs --- ShockOsc/Backend/BackendLiveApiManager.cs | 164 ++++++++++++++- ShockOsc/OscHandler.cs | 104 +++++++++- ShockOsc/ShockOsc.cs | 242 ++-------------------- ShockOsc/ShockOscData.cs | 6 +- 4 files changed, 290 insertions(+), 226 deletions(-) diff --git a/ShockOsc/Backend/BackendLiveApiManager.cs b/ShockOsc/Backend/BackendLiveApiManager.cs index 2c02a77..60aa42c 100644 --- a/ShockOsc/Backend/BackendLiveApiManager.cs +++ b/ShockOsc/Backend/BackendLiveApiManager.cs @@ -1,7 +1,12 @@ -using Microsoft.Extensions.Logging; +using System.Globalization; +using Microsoft.Extensions.Logging; using OpenShock.SDK.CSharp.Live; +using OpenShock.SDK.CSharp.Live.Models; +using OpenShock.SDK.CSharp.Models; using OpenShock.ShockOsc.Config; +using OpenShock.ShockOsc.Models; using Serilog; +using SmartFormat; namespace OpenShock.ShockOsc.Backend; @@ -10,12 +15,33 @@ public sealed class BackendLiveApiManager private readonly ILogger _logger; private readonly ConfigManager _configManager; private readonly OpenShockApiLiveClient _openShockApiLiveClient; + private readonly OscClient _oscClient; + private readonly ShockOscData _dataLayer; + private readonly OscHandler _oscHandler; - public BackendLiveApiManager(ILogger logger, ConfigManager configManager, OpenShockApiLiveClient openShockApiLiveClient) + private string _liveConnectionId = string.Empty; + + public BackendLiveApiManager(ILogger logger, + ConfigManager configManager, + OpenShockApiLiveClient openShockApiLiveClient, + OscClient oscClient, + ShockOscData dataLayer, + OscHandler oscHandler) { _logger = logger; _configManager = configManager; _openShockApiLiveClient = openShockApiLiveClient; + _oscClient = oscClient; + _dataLayer = dataLayer; + _oscHandler = oscHandler; + + _openShockApiLiveClient.OnWelcome += s => + { + _liveConnectionId = s; + return Task.CompletedTask; + }; + + _openShockApiLiveClient.OnLog += RemoteActivateShockers; } @@ -34,4 +60,138 @@ await _openShockApiLiveClient.Setup(new ApiLiveClientOptions() }); } + /// + /// Send a stop command for every shocker in a group + /// + /// + /// + public Task CancelControl(ProgramGroup programGroup) + { + _logger.LogTrace("Cancelling action"); + return ControlGroup(programGroup.Id, 0, 0, ControlType.Stop); + } + + /// + /// Control a group, if guid is empty, all shockers will be controlled + /// + /// + /// + /// + /// + /// + public async Task ControlGroup(Guid groupId, uint duration, byte intensity, ControlType type) + { + if(groupId == Guid.Empty) + { + var controlCommandsAll = _configManager.Config.OpenShock.Shockers + .Where(x => x.Value.Enabled) + .Select(x => new Control + { + Id = x.Key, + Duration = duration, + Intensity = intensity, + Type = type + }); + await _openShockApiLiveClient.Control(controlCommandsAll); + return true; + } + + + if (!_configManager.Config.Groups.TryGetValue(groupId, out var group)) return false; + + var controlCommands = group.Shockers.Select(x => new Control + { + Id = x, + Duration = duration, + Intensity = intensity, + Type = type + }); + + await _openShockApiLiveClient.Control(controlCommands); + return true; + } + + private async Task RemoteActivateShockers(ControlLogSender sender, ICollection logs) + { + if (sender.ConnectionId == _liveConnectionId) + { + _logger.LogDebug("Ignoring remote command log cause it was the local connection"); + return; + } + + foreach (var controlLog in logs) await RemoteActivateShocker(sender, controlLog); + } + + private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) + { + var inSeconds = ((float)log.Duration / 1000).ToString(CultureInfo.InvariantCulture); + + if (sender.CustomName == null) + _logger.LogInformation( + "Received remote {Type} for \"{ShockerName}\" at {Intensity}%:{Duration}s by {Sender}", + log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.Name); + else + _logger.LogInformation( + "Received remote {Type} for \"{ShockerName}\" at {Intensity}%:{Duration}s by {SenderCustomName} [{Sender}]", + log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.CustomName, sender.Name); + + var template = _configManager.Config.Chatbox.Types[log.Type]; + if (_configManager.Config.Osc.Chatbox && + _configManager.Config.Chatbox.DisplayRemoteControl && template.Enabled) + { + // Chatbox message remote + var dat = new + { + ShockerName = log.Shocker.Name, + Intensity = log.Intensity, + Duration = log.Duration, + DurationSeconds = inSeconds, + Name = sender.Name, + CustomName = sender.CustomName + }; + + var msg = + $"{_configManager.Config.Chatbox.Prefix}{Smart.Format(sender.CustomName == null ? template.Remote : template.RemoteWithCustomName, dat)}"; + await _oscClient.SendChatboxMessage(msg); + } + + var configGroupsAffected = _configManager.Config.Groups.Where(s => s.Value.Shockers.Any(x => x == log.Shocker.Id)).Select(x => x.Key).ToArray(); + var programGroupsAffected = _dataLayer.ProgramGroups.Where(x => configGroupsAffected.Contains(x.Key)).Select(x => x.Value); + var oneShock = false; + + foreach (var pain in programGroupsAffected) + { + switch (log.Type) + + { + case ControlType.Shock: + { + pain.LastIntensity = log.Intensity; + pain.LastDuration = log.Duration; + pain.LastExecuted = log.ExecutedAt; + + oneShock = true; + break; + } + case ControlType.Vibrate: + pain.LastVibration = log.ExecutedAt; + break; + case ControlType.Stop: + pain.LastDuration = 0; + await _oscHandler.SendParams(); + break; + case ControlType.Sound: + break; + default: + _logger.LogError("ControlType was out of range. Value was: {Type}", log.Type); + break; + } + + if (oneShock) + { + await _oscHandler.ForceUnmute(); + await _oscHandler.SendParams(); + } + } + } } \ No newline at end of file diff --git a/ShockOsc/OscHandler.cs b/ShockOsc/OscHandler.cs index eb0cffc..b5cdac8 100644 --- a/ShockOsc/OscHandler.cs +++ b/ShockOsc/OscHandler.cs @@ -1,13 +1,113 @@ using Microsoft.Extensions.Logging; +using OpenShock.ShockOsc.Config; +using OpenShock.ShockOsc.OscChangeTracker; +using OpenShock.ShockOsc.Utils; namespace OpenShock.ShockOsc; public sealed class OscHandler { + private readonly ChangeTrackedOscParam _paramAnyActive; + private readonly ChangeTrackedOscParam _paramAnyCooldown; + private readonly ChangeTrackedOscParam _paramAnyCooldownPercentage; + private readonly ChangeTrackedOscParam _paramAnyIntensity; + private readonly ILogger _logger; + private readonly OscClient _oscClient; + private readonly ConfigManager _configManager; + private readonly ShockOscData _shockOscData; - public OscHandler(ILogger logger) + public OscHandler(ILogger logger, OscClient oscClient, ConfigManager configManager, ShockOscData shockOscData) { - logger.LogInformation("YES STARTED"); + _logger = logger; + _oscClient = oscClient; + _configManager = configManager; + _shockOscData = shockOscData; + + _paramAnyActive = new ChangeTrackedOscParam("_Any", "_Active", false, _oscClient); + _paramAnyCooldown = new ChangeTrackedOscParam("_Any", "_Cooldown", false, _oscClient); + _paramAnyCooldownPercentage = new ChangeTrackedOscParam("_Any", "_CooldownPercentage", 0f, _oscClient); + _paramAnyIntensity = new ChangeTrackedOscParam("_Any", "_Intensity", 0f, _oscClient); + } + + /// + /// Force unmute the users if enabled in config + /// + public async Task ForceUnmute() + { + // If we don't have to force unmute or we're not muted, also check config here. + if (!_configManager.Config.Behaviour.ForceUnmute || !_shockOscData.IsMuted) return; + + _logger.LogDebug("Force unmuting..."); + + // So this is absolutely disgusting, but vrchat seems to be very retarded. + // PS: If you send true for more than 500ms the game locks up. + + // Button press off + await _oscClient.SendGameMessage("/input/Voice", false) + .ConfigureAwait(false); + + // We wait 50 ms.. + await Task.Delay(50) + .ConfigureAwait(false); + + // Button press on + await _oscClient.SendGameMessage("/input/Voice", true) + .ConfigureAwait(false); + + // We wait 50 ms.. + await Task.Delay(50) + .ConfigureAwait(false); + + // Button press off + await _oscClient.SendGameMessage("/input/Voice", false) + .ConfigureAwait(false); + } + + /// + /// Send parameter updates to osc + /// + public async Task SendParams() + { + // TODO: maybe force resend on avatar change + var anyActive = false; + var anyCooldown = false; + var anyCooldownPercentage = 0f; + var anyIntensity = 0f; + + foreach (var shocker in _shockOscData.ProgramGroups.Values) + { + var isActive = shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; + var isActiveOrOnCooldown = + shocker.LastExecuted.AddMilliseconds(_configManager.Config.Behaviour.CooldownTime) + .AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; + if (!isActiveOrOnCooldown && shocker.LastIntensity > 0) + shocker.LastIntensity = 0; + + var onCoolDown = !isActive && isActiveOrOnCooldown; + + var cooldownPercentage = 0f; + if (onCoolDown) + cooldownPercentage = MathUtils.ClampFloat(1 - + (float)(DateTime.UtcNow - + shocker.LastExecuted.AddMilliseconds(shocker.LastDuration)) + .TotalMilliseconds / + _configManager.Config.Behaviour.CooldownTime); + + await shocker.ParamActive.SetValue(isActive); + await shocker.ParamCooldown.SetValue(onCoolDown); + await shocker.ParamCooldownPercentage.SetValue(cooldownPercentage); + await shocker.ParamIntensity.SetValue(MathUtils.ClampFloat(shocker.LastIntensity)); + + if (isActive) anyActive = true; + if (onCoolDown) anyCooldown = true; + anyCooldownPercentage = Math.Max(anyCooldownPercentage, cooldownPercentage); + anyIntensity = Math.Max(anyIntensity, MathUtils.ClampFloat(shocker.LastIntensity)); + } + + await _paramAnyActive.SetValue(anyActive); + await _paramAnyCooldown.SetValue(anyCooldown); + await _paramAnyCooldownPercentage.SetValue(anyCooldownPercentage); + await _paramAnyIntensity.SetValue(anyIntensity); } } \ No newline at end of file diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/ShockOsc.cs index fbaf904..925031e 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/ShockOsc.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Globalization; using System.Net; using LucHeart.CoreOSC; @@ -9,7 +8,6 @@ using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; -using OpenShock.ShockOsc.OscChangeTracker; using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Utils; using SmartFormat; @@ -22,11 +20,12 @@ public sealed class ShockOsc { private readonly ILogger _logger; private readonly OscClient _oscClient; - private readonly OpenShockApiLiveClient _liveClient; + private readonly BackendLiveApiManager _backendLiveApiManager; private readonly UnderscoreConfig _underscoreConfig; private readonly ConfigManager _configManager; private readonly OscQueryServer _oscQueryServer; private readonly ShockOscData _dataLayer; + private readonly OscHandler _oscHandler; private bool _oscServerActive; private bool _isAfk; @@ -53,38 +52,27 @@ public sealed class ShockOsc public Action? OnParamsChange; - private readonly ChangeTrackedOscParam _paramAnyActive; - private readonly ChangeTrackedOscParam _paramAnyCooldown; - private readonly ChangeTrackedOscParam _paramAnyCooldownPercentage; - private readonly ChangeTrackedOscParam _paramAnyIntensity; - private string _liveConnectionId = string.Empty; - - public ShockOsc(ILogger logger, OscClient oscClient, OpenShockApi openShockApi, - OpenShockApiLiveClient liveClient, UnderscoreConfig underscoreConfig, - ConfigManager configManager, OscQueryServer oscQueryServer, ShockOscData dataLayer) + public ShockOsc(ILogger logger, + OscClient oscClient, + BackendLiveApiManager backendLiveApiManager, + UnderscoreConfig underscoreConfig, + ConfigManager configManager, + OscQueryServer oscQueryServer, + ShockOscData dataLayer, + OscHandler oscHandler + ) { _logger = logger; _oscClient = oscClient; - _liveClient = liveClient; + _backendLiveApiManager = backendLiveApiManager; _underscoreConfig = underscoreConfig; _configManager = configManager; _oscQueryServer = oscQueryServer; _dataLayer = dataLayer; + _oscHandler = oscHandler; - _paramAnyActive = new ChangeTrackedOscParam("_Any", "_Active", false, _oscClient); - _paramAnyCooldown = new ChangeTrackedOscParam("_Any", "_Cooldown", false, _oscClient); - _paramAnyCooldownPercentage = new ChangeTrackedOscParam("_Any", "_CooldownPercentage", 0f, _oscClient); - _paramAnyIntensity = new ChangeTrackedOscParam("_Any", "_Intensity", 0f, _oscClient); - - liveClient.OnWelcome += s => - { - _liveConnectionId = s; - return Task.CompletedTask; - }; - - liveClient.OnLog += RemoteActivateShockers; - + OnGroupsChanged += SetupGroups; oscQueryServer.FoundVrcClient += FoundVrcClient; @@ -341,7 +329,7 @@ private async Task ReceiveLogic() else if (_configManager.Config.Behaviour.WhileBoneHeld != BehaviourConf.BoneHeldAction.None) { - await CancelAction(programGroup); + await _backendLiveApiManager.CancelControl(programGroup); } } @@ -387,7 +375,7 @@ private async Task SenderLoopAsync() { while (_oscServerActive) { - await SendParams(); + await _oscHandler.SendParams(); await Task.Delay(300); } } @@ -399,8 +387,8 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i var intensityPercentage = Math.Round(MathUtils.ClampFloat(intensity) * 100f); programGroup.LastIntensity = intensity; - ForceUnmute(); - SendParams(); + _oscHandler.ForceUnmute(); + _oscHandler.SendParams(); programGroup.TriggerMethod = TriggerMethod.None; var inSeconds = MathF.Round(duration / 1000f, 1).ToString(CultureInfo.InvariantCulture); @@ -408,7 +396,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i "Sending shock to {GroupName} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s", programGroup.Name, intensity, intensityPercentage, inSeconds); - await ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock); + await _backendLiveApiManager.ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock); if (!_configManager.Config.Osc.Chatbox) return; // Chatbox message local @@ -424,59 +412,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i var msg = $"{_configManager.Config.Chatbox.Prefix}{Smart.Format(template.Local, dat)}"; await _oscClient.SendChatboxMessage(msg); } - - // /// - // /// Coverts to a 0-1 float and scale it to the max intensity - // /// - // /// - // /// - // private static float GetFloatScaled(byte intensity) => - // ClampFloat((float)intensity / _configManager.ConfigInstance.Behaviour.IntensityRange.Max); - - private async Task SendParams() - { - // TODO: maybe force resend on avatar change - var anyActive = false; - var anyCooldown = false; - var anyCooldownPercentage = 0f; - var anyIntensity = 0f; - - foreach (var shocker in _dataLayer.ProgramGroups.Values) - { - var isActive = shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; - var isActiveOrOnCooldown = - shocker.LastExecuted.AddMilliseconds(_configManager.Config.Behaviour.CooldownTime) - .AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; - if (!isActiveOrOnCooldown && shocker.LastIntensity > 0) - shocker.LastIntensity = 0; - - var onCoolDown = !isActive && isActiveOrOnCooldown; - - var cooldownPercentage = 0f; - if (onCoolDown) - cooldownPercentage = MathUtils.ClampFloat(1 - - (float)(DateTime.UtcNow - - shocker.LastExecuted.AddMilliseconds(shocker.LastDuration)) - .TotalMilliseconds / - _configManager.Config.Behaviour.CooldownTime); - - await shocker.ParamActive.SetValue(isActive); - await shocker.ParamCooldown.SetValue(onCoolDown); - await shocker.ParamCooldownPercentage.SetValue(cooldownPercentage); - await shocker.ParamIntensity.SetValue(MathUtils.ClampFloat(shocker.LastIntensity)); - - if (isActive) anyActive = true; - if (onCoolDown) anyCooldown = true; - anyCooldownPercentage = Math.Max(anyCooldownPercentage, cooldownPercentage); - anyIntensity = Math.Max(anyIntensity, MathUtils.ClampFloat(shocker.LastIntensity)); - } - - await _paramAnyActive.SetValue(anyActive); - await _paramAnyCooldown.SetValue(anyCooldown); - await _paramAnyCooldownPercentage.SetValue(anyCooldownPercentage); - await _paramAnyIntensity.SetValue(anyIntensity); - } - + private async Task CheckLoop() { while (_oscServerActive) @@ -526,7 +462,7 @@ private async Task CheckLogic() programGroup.LastVibration = DateTime.UtcNow; _logger.LogDebug("Vibrating {Shocker} at {Intensity}", pos, vibrationIntensity); - await ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, + await _backendLiveApiManager.ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, _configManager.Config.Behaviour.WhileBoneHeld == BehaviourConf.BoneHeldAction.Shock ? ControlType.Shock @@ -585,140 +521,4 @@ private uint GetDuration() (int)(rdr.Max / config.RandomDurationStep)) * config.RandomDurationStep); } - private async Task ControlGroup(Guid groupId, uint duration, byte intensity, ControlType type) - { - if(groupId == Guid.Empty) - { - var controlCommandsAll = _configManager.Config.OpenShock.Shockers - .Where(x => x.Value.Enabled) - .Select(x => new Control - { - Id = x.Key, - Duration = duration, - Intensity = intensity, - Type = type - }); - await _liveClient.Control(controlCommandsAll); - return true; - } - - - if (!_configManager.Config.Groups.TryGetValue(groupId, out var group)) return false; - - var controlCommands = group.Shockers.Select(x => new Control - { - Id = x, - Duration = duration, - Intensity = intensity, - Type = type - }); - - await _liveClient.Control(controlCommands); - return true; - } - - private async Task RemoteActivateShockers(ControlLogSender sender, ICollection logs) - { - if (sender.ConnectionId == _liveConnectionId) - { - _logger.LogDebug("Ignoring remote command log cause it was the local connection"); - return; - } - - foreach (var controlLog in logs) await RemoteActivateShocker(sender, controlLog); - - - } - - private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log) - { - var inSeconds = ((float)log.Duration / 1000).ToString(CultureInfo.InvariantCulture); - - if (sender.CustomName == null) - _logger.LogInformation( - "Received remote {Type} for \"{ShockerName}\" at {Intensity}%:{Duration}s by {Sender}", - log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.Name); - else - _logger.LogInformation( - "Received remote {Type} for \"{ShockerName}\" at {Intensity}%:{Duration}s by {SenderCustomName} [{Sender}]", - log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.CustomName, sender.Name); - - var template = _configManager.Config.Chatbox.Types[log.Type]; - if (_configManager.Config.Osc.Chatbox && - _configManager.Config.Chatbox.DisplayRemoteControl && template.Enabled) - { - // Chatbox message remote - var dat = new - { - ShockerName = log.Shocker.Name, - Intensity = log.Intensity, - Duration = log.Duration, - DurationSeconds = inSeconds, - Name = sender.Name, - CustomName = sender.CustomName - }; - - var msg = - $"{_configManager.Config.Chatbox.Prefix}{Smart.Format(sender.CustomName == null ? template.Remote : template.RemoteWithCustomName, dat)}"; - await _oscClient.SendChatboxMessage(msg); - } - - var configGroupsAffected = _configManager.Config.Groups.Where(s => s.Value.Shockers.Any(x => x == log.Shocker.Id)).Select(x => x.Key).ToArray(); - var programGroupsAffected = _dataLayer.ProgramGroups.Where(x => configGroupsAffected.Contains(x.Key)).Select(x => x.Value); - var oneShock = false; - - foreach (var pain in programGroupsAffected) - { - switch (log.Type) - - { - case ControlType.Shock: - { - pain.LastIntensity = log.Intensity; - pain.LastDuration = log.Duration; - pain.LastExecuted = log.ExecutedAt; - - oneShock = true; - break; - } - case ControlType.Vibrate: - pain.LastVibration = log.ExecutedAt; - break; - case ControlType.Stop: - pain.LastDuration = 0; - SendParams(); - break; - case ControlType.Sound: - break; - default: - _logger.LogError("ControlType was out of range. Value was: {Type}", log.Type); - break; - } - - if (oneShock) - { - ForceUnmute(); - SendParams(); - } - } - } - - private async Task ForceUnmute() - { - if (!_configManager.Config.Behaviour.ForceUnmute || !_isMuted) return; - _logger.LogDebug("Force unmuting..."); - await _oscClient.SendGameMessage("/input/Voice", false); - await Task.Delay(50); - await _oscClient.SendGameMessage("/input/Voice", true); - await Task.Delay(50); - await _oscClient.SendGameMessage("/input/Voice", false); - } - - private Task CancelAction(ProgramGroup programGroup) - { - _logger.LogDebug("Cancelling action"); - return ControlGroup(programGroup.Id, 0, 0, ControlType.Stop); - } - - } \ No newline at end of file diff --git a/ShockOsc/ShockOscData.cs b/ShockOsc/ShockOscData.cs index ad689c4..9f21a2b 100644 --- a/ShockOsc/ShockOscData.cs +++ b/ShockOsc/ShockOscData.cs @@ -3,7 +3,11 @@ namespace OpenShock.ShockOsc; +// In a perfect world, this class would not exist. +// But we kinda need it for now, dunno if it is possible to be removed ever. public sealed class ShockOscData { - public readonly ConcurrentDictionary ProgramGroups = new(); + public ConcurrentDictionary ProgramGroups { get; } = new(); + + public bool IsMuted { get; set; } } \ No newline at end of file From 94e0cb340bdebebd709eaf5fd962c0ee500952be Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 9 Apr 2024 03:33:14 +0200 Subject: [PATCH 28/95] Move stuff around --- ShockOsc/Backend/BackendLiveApiManager.cs | 1 + ShockOsc/MauiProgram.cs | 1 + ShockOsc/Models/ProgramGroup.cs | 1 + .../OscChangeTracker/ChangeTrackedOscParam.cs | 3 ++- ShockOsc/{ => Services}/OscClient.cs | 2 +- ShockOsc/{ => Services}/OscHandler.cs | 2 +- ShockOsc/{ => Services}/ShockOsc.cs | 1 + ShockOsc/{ => Services}/ShockOscData.cs | 2 +- ShockOsc/{ => Services}/UnderscoreConfig.cs | 2 +- ShockOsc/{ => Services}/Updater.cs | 3 +-- ShockOsc/ShockLinkModels.cs | 25 ------------------- ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 1 + ShockOsc/Ui/Components/UpdateDialog.razor | 1 + ShockOsc/Ui/Components/UpdateLogout.razor | 1 + 14 files changed, 14 insertions(+), 32 deletions(-) rename ShockOsc/{ => Services}/OscClient.cs (98%) rename ShockOsc/{ => Services}/OscHandler.cs (99%) rename ShockOsc/{ => Services}/ShockOsc.cs (99%) rename ShockOsc/{ => Services}/ShockOscData.cs (90%) rename ShockOsc/{ => Services}/UnderscoreConfig.cs (99%) rename ShockOsc/{ => Services}/Updater.cs (99%) delete mode 100644 ShockOsc/ShockLinkModels.cs diff --git a/ShockOsc/Backend/BackendLiveApiManager.cs b/ShockOsc/Backend/BackendLiveApiManager.cs index 60aa42c..d330efe 100644 --- a/ShockOsc/Backend/BackendLiveApiManager.cs +++ b/ShockOsc/Backend/BackendLiveApiManager.cs @@ -5,6 +5,7 @@ using OpenShock.SDK.CSharp.Models; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; +using OpenShock.ShockOsc.Services; using Serilog; using SmartFormat; diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 3f016c0..187e0db 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -6,6 +6,7 @@ using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.OscQueryLibrary; +using OpenShock.ShockOsc.Services; using OpenShock.ShockOsc.Ui; using Serilog; diff --git a/ShockOsc/Models/ProgramGroup.cs b/ShockOsc/Models/ProgramGroup.cs index 8b5a932..c913b2b 100644 --- a/ShockOsc/Models/ProgramGroup.cs +++ b/ShockOsc/Models/ProgramGroup.cs @@ -1,4 +1,5 @@ using OpenShock.ShockOsc.OscChangeTracker; +using OpenShock.ShockOsc.Services; namespace OpenShock.ShockOsc.Models; diff --git a/ShockOsc/OscChangeTracker/ChangeTrackedOscParam.cs b/ShockOsc/OscChangeTracker/ChangeTrackedOscParam.cs index ee95215..89ac2af 100644 --- a/ShockOsc/OscChangeTracker/ChangeTrackedOscParam.cs +++ b/ShockOsc/OscChangeTracker/ChangeTrackedOscParam.cs @@ -1,4 +1,5 @@ -using Serilog; +using OpenShock.ShockOsc.Services; +using Serilog; namespace OpenShock.ShockOsc.OscChangeTracker; diff --git a/ShockOsc/OscClient.cs b/ShockOsc/Services/OscClient.cs similarity index 98% rename from ShockOsc/OscClient.cs rename to ShockOsc/Services/OscClient.cs index 8f99a88..9cc5138 100644 --- a/ShockOsc/OscClient.cs +++ b/ShockOsc/Services/OscClient.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; using OpenShock.ShockOsc.Config; -namespace OpenShock.ShockOsc; +namespace OpenShock.ShockOsc.Services; public sealed class OscClient { diff --git a/ShockOsc/OscHandler.cs b/ShockOsc/Services/OscHandler.cs similarity index 99% rename from ShockOsc/OscHandler.cs rename to ShockOsc/Services/OscHandler.cs index b5cdac8..2bbcd05 100644 --- a/ShockOsc/OscHandler.cs +++ b/ShockOsc/Services/OscHandler.cs @@ -3,7 +3,7 @@ using OpenShock.ShockOsc.OscChangeTracker; using OpenShock.ShockOsc.Utils; -namespace OpenShock.ShockOsc; +namespace OpenShock.ShockOsc.Services; public sealed class OscHandler { diff --git a/ShockOsc/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs similarity index 99% rename from ShockOsc/ShockOsc.cs rename to ShockOsc/Services/ShockOsc.cs index 925031e..86c2a92 100644 --- a/ShockOsc/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -9,6 +9,7 @@ using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.OscQueryLibrary; +using OpenShock.ShockOsc.Services; using OpenShock.ShockOsc.Utils; using SmartFormat; diff --git a/ShockOsc/ShockOscData.cs b/ShockOsc/Services/ShockOscData.cs similarity index 90% rename from ShockOsc/ShockOscData.cs rename to ShockOsc/Services/ShockOscData.cs index 9f21a2b..8765183 100644 --- a/ShockOsc/ShockOscData.cs +++ b/ShockOsc/Services/ShockOscData.cs @@ -1,7 +1,7 @@ using System.Collections.Concurrent; using OpenShock.ShockOsc.Models; -namespace OpenShock.ShockOsc; +namespace OpenShock.ShockOsc.Services; // In a perfect world, this class would not exist. // But we kinda need it for now, dunno if it is possible to be removed ever. diff --git a/ShockOsc/UnderscoreConfig.cs b/ShockOsc/Services/UnderscoreConfig.cs similarity index 99% rename from ShockOsc/UnderscoreConfig.cs rename to ShockOsc/Services/UnderscoreConfig.cs index 07ce543..d978113 100644 --- a/ShockOsc/UnderscoreConfig.cs +++ b/ShockOsc/Services/UnderscoreConfig.cs @@ -2,7 +2,7 @@ using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Utils; -namespace OpenShock.ShockOsc; +namespace OpenShock.ShockOsc.Services; public sealed class UnderscoreConfig { diff --git a/ShockOsc/Updater.cs b/ShockOsc/Services/Updater.cs similarity index 99% rename from ShockOsc/Updater.cs rename to ShockOsc/Services/Updater.cs index 688d66c..978219a 100644 --- a/ShockOsc/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -4,9 +4,8 @@ using Microsoft.Extensions.Logging; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; -using Serilog; -namespace OpenShock.ShockOsc; +namespace OpenShock.ShockOsc.Services; public sealed class Updater { diff --git a/ShockOsc/ShockLinkModels.cs b/ShockOsc/ShockLinkModels.cs deleted file mode 100644 index 953064a..0000000 --- a/ShockOsc/ShockLinkModels.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace OpenShock.ShockOsc; - -public class OwnShockersResponseResponseData -{ - public string? Message { get; set; } - public required Device[] Data { get; set; } -} - -public class Device -{ - public required Guid Id { get; set; } - public required string Name { get; set; } - public DateTime CreatedOn { get; set; } - public IList Shockers { get; set; } = new List(); - - public class Shocker - { - public required Guid Id { get; set; } - public required string Name { get; set; } - public required bool IsPaused { get; set; } - public required DateTime CreatedOn { get; set; } - public required int RfId { get; set; } - public required string Model { get; set; } - } -} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor index 62a07ab..2c44478 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -3,6 +3,7 @@ @using OpenShock.ShockOsc.Utils @using System.Reactive.Linq @using OpenShock.ShockOsc.Config +@using OpenShock.ShockOsc.Services @implements IDisposable @inject UnderscoreConfig underscoreConfig @inject ConfigManager ConfigManager diff --git a/ShockOsc/Ui/Components/UpdateDialog.razor b/ShockOsc/Ui/Components/UpdateDialog.razor index 3b9e7f6..01f07d0 100644 --- a/ShockOsc/Ui/Components/UpdateDialog.razor +++ b/ShockOsc/Ui/Components/UpdateDialog.razor @@ -1,4 +1,5 @@ @using OpenShock.ShockOsc.Config +@using OpenShock.ShockOsc.Services @using OpenShock.ShockOsc.Utils @inject ConfigManager ConfigManager @inject Updater Updater diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Components/UpdateLogout.razor index 2f75e8d..6298ca7 100644 --- a/ShockOsc/Ui/Components/UpdateLogout.razor +++ b/ShockOsc/Ui/Components/UpdateLogout.razor @@ -1,4 +1,5 @@ @using OpenShock.ShockOsc.Config +@using OpenShock.ShockOsc.Services @inject IDialogService Dialog @inject IDialogService DialogService @inject ConfigManager ConfigManager From 92101095868c6f5ef5de98fd55dc337228898596 Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 9 Apr 2024 03:35:47 +0200 Subject: [PATCH 29/95] remove useless exception, if this fails we are in the shits anyways --- ShockOsc/Logging/UiLogSink.cs | 52 ++++++++++++++--------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/ShockOsc/Logging/UiLogSink.cs b/ShockOsc/Logging/UiLogSink.cs index ddc37be..b813674 100644 --- a/ShockOsc/Logging/UiLogSink.cs +++ b/ShockOsc/Logging/UiLogSink.cs @@ -23,46 +23,36 @@ public UiLogSink() public void Emit(LogEvent logEvent) { - try - { - using var textWriter = new StringWriter(); - _messageProvider.Format(logEvent, textWriter); - // var logMessage = logEvent.RenderMessage(_formatProvider); - var logMessage = textWriter.ToString(); - if (string.IsNullOrEmpty(logMessage)) return; - if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) - NotificationAction?.Invoke(logMessage[82..], Severity.Error); + using var textWriter = new StringWriter(); + _messageProvider.Format(logEvent, textWriter); + + var logMessage = textWriter.ToString(); + if (string.IsNullOrEmpty(logMessage)) return; + if (logMessage.StartsWith("[Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler] ")) + NotificationAction?.Invoke(logMessage[82..], Severity.Error); - Debug.WriteLine(logMessage); + var sourceContextString = string.Empty; - var sourceContextString = string.Empty; - - if (logEvent.Properties.TryGetValue("SourceContext", out var sourceContext) && sourceContext is ScalarValue - { - Value: string scalarString - }) sourceContextString = scalarString; - - var lastIndexSlash = Math.Min(sourceContextString.Length, sourceContextString.LastIndexOf('.') + 1); - - LogStore.AddLog(new LogStore.LogEntry + if (logEvent.Properties.TryGetValue("SourceContext", out var sourceContext) && sourceContext is ScalarValue { - Message = logMessage, - Time = logEvent.Timestamp, - Level = logEvent.Level, - SourceContext = sourceContextString, - SourceContextShort = sourceContextString[lastIndexSlash..] - }); - } - catch (Exception) + Value: string scalarString + }) sourceContextString = scalarString; + + var lastIndexSlash = Math.Min(sourceContextString.Length, sourceContextString.LastIndexOf('.') + 1); + + LogStore.AddLog(new LogStore.LogEntry { - // this kinda sucks - } + Message = logMessage, + Time = logEvent.Timestamp, + Level = logEvent.Level, + SourceContext = sourceContextString, + SourceContextShort = sourceContextString[lastIndexSlash..] + }); } } public static class UiLogSinkExtensions { - public static LoggerConfiguration UiLogSink( this LoggerSinkConfiguration sinkConfiguration) { From ad355a168207daab91c81113ae19ef9fcb4c15f7 Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 9 Apr 2024 17:07:25 +0200 Subject: [PATCH 30/95] Nav rework lol --- ...LiveApiManager.cs => BackendHubManager.cs} | 22 +-- ShockOsc/MauiProgram.cs | 4 +- ShockOsc/Services/BackendControlService.cs | 6 +- ShockOsc/Services/ShockOsc.cs | 12 +- ShockOsc/ShockOsc.csproj | 2 +- ShockOsc/Ui/Components/Layout/Logo.razor | 8 - ShockOsc/Ui/Components/MainLayout.razor | 141 +++++++++++------- ShockOsc/Ui/Components/MudNavLinkFix.razor | 62 ++++++++ ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 8 +- ShockOsc/Ui/Components/Tabs/DebugTab.razor | 51 ++++--- ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 14 +- ShockOsc/Ui/Components/Tabs/LogsTab.razor | 2 +- ShockOsc/Ui/Components/Tabs/ShockersTab.razor | 42 +++--- ShockOsc/Ui/Components/UpdateLogout.razor | 48 +++--- .../Pages/Authentication/Authenticate.razor | 21 ++- ShockOsc/wwwroot/app.css | 17 ++- ShockOsc/wwwroot/index.html | 2 +- 17 files changed, 294 insertions(+), 168 deletions(-) rename ShockOsc/Backend/{BackendLiveApiManager.cs => BackendHubManager.cs} (90%) delete mode 100644 ShockOsc/Ui/Components/Layout/Logo.razor create mode 100644 ShockOsc/Ui/Components/MudNavLinkFix.razor diff --git a/ShockOsc/Backend/BackendLiveApiManager.cs b/ShockOsc/Backend/BackendHubManager.cs similarity index 90% rename from ShockOsc/Backend/BackendLiveApiManager.cs rename to ShockOsc/Backend/BackendHubManager.cs index d330efe..f96d62e 100644 --- a/ShockOsc/Backend/BackendLiveApiManager.cs +++ b/ShockOsc/Backend/BackendHubManager.cs @@ -11,44 +11,44 @@ namespace OpenShock.ShockOsc.Backend; -public sealed class BackendLiveApiManager +public sealed class BackendHubManager { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ConfigManager _configManager; - private readonly OpenShockApiLiveClient _openShockApiLiveClient; + private readonly OpenShockHubClient _openShockHubClient; private readonly OscClient _oscClient; private readonly ShockOscData _dataLayer; private readonly OscHandler _oscHandler; private string _liveConnectionId = string.Empty; - public BackendLiveApiManager(ILogger logger, + public BackendHubManager(ILogger logger, ConfigManager configManager, - OpenShockApiLiveClient openShockApiLiveClient, + OpenShockHubClient openShockHubClient, OscClient oscClient, ShockOscData dataLayer, OscHandler oscHandler) { _logger = logger; _configManager = configManager; - _openShockApiLiveClient = openShockApiLiveClient; + _openShockHubClient = openShockHubClient; _oscClient = oscClient; _dataLayer = dataLayer; _oscHandler = oscHandler; - _openShockApiLiveClient.OnWelcome += s => + _openShockHubClient.OnWelcome += s => { _liveConnectionId = s; return Task.CompletedTask; }; - _openShockApiLiveClient.OnLog += RemoteActivateShockers; + _openShockHubClient.OnLog += RemoteActivateShockers; } public async Task SetupLiveClient() { - await _openShockApiLiveClient.Setup(new ApiLiveClientOptions() + await _openShockHubClient.Setup(new HubClientOptions() { Token = _configManager.Config.OpenShock.Token, Server = _configManager.Config.OpenShock.Backend, @@ -93,7 +93,7 @@ public async Task ControlGroup(Guid groupId, uint duration, byte intensity Intensity = intensity, Type = type }); - await _openShockApiLiveClient.Control(controlCommandsAll); + await _openShockHubClient.Control(controlCommandsAll); return true; } @@ -108,7 +108,7 @@ public async Task ControlGroup(Guid groupId, uint duration, byte intensity Type = type }); - await _openShockApiLiveClient.Control(controlCommands); + await _openShockHubClient.Control(controlCommands); return true; } diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 187e0db..0890194 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -55,8 +55,8 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/ShockOsc/Services/BackendControlService.cs b/ShockOsc/Services/BackendControlService.cs index 6a9b78a..b357be0 100644 --- a/ShockOsc/Services/BackendControlService.cs +++ b/ShockOsc/Services/BackendControlService.cs @@ -5,10 +5,10 @@ namespace OpenShock.ShockOsc.Services; public sealed class BackendControlService { - private readonly BackendLiveApiManager _backendLiveApiManager; + private readonly BackendHubManager _backendHubManager; - public BackendControlService(BackendLiveApiManager backendLiveApiManager) + public BackendControlService(BackendHubManager backendHubManager) { - _backendLiveApiManager = backendLiveApiManager; + _backendHubManager = backendHubManager; } } \ No newline at end of file diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index 86c2a92..439e041 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -21,7 +21,7 @@ public sealed class ShockOsc { private readonly ILogger _logger; private readonly OscClient _oscClient; - private readonly BackendLiveApiManager _backendLiveApiManager; + private readonly BackendHubManager _backendHubManager; private readonly UnderscoreConfig _underscoreConfig; private readonly ConfigManager _configManager; private readonly OscQueryServer _oscQueryServer; @@ -56,7 +56,7 @@ public sealed class ShockOsc public ShockOsc(ILogger logger, OscClient oscClient, - BackendLiveApiManager backendLiveApiManager, + BackendHubManager backendHubManager, UnderscoreConfig underscoreConfig, ConfigManager configManager, OscQueryServer oscQueryServer, @@ -66,7 +66,7 @@ OscHandler oscHandler { _logger = logger; _oscClient = oscClient; - _backendLiveApiManager = backendLiveApiManager; + _backendHubManager = backendHubManager; _underscoreConfig = underscoreConfig; _configManager = configManager; _oscQueryServer = oscQueryServer; @@ -330,7 +330,7 @@ private async Task ReceiveLogic() else if (_configManager.Config.Behaviour.WhileBoneHeld != BehaviourConf.BoneHeldAction.None) { - await _backendLiveApiManager.CancelControl(programGroup); + await _backendHubManager.CancelControl(programGroup); } } @@ -397,7 +397,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i "Sending shock to {GroupName} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s", programGroup.Name, intensity, intensityPercentage, inSeconds); - await _backendLiveApiManager.ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock); + await _backendHubManager.ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock); if (!_configManager.Config.Osc.Chatbox) return; // Chatbox message local @@ -463,7 +463,7 @@ private async Task CheckLogic() programGroup.LastVibration = DateTime.UtcNow; _logger.LogDebug("Vibrating {Shocker} at {Intensity}", pos, vibrationIntensity); - await _backendLiveApiManager.ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, + await _backendHubManager.ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, _configManager.Config.Behaviour.WhileBoneHeld == BehaviourConf.BoneHeldAction.Shock ? ControlType.Shock diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index a5d079a..243d734 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -56,7 +56,7 @@ - + diff --git a/ShockOsc/Ui/Components/Layout/Logo.razor b/ShockOsc/Ui/Components/Layout/Logo.razor deleted file mode 100644 index 3374757..0000000 --- a/ShockOsc/Ui/Components/Layout/Logo.razor +++ /dev/null @@ -1,8 +0,0 @@ - - - - - ShockOSC - - - diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 34f2e52..cf57d2e 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,86 +1,121 @@ @using OpenShock.ShockOsc.Logging -@using OpenShock.ShockOsc.Ui.Components.Layout @using OpenShock.ShockOsc.Ui.Components.Tabs @using OpenShock.ShockOsc.Ui.Utils -@using OpenShock.ShockOsc.Config +@using OpenShock.SDK.CSharp.Live @inject ISnackbar Snackbar @inherits LayoutComponentBase +@inject OpenShockHubClient ApiHubClient @page "/main" + + - - - - @* *@ - @* *@ - @* Groups *@ - @* *@ - @* *@ - @* *@ - @* @switch(_currentTab) *@ - @* { *@ - @* case Tab.Groups: *@ - @* *@ - @* break; *@ - @* case Tab.Config: *@ - @* *@ - @* break; *@ - @* case Tab.Shockers: *@ - @* *@ - @* break; *@ - @* case Tab.Debug: *@ - @* *@ - @* break; *@ - @* case Tab.Logs: *@ - @* *@ - @* break; *@ - @* } *@ - - - - - - - - - - - - - - - - - - - - +
+ + + +
+ +
+
+ + ShockOSC + +
+ +
+ + + + + Groups + Config + Shockers + Debug + Logs + + +
+ +
+ + ShockOSC v@(Version) + Connection Status: @ApiHubClient.State + + +
+
+ +
+
+ + + + +
+ @switch (_currentTab) + { + case Tab.Groups: + + break; + case Tab.Config: + + break; + case Tab.Shockers: + + break; + case Tab.Debug: + + break; + case Tab.Logs: + + break; + } +
+ +
+
@code { - public enum Tab { Groups, Config, Shockers, Debug, Logs } + private static readonly string Version = typeof(MainLayout).Assembly.GetName().Version!.ToString(); + + public enum Tab + { + Groups, + Config, + Shockers, + Debug, + Logs + } private Tab _currentTab = Tab.Groups; - + private void NavigateTab(Tab tab) { _currentTab = tab; } - + private void MsgNoty(string msg, Severity severity) { Snackbar.Add(msg, severity); } - + protected override void OnInitialized() { UiLogSink.NotificationAction = MsgNoty; } + } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/MudNavLinkFix.razor b/ShockOsc/Ui/Components/MudNavLinkFix.razor new file mode 100644 index 0000000..196e493 --- /dev/null +++ b/ShockOsc/Ui/Components/MudNavLinkFix.razor @@ -0,0 +1,62 @@ +@namespace OpenShock.ShockOsc.Ui.Components +@using MudBlazor.Utilities +@using MudBlazor.Interfaces +@inherits MudBaseSelectItem + +
+ @{ +
+ @if (!string.IsNullOrEmpty(Icon)) + { + + } + +
} +
+ +@code { + + protected string Classname => + new CssBuilder("mud-nav-item") + .AddClass(Class) + .Build(); + + protected string LinkClassname => + new CssBuilder("mud-nav-link") + .AddClass($"mud-nav-link-disabled", Disabled) + .AddClass($"mud-ripple", !DisableRipple && !Disabled) + .AddClass(ActiveClass, IsActive) + .Build(); + + protected string IconClassname => + new CssBuilder("mud-nav-link-icon") + .AddClass($"mud-nav-link-icon-default", IconColor == Color.Default) + .Build(); + + /// + /// Icon to use if set. + /// + [Parameter] + [Category(CategoryTypes.NavMenu.Behavior)] + public string? Icon { get; set; } + + /// + /// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color. + /// + [Parameter] + [Category(CategoryTypes.NavMenu.Appearance)] + public Color IconColor { get; set; } = Color.Default; + + /// + /// User class names when active, separated by space. + /// + [Parameter] + [Category(CategoryTypes.ComponentBase.Common)] + public string ActiveClass { get; set; } = "active"; + + [Parameter] public bool IsActive { get; set; } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor index 2c44478..c0c74f5 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -10,7 +10,7 @@ @inject ShockOsc ShockOsc @inject UnderscoreConfig UnderscoreConfig - + Shocker Options @@ -60,7 +60,7 @@
- + Game Options @@ -68,7 +68,7 @@ - + Chatbox Options @@ -117,7 +117,7 @@ - + OSC Options diff --git a/ShockOsc/Ui/Components/Tabs/DebugTab.razor b/ShockOsc/Ui/Components/Tabs/DebugTab.razor index a03201a..795a056 100644 --- a/ShockOsc/Ui/Components/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DebugTab.razor @@ -3,31 +3,36 @@ @implements IAsyncDisposable @inject ShockOsc ShockOsc -List of ShockOSC prefixed parameters and their states -
-Avatar ID: @ShockOsc.AvatarId -
- - -
-@if (_showAllAvatarParams) -{ - @foreach (var param in ShockOsc.AllAvatarParams) + + + Avatar ID: @ShockOsc.AvatarId + + + OSC Parameters + + + +
+ @if (_showAllAvatarParams) { - + @foreach (var param in ShockOsc.AllAvatarParams) + { + + } } -} -else -{ - @foreach (var param in ShockOsc.ShockOscParams) + else { - + @foreach (var param in ShockOsc.ShockOscParams) + { + + } } -} + +
@code { private bool _showAllAvatarParams = false; - + private void OnParamsChange(bool shockOscParam) { // only redraw page when needed @@ -36,7 +41,7 @@ else _updateQueued = true; } - + private bool _updateQueued = true; protected override void OnInitialized() @@ -54,14 +59,14 @@ else _updateQueued = false; await InvokeAsync(StateHasChanged); - + await Task.Delay(200); } } - + private CancellationTokenSource _cts = new CancellationTokenSource(); - - + + public async ValueTask DisposeAsync() { await _cts.CancelAsync(); diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index ae8bc24..215a92e 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -24,7 +24,7 @@ public async Task DeleteGroup() { if (Group == null) return; - + ConfigManager.Config.Groups.Remove(Group.Value); Group = null; await InvokeAsync(StateHasChanged); @@ -41,7 +41,7 @@ private async Task OnGroupSelect() { if (CurrentGroup != null) _selectedShockers = [..CurrentGroup.Shockers]; - + await InvokeAsync(StateHasChanged); } @@ -81,7 +81,7 @@ @if (CurrentGroup != null) { - + Group Settings
@@ -89,12 +89,12 @@
- + Shockers in Group
- + Name @@ -106,5 +106,7 @@ } else { - Please select a group to edit + + Please select a group to edit + } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/LogsTab.razor b/ShockOsc/Ui/Components/Tabs/LogsTab.razor index 2500f74..285663d 100644 --- a/ShockOsc/Ui/Components/Tabs/LogsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/LogsTab.razor @@ -3,7 +3,7 @@ @using Serilog.Events @implements IDisposable - + Time Source diff --git a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor b/ShockOsc/Ui/Components/Tabs/ShockersTab.razor index 4d46260..a710787 100644 --- a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ShockersTab.razor @@ -3,27 +3,35 @@ @inject OpenShockApi OpenShockApi @inject ConfigManager ConfigManager -Refresh -
-
- - - Enabled - Name - Guid - - - - - - @context.Name - @context.Id - - + + + Refresh +
+
+
+ + + Enabled + Name + Guid + + + + + + @context.Name + @context.Id + + +
+ +
@code { + private Task OnShockerConfigUpdate() { return ConfigManager.SaveAsync(); } + } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Components/UpdateLogout.razor index 6298ca7..365490f 100644 --- a/ShockOsc/Ui/Components/UpdateLogout.razor +++ b/ShockOsc/Ui/Components/UpdateLogout.razor @@ -4,11 +4,9 @@ @inject IDialogService DialogService @inject ConfigManager ConfigManager @inject Updater Updater +@inject NavigationManager NavigationManager @code { - [Parameter] - public bool Authenticated { get; set; } - private readonly DialogOptions _dialogOptions = new() { NoHeader = true, DisableBackdropClick = true }; private void OpenUpdateDialog() @@ -20,23 +18,35 @@ { if (await Updater.CheckUpdate()) OpenUpdateDialog(); } -} - - @if (Updater.UpdateAvailable) + private async Task Logout() { - - - - - + ConfigManager.Config.OpenShock.Token = string.Empty; + await ConfigManager.SaveAsync(); + + NavigationManager.NavigateTo("/"); } - @if (Authenticated) - { - - - - - +} + +
+ + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index a5c5026..1bd4180 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -2,14 +2,13 @@ @using OpenShock.SDK.CSharp.Live @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config -@using OpenShock.ShockOsc.Ui.Components.Layout @using Microsoft.Extensions.Logging @inherits LayoutComponentBase @inject ConfigManager ConfigManager @inject NavigationManager NavigationManager -@inject BackendLiveApiManager LiveApiManager -@inject OpenShockApiLiveClient LiveApiClient +@inject BackendHubManager HubManager +@inject OpenShockHubClient HubClient @inject OpenShockApi ApiClient @inject ILogger Logger @@ -19,10 +18,18 @@ - + + + + + ShockOSC + + + + - + Login
@if (!Loading) @@ -61,9 +68,9 @@ Loading = true; Logger.LogInformation("Setting up live client"); - await LiveApiManager.SetupLiveClient(); + await HubManager.SetupLiveClient(); Logger.LogInformation("Starting live client"); - await LiveApiClient.StartAsync(); + await HubClient.StartAsync(); Logger.LogInformation("Refreshing shockers"); await ApiClient.RefreshShockers(); diff --git a/ShockOsc/wwwroot/app.css b/ShockOsc/wwwroot/app.css index 53a80f9..f6d693a 100644 --- a/ShockOsc/wwwroot/app.css +++ b/ShockOsc/wwwroot/app.css @@ -13,15 +13,11 @@ html, body { } body { - background-color: #2f2f2f; - padding: 20px; + --mud-palette-background: #212121; + background-color: #212121; min-width: 600px; } -#login-background { - background-color: #272727; -} - .mud-tabs-toolbar { background-color: #1f1f1f !important; } @@ -38,3 +34,12 @@ body { .option-checkbox-height { padding-top: 10px !important; } + +.mud-paper-padding-margin { + padding: 10px 15px; + margin: 20px 0; +} + +.mud-paper-padding { + padding: 10px 15px; +} \ No newline at end of file diff --git a/ShockOsc/wwwroot/index.html b/ShockOsc/wwwroot/index.html index 9e32183..5ed7b49 100644 --- a/ShockOsc/wwwroot/index.html +++ b/ShockOsc/wwwroot/index.html @@ -1,5 +1,5 @@ - + From 055d3b32feb779eb7f0edd128ee0f2f168091969 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 14 Apr 2024 08:42:57 +0200 Subject: [PATCH 31/95] connection state display, live control impl --- ShockOsc/Backend/BackendHubManager.cs | 34 +-- ShockOsc/Backend/OpenShockApi.cs | 11 +- ShockOsc/MauiProgram.cs | 12 +- ShockOsc/OscQueryLibrary/OscQueryServer.cs | 1 + ShockOsc/Services/LiveControlManager.cs | 200 ++++++++++++++++++ ShockOsc/Services/ShockOsc.cs | 24 +-- ShockOsc/ShockOsc.csproj | 13 +- ShockOsc/Ui/Components/MainLayout.razor | 100 ++++++++- ShockOsc/Ui/Components/Parts/StatePart.razor | 12 ++ .../Ui/Components/Parts/StatePart.razor.cs | 19 ++ ShockOsc/Ui/Components/Tabs/DebugTab.razor | 3 +- ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 1 + .../Pages/Authentication/Authenticate.razor | 5 + ShockOsc/Ui/Utils/StringUtils.cs | 7 + ShockOsc/Utils/AsynchronousEventExtensions.cs | 20 -- global.json | 7 + 16 files changed, 403 insertions(+), 66 deletions(-) create mode 100644 ShockOsc/Services/LiveControlManager.cs create mode 100644 ShockOsc/Ui/Components/Parts/StatePart.razor create mode 100644 ShockOsc/Ui/Components/Parts/StatePart.razor.cs create mode 100644 ShockOsc/Ui/Utils/StringUtils.cs delete mode 100644 ShockOsc/Utils/AsynchronousEventExtensions.cs create mode 100644 global.json diff --git a/ShockOsc/Backend/BackendHubManager.cs b/ShockOsc/Backend/BackendHubManager.cs index f96d62e..00ee516 100644 --- a/ShockOsc/Backend/BackendHubManager.cs +++ b/ShockOsc/Backend/BackendHubManager.cs @@ -1,7 +1,9 @@ using System.Globalization; using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Hub; +using OpenShock.SDK.CSharp.Hub.Models; using OpenShock.SDK.CSharp.Live; -using OpenShock.SDK.CSharp.Live.Models; +using OpenShock.SDK.CSharp.Live.LiveControlModels; using OpenShock.SDK.CSharp.Models; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; @@ -21,7 +23,7 @@ public sealed class BackendHubManager private readonly OscHandler _oscHandler; private string _liveConnectionId = string.Empty; - + public BackendHubManager(ILogger logger, ConfigManager configManager, OpenShockHubClient openShockHubClient, @@ -60,7 +62,7 @@ await _openShockHubClient.Setup(new HubClientOptions() } }); } - + /// /// Send a stop command for every shocker in a group /// @@ -71,7 +73,7 @@ public Task CancelControl(ProgramGroup programGroup) _logger.LogTrace("Cancelling action"); return ControlGroup(programGroup.Id, 0, 0, ControlType.Stop); } - + /// /// Control a group, if guid is empty, all shockers will be controlled /// @@ -82,7 +84,7 @@ public Task CancelControl(ProgramGroup programGroup) /// public async Task ControlGroup(Guid groupId, uint duration, byte intensity, ControlType type) { - if(groupId == Guid.Empty) + if (groupId == Guid.Empty) { var controlCommandsAll = _configManager.Config.OpenShock.Shockers .Where(x => x.Value.Enabled) @@ -96,8 +98,8 @@ public async Task ControlGroup(Guid groupId, uint duration, byte intensity await _openShockHubClient.Control(controlCommandsAll); return true; } - - + + if (!_configManager.Config.Groups.TryGetValue(groupId, out var group)) return false; var controlCommands = group.Shockers.Select(x => new Control @@ -111,7 +113,7 @@ public async Task ControlGroup(Guid groupId, uint duration, byte intensity await _openShockHubClient.Control(controlCommands); return true; } - + private async Task RemoteActivateShockers(ControlLogSender sender, ICollection logs) { if (sender.ConnectionId == _liveConnectionId) @@ -119,7 +121,7 @@ private async Task RemoteActivateShockers(ControlLogSender sender, ICollection s.Value.Shockers.Any(x => x == log.Shocker.Id)).Select(x => x.Key).ToArray(); - var programGroupsAffected = _dataLayer.ProgramGroups.Where(x => configGroupsAffected.Contains(x.Key)).Select(x => x.Value); + + var configGroupsAffected = _configManager.Config.Groups + .Where(s => s.Value.Shockers.Any(x => x == log.Shocker.Id)).Select(x => x.Key).ToArray(); + var programGroupsAffected = _dataLayer.ProgramGroups.Where(x => configGroupsAffected.Contains(x.Key)) + .Select(x => x.Value); var oneShock = false; foreach (var pain in programGroupsAffected) { switch (log.Type) - + { case ControlType.Shock: { pain.LastIntensity = log.Intensity; pain.LastDuration = log.Duration; pain.LastExecuted = log.ExecutedAt; - + oneShock = true; break; } @@ -187,7 +191,7 @@ private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log _logger.LogError("ControlType was out of range. Value was: {Type}", log.Type); break; } - + if (oneShock) { await _oscHandler.ForceUnmute(); diff --git a/ShockOsc/Backend/OpenShockApi.cs b/ShockOsc/Backend/OpenShockApi.cs index 320d213..22f2e73 100644 --- a/ShockOsc/Backend/OpenShockApi.cs +++ b/ShockOsc/Backend/OpenShockApi.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.Logging; +using OneOf; +using OneOf.Types; using OpenShock.SDK.CSharp; -using OpenShock.SDK.CSharp.Live.Utils; using OpenShock.SDK.CSharp.Models; +using OpenShock.SDK.CSharp.Utils; using OpenShock.ShockOsc.Config; namespace OpenShock.ShockOsc.Backend; @@ -30,6 +32,7 @@ public void SetupApiClient() public event Func, Task>? OnShockersUpdated; + public IReadOnlyCollection Devices = Array.Empty(); public IReadOnlyCollection Shockers = Array.Empty(); public async Task RefreshShockers() @@ -38,6 +41,7 @@ public async Task RefreshShockers() response.Switch(success => { + Devices = success.Value; Shockers = success.Value.SelectMany(x => x.Shockers).ToArray(); // re-populate config with previous data if present, this also deletes any shockers that are no longer present @@ -66,4 +70,9 @@ public async Task RefreshShockers() // TODO: handle unauthenticated error }); } + + public + Task, NotFound, DeviceOffline, DeviceNotConnectedToGateway, UnauthenticatedError>> + GetDeviceGateway(Guid deviceId, CancellationToken cancellationToken = default) => + _client.GetDeviceGateway(deviceId, cancellationToken); } \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 0890194..8e6af59 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,7 +1,6 @@ using System.Net; -using Microsoft.Extensions.Logging; using MudBlazor.Services; -using OpenShock.SDK.CSharp.Live; +using OpenShock.SDK.CSharp.Hub; using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Logging; @@ -57,8 +56,9 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - - + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(provider => @@ -68,7 +68,7 @@ public static MauiApp CreateMauiApp() return new OscQueryServer("ShockOsc", listenAddress, config); }); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddMudServices(); @@ -88,7 +88,7 @@ public static MauiApp CreateMauiApp() var app = builder.Build(); // <---- Warmup ----> - app.Services.GetRequiredService(); + app.Services.GetRequiredService(); app.Services.GetRequiredService().Start(); return app; diff --git a/ShockOsc/OscQueryLibrary/OscQueryServer.cs b/ShockOsc/OscQueryLibrary/OscQueryServer.cs index 1757f5a..e995422 100644 --- a/ShockOsc/OscQueryLibrary/OscQueryServer.cs +++ b/ShockOsc/OscQueryLibrary/OscQueryServer.cs @@ -9,6 +9,7 @@ using EmbedIO.Actions; using Microsoft.Extensions.Hosting; using OpenShock.SDK.CSharp.Live.Utils; +using OpenShock.SDK.CSharp.Utils; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Utils; diff --git a/ShockOsc/Services/LiveControlManager.cs b/ShockOsc/Services/LiveControlManager.cs new file mode 100644 index 0000000..2d5fd78 --- /dev/null +++ b/ShockOsc/Services/LiveControlManager.cs @@ -0,0 +1,200 @@ +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Hub; +using OpenShock.SDK.CSharp.Hub.Models; +using OpenShock.SDK.CSharp.Live; +using OpenShock.SDK.CSharp.Live.LiveControlModels; +using OpenShock.SDK.CSharp.Models; +using OpenShock.SDK.CSharp.Utils; +using OpenShock.ShockOsc.Backend; +using OpenShock.ShockOsc.Config; + +namespace OpenShock.ShockOsc.Services; + +public sealed class LiveControlManager +{ + private readonly ILogger _logger; + private readonly OpenShockApi _api; + private readonly ConfigManager _configManager; + private readonly ILogger _liveControlLogger; + private readonly OpenShockHubClient _hubClient; + private readonly OpenShockApi _apiClient; + private readonly SemaphoreSlim _refreshLock = new(1, maxCount: 1); + + public LiveControlManager( + ILogger logger, + OpenShockApi api, + ConfigManager configManager, + ILogger liveControlLogger, + OpenShockHubClient hubClient, + OpenShockApi apiClient) + { + _logger = logger; + _api = api; + _configManager = configManager; + _liveControlLogger = liveControlLogger; + _hubClient = hubClient; + _apiClient = apiClient; + + _hubClient.OnDeviceStatus += HubClientOnDeviceStatus; + _hubClient.OnDeviceUpdate += HubClientOnDeviceUpdate; + } + + public event Func? OnStateUpdated; + + private async Task HubClientOnDeviceUpdate(Guid device, DeviceUpdateType type) + { + _logger.LogDebug("Device update received, updating shockers and refreshing connections"); + + await _apiClient.RefreshShockers(); + await RefreshConnections(); + } + + private async Task HubClientOnDeviceStatus(IEnumerable deviceStatus) + { + _logger.LogDebug("Device status received, refreshing connections"); + await RefreshConnections(); + } + + public Dictionary LiveControlClients { get; } = new(); + + public async Task RefreshConnections() + { + await _refreshLock.WaitAsync(); + try + { + await RefreshInternal(); + } + finally + { + _refreshLock.Release(); + } + } + + private async Task RefreshInternal() + { + _logger.LogDebug("Refreshing live control connections"); + + // Remove devices that dont exist anymore + foreach (var liveControlClient in LiveControlClients) + { + if (_api.Devices.Any(x => x.Id == liveControlClient.Key)) continue; + if (!LiveControlClients.Remove(liveControlClient.Key, out var removedClient)) + await removedClient!.DisposeAsync(); + } + + foreach (var device in _api.Devices) + { + if (LiveControlClients.ContainsKey(device.Id)) continue; + + _logger.LogTrace("Creating live control client for device [{DeviceId}]", device.Id); + + _logger.LogTrace("Getting device gateway for device [{DeviceId}]", device.Id); + var deviceGateway = await _apiClient.GetDeviceGateway(device.Id); + + deviceGateway.Switch(success => + { + var gateway = success.Value; + _logger.LogTrace("Got device gateway for device [{DeviceId}] [{Gateway}]", device.Id, + gateway.Gateway); + + var client = new OpenShockLiveControlClient(gateway.Gateway, device.Id, + _configManager.Config.OpenShock.Token, _liveControlLogger); + LiveControlClients.Add(device.Id, client); + + client.OnStateUpdate += async (state) => + { + _logger.LogTrace("Live control client for device [{DeviceId}] status updated {Status}", + device.Id, state); + await OnStateUpdated.Raise(); + }; + + client.OnDeviceNotConnected += async () => + { + _logger.LogInformation("Live control client for device [{DeviceId}] ending, device disconnected", device.Id); + // Dispose the client so it gets removed from the list and co + await client.DisposeAsync(); + }; + + // When the client shuts down, remove it from the list + client.OnDispose += async () => + { + _logger.LogTrace("Live control client for device [{DeviceId}] disposed, removing from list", + device.Id); + if (!LiveControlClients.Remove(device.Id, out var removedClient)) return; + await removedClient.DisposeAsync(); // Dispose incase it was not disposed + + await OnStateUpdated.Raise(); + }; + + client.InitializeAsync(); + }, + found => + { + _logger.LogError( + "Failed to get device gateway for device [{DeviceId}], not found or no permission", + device.Id); + }, + offline => + { + _logger.LogInformation("Failed to get device gateway for device [{DeviceId}], device offline", + device.Id); + }, + gateway => + { + _logger.LogError( + "Failed to get device gateway for device [{DeviceId}], " + + "the device is online but its not connected to a gateway, this means the device is probably" + + " outdated and does not support live control. Please upgrade your device", + device.Id); + }, unauthenticated => + { + _logger.LogError( + "Failed to get device gateway for device [{DeviceId}], we are not authenticated", + device.Id); + // TODO: Handle unauthenticated globally + }); + } + + await OnStateUpdated.Raise(); + } + + public Task ControlGroupFrame(Guid groupId, float intensity) + { + if (groupId == Guid.Empty) + { + var controlTasks = LiveControlClients + .Select(clientPair => + { + var apiDevice = _apiClient.Devices + .FirstOrDefault(x => x.Id == clientPair.Key); + if (apiDevice == null) return Task.CompletedTask; + + return ControlFrame(apiDevice.Shockers.Where(x => _configManager.Config.OpenShock.Shockers.Any(y => y.Key == x.Id && y.Value.Enabled)).Select(x => x.Id), clientPair.Value, intensity); + }); + + return Task.WhenAll(controlTasks); + } + + if (LiveControlClients.TryGetValue(groupId, out var client) && + _configManager.Config.Groups.TryGetValue(groupId, out var group)) + return ControlFrame(group.Shockers, client, intensity); + + return Task.CompletedTask; + } + + private async Task ControlFrame(IEnumerable shockers, IOpenShockLiveControlClient client, + float vibrationIntensity) + { + var shockersFrames = shockers.Select(x => client.SendFrame(new ClientLiveFrame + { + Type = _configManager.Config.Behaviour.WhileBoneHeld == + BehaviourConf.BoneHeldAction.Shock + ? ControlType.Shock + : ControlType.Vibrate, + Intensity = (byte)vibrationIntensity, + Shocker = x + })); + + await Task.WhenAll(shockersFrames); + } +} \ No newline at end of file diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index 439e041..91ac432 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -2,20 +2,18 @@ using System.Net; using LucHeart.CoreOSC; using Microsoft.Extensions.Logging; -using OpenShock.SDK.CSharp.Live; -using OpenShock.SDK.CSharp.Live.Models; using OpenShock.SDK.CSharp.Models; +using OpenShock.SDK.CSharp.Utils; using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.OscQueryLibrary; -using OpenShock.ShockOsc.Services; using OpenShock.ShockOsc.Utils; using SmartFormat; #pragma warning disable CS4014 -namespace OpenShock.ShockOsc; +namespace OpenShock.ShockOsc.Services; public sealed class ShockOsc { @@ -27,6 +25,7 @@ public sealed class ShockOsc private readonly OscQueryServer _oscQueryServer; private readonly ShockOscData _dataLayer; private readonly OscHandler _oscHandler; + private readonly LiveControlManager _liveControlManager; private bool _oscServerActive; private bool _isAfk; @@ -61,8 +60,7 @@ public ShockOsc(ILogger logger, ConfigManager configManager, OscQueryServer oscQueryServer, ShockOscData dataLayer, - OscHandler oscHandler - ) + OscHandler oscHandler, LiveControlManager liveControlManager) { _logger = logger; _oscClient = oscClient; @@ -72,8 +70,9 @@ OscHandler oscHandler _oscQueryServer = oscQueryServer; _dataLayer = dataLayer; _oscHandler = oscHandler; + _liveControlManager = liveControlManager; + - OnGroupsChanged += SetupGroups; oscQueryServer.FoundVrcClient += FoundVrcClient; @@ -455,7 +454,7 @@ private async Task CheckLogic() BehaviourConf.BoneHeldAction.None && !isActiveOrOnCooldown && programGroup.IsGrabbed && - programGroup.LastVibration < DateTime.UtcNow.Subtract(TimeSpan.FromMilliseconds(300))) + programGroup.LastVibration < DateTime.UtcNow.Subtract(TimeSpan.FromMilliseconds(100))) { var vibrationIntensity = programGroup.LastStretchValue * 100f; if (vibrationIntensity < 1) @@ -463,11 +462,8 @@ private async Task CheckLogic() programGroup.LastVibration = DateTime.UtcNow; _logger.LogDebug("Vibrating {Shocker} at {Intensity}", pos, vibrationIntensity); - await _backendHubManager.ControlGroup(programGroup.Id, 1000, (byte)vibrationIntensity, - _configManager.Config.Behaviour.WhileBoneHeld == - BehaviourConf.BoneHeldAction.Shock - ? ControlType.Shock - : ControlType.Vibrate); + + await _liveControlManager.ControlGroupFrame(programGroup.Id, vibrationIntensity); } if (programGroup.TriggerMethod == TriggerMethod.None) @@ -512,6 +508,8 @@ await _backendHubManager.ControlGroup(programGroup.Id, 1000, (byte)vibrationInte } } + + private uint GetDuration() { var config = _configManager.Config.Behaviour; diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 243d734..efb3ce3 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -47,18 +47,19 @@
- + - - + + - - + + + - + diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index cf57d2e..4c25c9e 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,10 +1,18 @@ +@using Microsoft.AspNetCore.SignalR.Client +@using OpenShock.SDK.CSharp.Hub +@using OpenShock.SDK.CSharp.Live.LiveControlModels @using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Tabs @using OpenShock.ShockOsc.Ui.Utils -@using OpenShock.SDK.CSharp.Live +@using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Services +@using OpenShock.ShockOsc.Ui.Components.Parts @inject ISnackbar Snackbar @inherits LayoutComponentBase @inject OpenShockHubClient ApiHubClient +@inject OpenShockApi Api +@inject LiveControlManager LiveControlManager +@implements IDisposable @page "/main" @@ -12,7 +20,7 @@ .childs-width-100 > * { width: 100%; } - + @@ -50,8 +58,30 @@
ShockOSC v@(Version) - Connection Status: @ApiHubClient.State - + +
+ + + @foreach (var device in Api.Devices) + { + if (LiveControlManager.LiveControlClients.TryGetValue(device.Id, out var client)) + { + + } + else + { + + } + } + + +
+
@@ -116,6 +146,68 @@ protected override void OnInitialized() { UiLogSink.NotificationAction = MsgNoty; + + ApiHubClient.Reconnecting += ApiHubClientOnReconnecting; + ApiHubClient.Reconnected += ApiHubClientOnReconnected; + ApiHubClient.Closed += ApiHubClientOnClosed; + + LiveControlManager.OnStateUpdated += LiveControlManagerOnOnStateUpdated; + } + + private Task LiveControlManagerOnOnStateUpdated() + { + return InvokeAsync(StateHasChanged); + } + + private Task ApiHubClientOnReconnected(string? arg) => InvokeAsync(() => + { + StateHasChanged(); + Snackbar.Add("Reconnected to hub", Severity.Success); + }); + + + private Task ApiHubClientOnReconnecting(Exception? arg) => InvokeAsync(() => + { + StateHasChanged(); + Snackbar.Add("Reconnecting to hub...", Severity.Warning); + }); + + private Task ApiHubClientOnClosed(Exception? arg) => InvokeAsync(() => + { + StateHasChanged(); + Snackbar.Add("Disconnected from hub", Severity.Error); + }); + + + private Color GetConnectionStateColor(HubConnectionState state) => + state switch + { + HubConnectionState.Connected => Color.Success, + HubConnectionState.Reconnecting => Color.Warning, + HubConnectionState.Connecting => Color.Warning, + HubConnectionState.Disconnected => Color.Error, + _ => Color.Error + }; + + private Color GetConnectionStateColor(WebsocketConnectionState state) => + state switch + { + WebsocketConnectionState.Connected => Color.Success, + WebsocketConnectionState.Reconnecting => Color.Warning, + WebsocketConnectionState.Connecting => Color.Warning, + WebsocketConnectionState.Disconnected => Color.Error, + _ => Color.Error + }; + + public void Dispose() + { + Snackbar?.Dispose(); + + ApiHubClient.Reconnecting -= ApiHubClientOnReconnecting; + ApiHubClient.Reconnected -= ApiHubClientOnReconnected; + ApiHubClient.Closed -= ApiHubClientOnClosed; + + LiveControlManager.OnStateUpdated -= LiveControlManagerOnOnStateUpdated; } } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Parts/StatePart.razor b/ShockOsc/Ui/Components/Parts/StatePart.razor new file mode 100644 index 0000000..c118b52 --- /dev/null +++ b/ShockOsc/Ui/Components/Parts/StatePart.razor @@ -0,0 +1,12 @@ +
+ @Text + + + +
+ + \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Parts/StatePart.razor.cs b/ShockOsc/Ui/Components/Parts/StatePart.razor.cs new file mode 100644 index 0000000..cc8eacf --- /dev/null +++ b/ShockOsc/Ui/Components/Parts/StatePart.razor.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Components; +using Color = MudBlazor.Color; + +namespace OpenShock.ShockOsc.Ui.Components.Parts; + +public partial class StatePart : ComponentBase +{ + [Parameter] + public required Color IconColor { get; set; } + + [Parameter] + public required string Icon { get; set; } + + [Parameter] + public required string Tooltip { get; set; } + + [Parameter] + public required string Text { get; set; } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/DebugTab.razor b/ShockOsc/Ui/Components/Tabs/DebugTab.razor index 795a056..05808d2 100644 --- a/ShockOsc/Ui/Components/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DebugTab.razor @@ -1,4 +1,5 @@ -@using OpenShock.ShockOsc.Utils +@using OpenShock.ShockOsc.Services +@using OpenShock.ShockOsc.Utils @using Serilog.Sinks.SystemConsole.Themes @implements IAsyncDisposable @inject ShockOsc ShockOsc diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index 215a92e..d6d5625 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -1,6 +1,7 @@ @using System.Text.RegularExpressions @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config +@using OpenShock.ShockOsc.Services @inject OpenShockApi OpenShockApi @inject ShockOsc ShockOsc @inject ConfigManager ConfigManager diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index 1bd4180..a38f337 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -3,6 +3,8 @@ @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config @using Microsoft.Extensions.Logging +@using OpenShock.SDK.CSharp.Hub +@using OpenShock.ShockOsc.Services @inherits LayoutComponentBase @inject ConfigManager ConfigManager @@ -11,6 +13,7 @@ @inject OpenShockHubClient HubClient @inject OpenShockApi ApiClient @inject ILogger Logger +@inject LiveControlManager LiveControlManager @page "/" @@ -75,6 +78,8 @@ Logger.LogInformation("Refreshing shockers"); await ApiClient.RefreshShockers(); + await LiveControlManager.RefreshConnections(); + NavigationManager.NavigateTo("main"); } } \ No newline at end of file diff --git a/ShockOsc/Ui/Utils/StringUtils.cs b/ShockOsc/Ui/Utils/StringUtils.cs new file mode 100644 index 0000000..d7bdca9 --- /dev/null +++ b/ShockOsc/Ui/Utils/StringUtils.cs @@ -0,0 +1,7 @@ +namespace OpenShock.ShockOsc.Ui.Utils; + +public static class StringUtils +{ + public static string Truncate(this string input, int maxLength) => input[..Math.Min(maxLength, input.Length)]; + +} \ No newline at end of file diff --git a/ShockOsc/Utils/AsynchronousEventExtensions.cs b/ShockOsc/Utils/AsynchronousEventExtensions.cs deleted file mode 100644 index 387589f..0000000 --- a/ShockOsc/Utils/AsynchronousEventExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Net; -using OpenShock.ShockOsc.OscQueryLibrary; - -namespace OpenShock.ShockOsc.Utils; - -public static class AsynchronousEventExtensions -{ - public static Task Raise(this Func? handlers) - { - if (handlers != null) - { - return Task.WhenAll(handlers.GetInvocationList() - .OfType>() - .Select(h => h())); - } - - return Task.CompletedTask; - } - -} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..dad2db5 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file From 62d1a9a7e43fc303609872ecf5e394ab76940cad Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 15 Apr 2024 21:00:31 +0200 Subject: [PATCH 32/95] auth page improvements --- ShockOsc/ShockOsc.csproj | 3 +- .../Pages/Authentication/Authenticate.razor | 86 +++++++++++++------ 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index efb3ce3..412d9eb 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -17,8 +17,7 @@ OpenShock.ShockOsc OpenShock.ShockOsc OpenShock - 2.0.1 - 2.0.1 + 2.0.0 Resources\openshock-icon.ico true ShockOsc diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index a38f337..7174c4b 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -14,6 +14,7 @@ @inject OpenShockApi ApiClient @inject ILogger Logger @inject LiveControlManager LiveControlManager +@inject ISnackbar Snackbar @page "/" @@ -23,7 +24,7 @@ - + ShockOSC @@ -35,30 +36,54 @@ Login
- @if (!Loading) + @switch (_currentState) { - -
- Continue -
- } - else - { - + case State.Login: + +
+ Continue +
+ break; + case State.Loading: + + break; + case State.Failed: + Failed to authenticate + Re-login + break; + case State.Authenticated: + Successful, redirecting... + break; }
-
+
@code { + + private enum State + { + Login, + Loading, + Authenticated, + Failed + } + + private State _currentState = State.Login; + private bool Loading { get; set; } protected override async Task OnInitializedAsync() { - if(string.IsNullOrEmpty(ConfigManager.Config.OpenShock.Token)) return; + if (string.IsNullOrEmpty(ConfigManager.Config.OpenShock.Token)) return; await ProceedAuthenticated(); } + public void ReLogin() + { + _currentState = State.Login; + } + public async Task Login() { await ConfigManager.SaveAsync(); @@ -69,17 +94,30 @@ private async Task ProceedAuthenticated() { Loading = true; - - Logger.LogInformation("Setting up live client"); - await HubManager.SetupLiveClient(); - Logger.LogInformation("Starting live client"); - await HubClient.StartAsync(); - - Logger.LogInformation("Refreshing shockers"); - await ApiClient.RefreshShockers(); - - await LiveControlManager.RefreshConnections(); - - NavigationManager.NavigateTo("main"); + _currentState = State.Loading; + + try + { + Logger.LogInformation("Setting up live client"); + await HubManager.SetupLiveClient(); + Logger.LogInformation("Starting live client"); + await HubClient.StartAsync(); + + Logger.LogInformation("Refreshing shockers"); + await ApiClient.RefreshShockers(); + + await LiveControlManager.RefreshConnections(); + + + _currentState = State.Authenticated; + + NavigationManager.NavigateTo("main"); + } + catch (Exception e) + { + _currentState = State.Failed; + Snackbar.Add("Failed to authenticate", Severity.Error); + } } + } \ No newline at end of file From 703f636113e2c6773259e8fbbbebf736f185199c Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 18 Apr 2024 20:26:00 +0200 Subject: [PATCH 33/95] Add le sys tray --- ShockOsc/MauiProgram.cs | 34 +++++--- .../Platforms/Windows/WindowsTrayService.cs | 81 +++++++++++++++++++ ShockOsc/Services/ITrayService.cs | 9 +++ ShockOsc/ShockOsc.csproj | 2 +- ShockOsc/Ui/App.xaml.cs | 23 ------ ShockOsc/Ui/{App.xaml => MauiApp.xaml} | 2 +- ShockOsc/Ui/MauiApp.xaml.cs | 20 +++++ 7 files changed, 136 insertions(+), 35 deletions(-) create mode 100644 ShockOsc/Platforms/Windows/WindowsTrayService.cs create mode 100644 ShockOsc/Services/ITrayService.cs delete mode 100644 ShockOsc/Ui/App.xaml.cs rename ShockOsc/Ui/{App.xaml => MauiApp.xaml} (94%) create mode 100644 ShockOsc/Ui/MauiApp.xaml.cs diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 8e6af59..386e7d7 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,4 +1,8 @@ using System.Net; +using System.Windows.Controls; +using Microsoft.Maui.LifecycleEvents; +using Microsoft.UI; +using Microsoft.UI.Windowing; using MudBlazor.Services; using OpenShock.SDK.CSharp.Hub; using OpenShock.ShockOsc.Backend; @@ -8,14 +12,16 @@ using OpenShock.ShockOsc.Services; using OpenShock.ShockOsc.Ui; using Serilog; +using MauiApp = OpenShock.ShockOsc.Ui.MauiApp; +using MenuItem = System.Windows.Controls.MenuItem; namespace OpenShock.ShockOsc; public static class MauiProgram { - public static MauiApp CreateMauiApp() + public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() { - var builder = MauiApp.CreateBuilder(); + var builder = Microsoft.Maui.Hosting.MauiApp.CreateBuilder(); // <---- Services ----> @@ -45,9 +51,9 @@ public static MauiApp CreateMauiApp() Log.Logger = loggerConfiguration.CreateLogger(); builder.Services.AddSerilog(Log.Logger); - + builder.Services.AddSingleton(); - + builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -56,9 +62,9 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - + builder.Services.AddSingleton(); - + builder.Services.AddSingleton(); builder.Services.AddSingleton(provider => @@ -67,17 +73,23 @@ public static MauiApp CreateMauiApp() var listenAddress = config.Config.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; return new OscQueryServer("ShockOsc", listenAddress, config); }); - + builder.Services.AddSingleton(); builder.Services.AddSingleton(); + +#if WINDOWS + builder.Services.AddSingleton(); +#endif + + builder.Services.AddMudServices(); builder.Services.AddMauiBlazorWebView(); // <---- App ----> builder - .UseMauiApp() + .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); @@ -86,11 +98,13 @@ public static MauiApp CreateMauiApp() #endif var app = builder.Build(); - + + app.Services.GetService()?.Initialize(); + // <---- Warmup ----> app.Services.GetRequiredService(); app.Services.GetRequiredService().Start(); - + return app; } } \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs new file mode 100644 index 0000000..6162972 --- /dev/null +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -0,0 +1,81 @@ +using System.Drawing; +using System.Windows.Forms; +using Microsoft.UI; +using Microsoft.UI.Windowing; +using OpenShock.SDK.CSharp.Hub; +using OpenShock.ShockOsc.Services; +using Application = Microsoft.Maui.Controls.Application; +using Image = System.Drawing.Image; + +namespace OpenShock.ShockOsc; + +public class WindowsTrayService : ITrayService +{ + private readonly OpenShockHubClient _apiHubClient; + + public WindowsTrayService(OpenShockHubClient apiHubClient) + { + _apiHubClient = apiHubClient; + + _apiHubClient.Reconnecting += _ => HubStateChanged(); + _apiHubClient.Reconnected += _ => HubStateChanged(); + _apiHubClient.Closed += _ => HubStateChanged(); + } + + private ToolStripLabel _stateLabel; + + private Task HubStateChanged() + { + _stateLabel.Text = "State: " + _apiHubClient.State; + return Task.CompletedTask; + } + + public void Initialize() + { + var tray = new NotifyIcon(); + tray.Icon = Icon.ExtractAssociatedIcon(@"Resources\openshock-icon.ico"); + tray.Text = "ShockOSC"; + + var menu = new ContextMenuStrip(); + + menu.Items.Add("ShockOSC", Image.FromFile(@"Resources\openshock-icon.ico"), OnMainClick); + menu.Items.Add(new ToolStripSeparator()); + _stateLabel = new ToolStripLabel("State: " + _apiHubClient.State); + menu.Items.Add(_stateLabel); + menu.Items.Add(new ToolStripSeparator()); + menu.Items.Add("Restart", null, Restart); + menu.Items.Add("Quit ShockOSC", null, OnQuitClick); + + tray.ContextMenuStrip = menu; + + tray.Click += OnMainClick; + + tray.Visible = true; + } + + private void Restart(object? sender, EventArgs e) + { + Application.Current?.Quit(); + } + + private static void OnMainClick(object? sender, EventArgs eventArgs) + { + if (eventArgs is MouseEventArgs mouseEventArgs && mouseEventArgs.Button != MouseButtons.Left) return; + + var window = Application.Current?.Windows[0]; + var nativeWindow = window?.Handler?.PlatformView; + if (nativeWindow == null) return; + + var windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow); + var windowId = Win32Interop.GetWindowIdFromWindow(windowHandle); + var appWindow = AppWindow.GetFromWindowId(windowId); + + if (appWindow.IsVisible) appWindow.Hide(); + else appWindow.Show(); + } + + private static void OnQuitClick(object? sender, EventArgs eventArgs) + { + Application.Current?.Quit(); + } +} \ No newline at end of file diff --git a/ShockOsc/Services/ITrayService.cs b/ShockOsc/Services/ITrayService.cs new file mode 100644 index 0000000..8850f97 --- /dev/null +++ b/ShockOsc/Services/ITrayService.cs @@ -0,0 +1,9 @@ +namespace OpenShock.ShockOsc.Services; + +public interface ITrayService +{ + /// + /// Setup the tray icon and make it visible + /// + public void Initialize(); +} \ No newline at end of file diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 412d9eb..175c75f 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -32,7 +32,7 @@ None Resources\Icon512.png - + diff --git a/ShockOsc/Ui/App.xaml.cs b/ShockOsc/Ui/App.xaml.cs deleted file mode 100644 index 72de8f2..0000000 --- a/ShockOsc/Ui/App.xaml.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OpenShock.ShockOsc.Ui; - -public partial class App : Application -{ - public App() - { - InitializeComponent(); - MainPage = new MainPage(); - } - - protected override Window CreateWindow(IActivationState activationState) - { - var window = base.CreateWindow(activationState); - if (window != null) - { - window.Title = "ShockOSC"; - window.MinimumHeight = 600; - window.MinimumWidth = 1000; - } - - return window; - } -} \ No newline at end of file diff --git a/ShockOsc/Ui/App.xaml b/ShockOsc/Ui/MauiApp.xaml similarity index 94% rename from ShockOsc/Ui/App.xaml rename to ShockOsc/Ui/MauiApp.xaml index ac2c6bc..f772086 100644 --- a/ShockOsc/Ui/App.xaml +++ b/ShockOsc/Ui/MauiApp.xaml @@ -1,7 +1,7 @@  + x:Class="OpenShock.ShockOsc.Ui.MauiApp"> diff --git a/ShockOsc/Ui/MauiApp.xaml.cs b/ShockOsc/Ui/MauiApp.xaml.cs new file mode 100644 index 0000000..0a2bf3e --- /dev/null +++ b/ShockOsc/Ui/MauiApp.xaml.cs @@ -0,0 +1,20 @@ +namespace OpenShock.ShockOsc.Ui; + +public partial class MauiApp +{ + public MauiApp() + { + InitializeComponent(); + MainPage = new MainPage(); + } + + protected override Window CreateWindow(IActivationState? activationState) + { + var window = base.CreateWindow(activationState); + window.Title = "ShockOSC"; + window.MinimumHeight = 600; + window.MinimumWidth = 1000; + + return window; + } +} \ No newline at end of file From 56acf301fc0a95e6987c26fca727206e6fe3d562 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 19 Apr 2024 01:58:02 +0200 Subject: [PATCH 34/95] Debounced slider --- .../Ui/Components/Parts/DebouncedSlider.razor | 3 + .../Components/Parts/DebouncedSlider.razor.cs | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 ShockOsc/Ui/Components/Parts/DebouncedSlider.razor create mode 100644 ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs diff --git a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor b/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor new file mode 100644 index 0000000..592107d --- /dev/null +++ b/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor @@ -0,0 +1,3 @@ +@typeparam T + +@ChildContent \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs b/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs new file mode 100644 index 0000000..8cc58c7 --- /dev/null +++ b/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs @@ -0,0 +1,60 @@ +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Size = MudBlazor.Size; + +namespace OpenShock.ShockOsc.Ui.Components.Parts; + +public partial class DebouncedSlider : ComponentBase +{ + + private BehaviorSubject _subject = null!; + + private T ValueProp + { + get => _subject.Value; + set + { + _subject.OnNext(value); + SliderValue = value; + } + } + + protected override void OnInitialized() + { + _subject = new BehaviorSubject(SliderValue); + _subject.Throttle(DebounceTime).Subscribe(value => OnSaveAction?.Invoke(value)); + } + + [Parameter] public string Label { get; set; } = string.Empty; + [Parameter] public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(500); + + [Parameter] public EventCallback SliderValueChanged { get; set; } + + private T _sliderValue = default!; + + [Parameter] + public T SliderValue + { + get => _sliderValue; + set + { + if(_sliderValue.Equals(value)) return; + + SliderValueChanged.InvokeAsync(value); + _sliderValue = value!; + } + } + + [Parameter] public Action? OnSaveAction { get; set; } + + [Parameter] + public Size Size { get; set; } = Size.Small; + + [Parameter] + public string? Style { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } +} \ No newline at end of file From 552f6475a77f3686f0fbddb1763c479a413cb5aa Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 19 Apr 2024 03:27:54 +0200 Subject: [PATCH 35/95] Group options --- Installer/installer.nsi | 2 +- ShockOsc/Config/Group.cs | 22 +++ ShockOsc/Config/ShockOscConfig.cs | 6 +- ShockOsc/Models/ProgramGroup.cs | 6 +- .../Platforms/Windows/Package.appxmanifest | 2 +- .../Platforms/Windows/WindowsTrayService.cs | 6 +- ShockOsc/Services/ShockOsc.cs | 74 +++++--- ShockOsc/ShockOsc.csproj | 11 +- ShockOsc/Ui/Components/MainLayout.razor | 7 + .../Ui/Components/Parts/DebouncedSlider.razor | 2 +- .../Components/Parts/DebouncedSlider.razor.cs | 25 ++- ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 168 +++++++++++++++++- ShockOsc/wwwroot/app.css | 7 +- 13 files changed, 287 insertions(+), 51 deletions(-) create mode 100644 ShockOsc/Config/Group.cs diff --git a/Installer/installer.nsi b/Installer/installer.nsi index 68ce386..b3460a0 100644 --- a/Installer/installer.nsi +++ b/Installer/installer.nsi @@ -152,7 +152,7 @@ Section "Install" SecInstall SetOutPath "$INSTDIR" - File /r /x *.log /x *.pdb "..\ShockOsc\bin\Release\net8.0-windows10.0.19041.0\win10-x64\*.*" + File /r /x *.log /x *.pdb /x *.mui "..\ShockOsc\bin\Release\net8.0-windows10.0.19041.0\win10-x64\*.*" WriteRegStr HKLM "Software\ShockOSC" "InstallDir" $INSTDIR WriteUninstaller "$INSTDIR\Uninstall.exe" diff --git a/ShockOsc/Config/Group.cs b/ShockOsc/Config/Group.cs new file mode 100644 index 0000000..f442b18 --- /dev/null +++ b/ShockOsc/Config/Group.cs @@ -0,0 +1,22 @@ +namespace OpenShock.ShockOsc.Config; + +public sealed class Group +{ + public required string Name { get; set; } + public IList Shockers { get; set; } = new List(); + + public bool OverrideIntensity { get; set; } + + public bool RandomIntensity { get; set; } + public JsonRange IntensityRange { get; set; } = new JsonRange { Min = 1, Max = 30 }; + public byte FixedIntensity { get; set; } = 50; + + public bool OverrideDuration { get; set; } + public bool RandomDuration { get; set; } + public uint RandomDurationStep { get; set; } = 1000; + public JsonRange DurationRange { get; set; } = new JsonRange { Min = 1000, Max = 5000 }; + public uint FixedDuration { get; set; } = 2000; + + public bool OverrideCooldownTime { get; set; } + public uint CooldownTime { get; set; } = 5000; +} \ No newline at end of file diff --git a/ShockOsc/Config/ShockOscConfig.cs b/ShockOsc/Config/ShockOscConfig.cs index 91b48ee..bb334dc 100644 --- a/ShockOsc/Config/ShockOscConfig.cs +++ b/ShockOsc/Config/ShockOscConfig.cs @@ -9,9 +9,5 @@ public sealed class ShockOscConfig public IDictionary Groups { get; set; } = new Dictionary(); public Version? LastIgnoredVersion { get; set; } = null; - public sealed class Group - { - public required string Name { get; set; } - public IList Shockers { get; set; } = new List(); - } + } \ No newline at end of file diff --git a/ShockOsc/Models/ProgramGroup.cs b/ShockOsc/Models/ProgramGroup.cs index c913b2b..ac9ae82 100644 --- a/ShockOsc/Models/ProgramGroup.cs +++ b/ShockOsc/Models/ProgramGroup.cs @@ -1,3 +1,4 @@ +using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.OscChangeTracker; using OpenShock.ShockOsc.Services; @@ -23,10 +24,13 @@ public sealed class ProgramGroup public string Name { get; } public TriggerMethod TriggerMethod { get; set; } - public ProgramGroup(Guid id, string name, OscClient oscClient) + public Group? ConfigGroup { get; } + + public ProgramGroup(Guid id, string name, OscClient oscClient, Group? group) { Id = id; Name = name; + ConfigGroup = group; ParamActive = new ChangeTrackedOscParam(Name, "_Active", false, oscClient); ParamCooldown = new ChangeTrackedOscParam(Name, "_Cooldown", false, oscClient); diff --git a/ShockOsc/Platforms/Windows/Package.appxmanifest b/ShockOsc/Platforms/Windows/Package.appxmanifest index f94b3de..9d79f55 100644 --- a/ShockOsc/Platforms/Windows/Package.appxmanifest +++ b/ShockOsc/Platforms/Windows/Package.appxmanifest @@ -20,7 +20,7 @@ - + diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index 6162972..f497249 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -5,6 +5,7 @@ using OpenShock.SDK.CSharp.Hub; using OpenShock.ShockOsc.Services; using Application = Microsoft.Maui.Controls.Application; +using Color = System.Drawing.Color; using Image = System.Drawing.Image; namespace OpenShock.ShockOsc; @@ -20,13 +21,14 @@ public WindowsTrayService(OpenShockHubClient apiHubClient) _apiHubClient.Reconnecting += _ => HubStateChanged(); _apiHubClient.Reconnected += _ => HubStateChanged(); _apiHubClient.Closed += _ => HubStateChanged(); + _apiHubClient.Connected += _ => HubStateChanged(); } private ToolStripLabel _stateLabel; private Task HubStateChanged() { - _stateLabel.Text = "State: " + _apiHubClient.State; + _stateLabel.Text = $"State: {_apiHubClient.State}"; return Task.CompletedTask; } @@ -40,7 +42,7 @@ public void Initialize() menu.Items.Add("ShockOSC", Image.FromFile(@"Resources\openshock-icon.ico"), OnMainClick); menu.Items.Add(new ToolStripSeparator()); - _stateLabel = new ToolStripLabel("State: " + _apiHubClient.State); + _stateLabel = new ToolStripLabel($"State: {_apiHubClient.State}"); menu.Items.Add(_stateLabel); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add("Restart", null, Restart); diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index 91ac432..a398905 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -91,8 +91,8 @@ public ShockOsc(ILogger logger, private async Task SetupGroups() { _dataLayer.ProgramGroups.Clear(); - _dataLayer.ProgramGroups[Guid.Empty] = new ProgramGroup(Guid.Empty, "_All", _oscClient); - foreach (var (id, group) in _configManager.Config.Groups) _dataLayer.ProgramGroups[id] = new ProgramGroup(id, group.Name, _oscClient); + _dataLayer.ProgramGroups[Guid.Empty] = new ProgramGroup(Guid.Empty, "_All", _oscClient, null); + foreach (var (id, group) in _configManager.Config.Groups) _dataLayer.ProgramGroups[id] = new ProgramGroup(id, group.Name, _oscClient, group); } public Task RaiseOnGroupsChanged() => OnGroupsChanged.Raise(); @@ -309,7 +309,7 @@ private async Task ReceiveLogic() return; } - OsTask.Run(() => InstantShock(programGroup, GetDuration(), GetIntensity())); + OsTask.Run(() => InstantShock(programGroup, GetDuration(programGroup), GetIntensity(programGroup))); return; case "Stretch": @@ -430,14 +430,26 @@ private async Task CheckLoop() } } - private byte GetIntensity() + private byte GetIntensity(ProgramGroup programGroup) { - var config = _configManager.Config.Behaviour; + if (programGroup.ConfigGroup is not { OverrideDuration: true }) + { + // Use global config + var config = _configManager.Config.Behaviour; + + if (!config.RandomIntensity) return config.FixedIntensity; + var rir = config.IntensityRange; + var intensityValue = Random.Next((int)rir.Min, (int)rir.Max); + return (byte)intensityValue; + } + + // Use groupConfig + var groupConfig = programGroup.ConfigGroup; - if (!config.RandomIntensity) return config.FixedIntensity; - var rir = config.IntensityRange; - var intensityValue = Random.Next((int)rir.Min, (int)rir.Max); - return (byte)intensityValue; + if (!groupConfig.RandomIntensity) return groupConfig.FixedIntensity; + var groupRir = groupConfig.IntensityRange; + var groupIntensityValue = Random.Next((int)groupRir.Min, (int)groupRir.Max); + return (byte)groupIntensityValue; } private async Task CheckLogic() @@ -445,8 +457,12 @@ private async Task CheckLogic() var config = _configManager.Config.Behaviour; foreach (var (pos, programGroup) in _dataLayer.ProgramGroups) { + var cooldownTime = _configManager.Config.Behaviour.CooldownTime; + if(programGroup.ConfigGroup is { OverrideCooldownTime: true }) + cooldownTime = programGroup.ConfigGroup.CooldownTime; + var isActiveOrOnCooldown = - programGroup.LastExecuted.AddMilliseconds(_configManager.Config.Behaviour.CooldownTime) + programGroup.LastExecuted.AddMilliseconds(cooldownTime) .AddMilliseconds(programGroup.LastDuration) > DateTime.UtcNow; if (programGroup.TriggerMethod == TriggerMethod.None && @@ -498,26 +514,40 @@ private async Task CheckLogic() if (programGroup.TriggerMethod == TriggerMethod.PhysBoneRelease) { - intensity = (byte)MathUtils.LerpFloat(config.IntensityRange.Min, config.IntensityRange.Max, - programGroup.LastStretchValue); + intensity = programGroup.ConfigGroup is { OverrideIntensity: true } + ? (byte)MathUtils.LerpFloat(programGroup.ConfigGroup.IntensityRange.Min, + programGroup.ConfigGroup.IntensityRange.Max, + programGroup.LastStretchValue) + : (byte)MathUtils.LerpFloat(config.IntensityRange.Min, config.IntensityRange.Max, + programGroup.LastStretchValue); programGroup.LastStretchValue = 0; } - else intensity = GetIntensity(); + else intensity = GetIntensity(programGroup); - InstantShock(programGroup, GetDuration(), intensity); + InstantShock(programGroup, GetDuration(programGroup), intensity); } } - - - private uint GetDuration() + private uint GetDuration(ProgramGroup programGroup) { - var config = _configManager.Config.Behaviour; + if (programGroup.ConfigGroup is not { OverrideDuration: true }) + { + // Use global config + var config = _configManager.Config.Behaviour; + + if (!config.RandomDuration) return config.FixedDuration; + var rdr = config.DurationRange; + return (uint)(Random.Next((int)(rdr.Min / config.RandomDurationStep), + (int)(rdr.Max / config.RandomDurationStep)) * config.RandomDurationStep); + } + + // Use group config + var groupConfig = programGroup.ConfigGroup; - if (!config.RandomDuration) return config.FixedDuration; - var rdr = config.DurationRange; - return (uint)(Random.Next((int)(rdr.Min / config.RandomDurationStep), - (int)(rdr.Max / config.RandomDurationStep)) * config.RandomDurationStep); + if (!groupConfig.RandomDuration) return groupConfig.FixedDuration; + var groupRdr = groupConfig.DurationRange; + return (uint)(Random.Next((int)(groupRdr.Min / groupConfig.RandomDurationStep), + (int)(groupRdr.Max / groupConfig.RandomDurationStep)) * groupConfig.RandomDurationStep); } } \ No newline at end of file diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 175c75f..380c583 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -31,6 +31,9 @@ None Resources\Icon512.png + + en + en-US;en @@ -54,9 +57,9 @@ - - - + + + @@ -66,7 +69,7 @@ - + PreserveNewest diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 4c25c9e..224904c 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -150,10 +150,17 @@ ApiHubClient.Reconnecting += ApiHubClientOnReconnecting; ApiHubClient.Reconnected += ApiHubClientOnReconnected; ApiHubClient.Closed += ApiHubClientOnClosed; + ApiHubClient.Connected += AbiHubClientOnConnected; LiveControlManager.OnStateUpdated += LiveControlManagerOnOnStateUpdated; } + private Task AbiHubClientOnConnected(string? arg)=> InvokeAsync(() => + { + StateHasChanged(); + Snackbar.Add("Connected to hub", Severity.Warning); + }); + private Task LiveControlManagerOnOnStateUpdated() { return InvokeAsync(StateHasChanged); diff --git a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor b/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor index 592107d..9da90b0 100644 --- a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor +++ b/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor @@ -1,3 +1,3 @@ @typeparam T -@ChildContent \ No newline at end of file +@ChildContent \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs b/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs index 8cc58c7..38b6205 100644 --- a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs +++ b/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs @@ -1,7 +1,6 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using Microsoft.AspNetCore.Components; -using MudBlazor; using Size = MudBlazor.Size; namespace OpenShock.ShockOsc.Ui.Components.Parts; @@ -9,15 +8,15 @@ namespace OpenShock.ShockOsc.Ui.Components.Parts; public partial class DebouncedSlider : ComponentBase { - private BehaviorSubject _subject = null!; + private BehaviorSubject? _subject; private T ValueProp { - get => _subject.Value; + get => _subject!.Value; set { - _subject.OnNext(value); SliderValue = value; + OnValueChanged?.Invoke(value); } } @@ -31,6 +30,9 @@ protected override void OnInitialized() [Parameter] public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(500); [Parameter] public EventCallback SliderValueChanged { get; set; } + + [Parameter] + public Action? OnValueChanged { get; set; } private T _sliderValue = default!; @@ -40,7 +42,8 @@ public T SliderValue get => _sliderValue; set { - if(_sliderValue.Equals(value)) return; + _subject?.OnNext(value); + if(_sliderValue != null && _sliderValue.Equals(value)) return; SliderValueChanged.InvokeAsync(value); _sliderValue = value!; @@ -55,6 +58,18 @@ public T SliderValue [Parameter] public string? Style { get; set; } + [Parameter] + public string? Class { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + + [Parameter] + public T? Min { get; set; } + + [Parameter] + public T? Max { get; set; } + + [Parameter] + public T? Step { get; set; } } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index d6d5625..7c3c30b 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -1,7 +1,10 @@ -@using System.Text.RegularExpressions +@using System.Globalization +@using System.Text.RegularExpressions @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config +@using OpenShock.ShockOsc.Ui.Components.Parts @using OpenShock.ShockOsc.Services +@using Group = OpenShock.ShockOsc.Config.Group @inject OpenShockApi OpenShockApi @inject ShockOsc ShockOsc @inject ConfigManager ConfigManager @@ -14,7 +17,7 @@ public async Task AddGroup() { var groupId = Guid.NewGuid(); - ConfigManager.Config.Groups.Add(groupId, new ShockOscConfig.Group { Name = "New Group" }); + ConfigManager.Config.Groups.Add(groupId, new Group() { Name = "New Group" }); Group = groupId; await InvokeAsync(StateHasChanged); @@ -39,6 +42,8 @@ await ShockOsc.RaiseOnGroupsChanged(); } + private Task OnGroupSettingsValueChange() => ConfigManager.SaveAsync(); + private async Task OnGroupSelect() { if (CurrentGroup != null) _selectedShockers = [..CurrentGroup.Shockers]; @@ -55,7 +60,7 @@ } } - private ShockOscConfig.Group? CurrentGroup => Group == null ? null : ConfigManager.Config.Groups.TryGetValue(Group.Value, out var group) ? group : null; + private Group? CurrentGroup => Group == null ? null : ConfigManager.Config.Groups.TryGetValue(Group.Value, out var group) ? group : null; private static Regex _nameRegex = new Regex(@"^[a-zA-Z0-9\/\\ -]+$", RegexOptions.Compiled); @@ -68,10 +73,54 @@ } private HashSet _selectedShockers = []; + + + private string CurrentRandomIntensityString + { + get => CurrentGroup?.RandomIntensity ?? false ? "Random Intensity" : "Fixed Intensity"; + set + { + if (CurrentGroup == null) return; + CurrentGroup.RandomIntensity = value == "Random Intensity"; + } + } + + private string CurrentRandomDurationString + { + get => CurrentGroup?.RandomDuration ?? false ? "Random Duration" : "Fixed Duration"; + set + { + if (CurrentGroup == null) return; + CurrentGroup.RandomDuration = value == "Random Duration"; + } + } + + private async Task ResetIntensity() + { + if (CurrentGroup == null) return; + CurrentGroup.FixedIntensity = ConfigManager.Config.Behaviour.FixedIntensity; + CurrentGroup.IntensityRange = ConfigManager.Config.Behaviour.IntensityRange; + CurrentGroup.RandomIntensity = ConfigManager.Config.Behaviour.RandomIntensity; + + await InvokeAsync(StateHasChanged); + await ConfigManager.SaveAsync(); + } + + private async Task ResetDuration() + { + if (CurrentGroup == null) return; + CurrentGroup.FixedDuration = ConfigManager.Config.Behaviour.FixedDuration; + CurrentGroup.DurationRange = ConfigManager.Config.Behaviour.DurationRange; + CurrentGroup.RandomDuration = ConfigManager.Config.Behaviour.RandomDuration; + + await InvokeAsync(StateHasChanged); + await ConfigManager.SaveAsync(); + } + } -Add New Group -Delete Group +Add New Group +Delete Group

@foreach (var group in ConfigManager.Config.Groups) @@ -82,7 +131,7 @@ @if (CurrentGroup != null) { - + Group Settings
@@ -90,7 +139,103 @@
- + + + @if (CurrentGroup.OverrideIntensity) + { + +
+ +
+ + + + + + Reset +
+
+ @if (!CurrentGroup.RandomIntensity) + { + + Intensity: @CurrentGroup.FixedIntensity% + + } + else + { + + Min Intensity: @CurrentGroup.IntensityRange.Min% + + + + Max Intensity: @CurrentGroup.IntensityRange.Max% + + } + } +
+ + + + @if (CurrentGroup.OverrideDuration) + { + +
+ +
+ + + + + + Reset +
+
+ @if (!CurrentGroup.RandomDuration) + { + + Duration: @MathF.Round(CurrentGroup.FixedDuration / 1000f, 1).ToString(CultureInfo.InvariantCulture)s + + } + else + { + + Min Duration: @MathF.Round(CurrentGroup.DurationRange.Min / 1000f, 1).ToString(CultureInfo.InvariantCulture)s + + + + Max Duration: @MathF.Round(CurrentGroup.DurationRange.Max / 1000f, 1).ToString(CultureInfo.InvariantCulture)s + + } + } +
+ + + + @if (CurrentGroup.OverrideCooldownTime) + { + +
+ + + } +
+ + Shockers in Group
@@ -110,4 +255,11 @@ else Please select a group to edit -} \ No newline at end of file +} + + \ No newline at end of file diff --git a/ShockOsc/wwwroot/app.css b/ShockOsc/wwwroot/app.css index f6d693a..6ac3583 100644 --- a/ShockOsc/wwwroot/app.css +++ b/ShockOsc/wwwroot/app.css @@ -4,7 +4,7 @@ font-display: swap; font-weight: 400; src: url(fonts/poppins-latin-400-normal.405055dd..woff2) format('woff2'), url(fonts/poppins-latin-400-normal.cda9c93f..woff) format('woff'); - unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } html, body { @@ -42,4 +42,9 @@ body { .mud-paper-padding { padding: 10px 15px; +} + +.openshock-slider-length { + width: 300px !important; + margin-left: 30px; } \ No newline at end of file From 3430ee420cb22d037cf256eb1d07be66cc801dcf Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 04:51:26 +0200 Subject: [PATCH 36/95] Close to tray & build ci --- .github/workflows/ci-windows.yml | 77 +++++++++++++++++++ ShockOsc/Config/AppConfig.cs | 6 ++ ShockOsc/Config/ShockOscConfig.cs | 2 +- ShockOsc/MauiProgram.cs | 42 +++++++++- .../Platforms/Windows/WindowsTrayService.cs | 2 + 5 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci-windows.yml create mode 100644 ShockOsc/Config/AppConfig.cs diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000..354fde5 --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,77 @@ +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + types: [opened, reopened, synchronize] + workflow_call: + workflow_dispatch: + +name: ci-windowsl + +env: + DOTNET_VERSION: 8.0.x + REGISTRY: ghcr.io + +jobs: + + build: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET SDK ${{ env.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies + run: dotnet restore + + - name: Install maui workload + run: dotnet workload install maui + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test --no-restore --verbosity normal + + - name: Publish ShockOSC Windows + run: dotnet publish ShockOsc/ShockOsc.csproj -c Release -f net8.0-windows10.0.19041.0 -o ./publish/API + + - name: Upload ShockOSC Windows artifacts + uses: actions/upload-artifact@v4 + with: + name: ShockOsc + path: publish/ShockOsc/* + retention-days: 1 + if-no-files-found: error + + instller: + runs-on: windows-latest + needs: build + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: | + Installer + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: ShockOsc + path: publish/ + + \ No newline at end of file diff --git a/ShockOsc/Config/AppConfig.cs b/ShockOsc/Config/AppConfig.cs new file mode 100644 index 0000000..267d166 --- /dev/null +++ b/ShockOsc/Config/AppConfig.cs @@ -0,0 +1,6 @@ +namespace OpenShock.ShockOsc.Config; + +public sealed class AppConfig +{ + public bool CloseToTray { get; set; } = true; +} \ No newline at end of file diff --git a/ShockOsc/Config/ShockOscConfig.cs b/ShockOsc/Config/ShockOscConfig.cs index bb334dc..845e530 100644 --- a/ShockOsc/Config/ShockOscConfig.cs +++ b/ShockOsc/Config/ShockOscConfig.cs @@ -9,5 +9,5 @@ public sealed class ShockOscConfig public IDictionary Groups { get; set; } = new Dictionary(); public Version? LastIgnoredVersion { get; set; } = null; - + public AppConfig App { get; set; } = new(); } \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 386e7d7..3da63a1 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -19,6 +19,8 @@ namespace OpenShock.ShockOsc; public static class MauiProgram { + private static ShockOscConfig? _config; + public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() { var builder = Microsoft.Maui.Hosting.MauiApp.CreateBuilder(); @@ -76,9 +78,43 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); - - + #if WINDOWS + builder.ConfigureLifecycleEvents(lifecycleBuilder => + { + lifecycleBuilder.AddWindows(windowsLifecycleBuilder => + { + windowsLifecycleBuilder.OnWindowCreated(window => + { + //use Microsoft.UI.Windowing functions for window + var handle = WinRT.Interop.WindowNative.GetWindowHandle(window); + var id = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(handle); + var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(id); + + //When user execute the closing method, we can push a display alert. If user click Yes, close this application, if click the cancel, display alert will dismiss. + appWindow.Closing += async (s, e) => + { + e.Cancel = true; + + if (_config?.App.CloseToTray ?? false) + { + appWindow.Hide(); + return; + } + + var result = await Application.Current.MainPage.DisplayAlert( + "Close?", + "Do you want to close ShockOSC?", + "Yes", + "Cancel"); + + if (result) Application.Current.Quit(); + }; + }); + }); + }); + + builder.Services.AddSingleton(); #endif @@ -99,6 +135,8 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() var app = builder.Build(); + _config = app.Services.GetRequiredService().Config; + app.Services.GetService()?.Initialize(); // <---- Warmup ----> diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index f497249..eb0cb7b 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -72,6 +72,8 @@ private static void OnMainClick(object? sender, EventArgs eventArgs) var windowId = Win32Interop.GetWindowIdFromWindow(windowHandle); var appWindow = AppWindow.GetFromWindowId(windowId); + + if (appWindow.IsVisible) appWindow.Hide(); else appWindow.Show(); } From 3327bc48081288ba32a11aa932d2fbe527703a1d Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 04:57:59 +0200 Subject: [PATCH 37/95] fix publish path --- .github/workflows/ci-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 354fde5..d7c1747 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -44,7 +44,7 @@ jobs: run: dotnet test --no-restore --verbosity normal - name: Publish ShockOSC Windows - run: dotnet publish ShockOsc/ShockOsc.csproj -c Release -f net8.0-windows10.0.19041.0 -o ./publish/API + run: dotnet publish ShockOsc/ShockOsc.csproj -c Release -f net8.0-windows10.0.19041.0 -o ./publish/ShockOsc - name: Upload ShockOSC Windows artifacts uses: actions/upload-artifact@v4 From 701d42437a339ca531899221792198588fd406bf Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 05:14:44 +0200 Subject: [PATCH 38/95] Dont need workload? --- .github/workflows/ci-windows.yml | 3 --- global.json | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index d7c1747..0283297 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -33,9 +33,6 @@ jobs: - name: Install dependencies run: dotnet restore - - - name: Install maui workload - run: dotnet workload install maui - name: Build run: dotnet build --configuration Release --no-restore diff --git a/global.json b/global.json index dad2db5..b5b37b6 100644 --- a/global.json +++ b/global.json @@ -2,6 +2,6 @@ "sdk": { "version": "8.0.0", "rollForward": "latestMajor", - "allowPrerelease": true + "allowPrerelease": false } } \ No newline at end of file From bb8da9275bd9c8a9b7e5f027ea7bdb1edcd3a754 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 05:24:20 +0200 Subject: [PATCH 39/95] cache nuget --- .github/workflows/ci-windows.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 0283297..3812caa 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -31,6 +31,14 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Get Cache NuGet packages + uses: actions/cache@v3 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Install dependencies run: dotnet restore From 272a2e8b18a5e83d03ed4d3e80c3b06b5b77beb7 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 05:53:31 +0200 Subject: [PATCH 40/95] Try ceeate installer --- .github/workflows/ci-windows.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 3812caa..61e215e 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -31,7 +31,7 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Get Cache NuGet packages + - name: Cache NuGet packages uses: actions/cache@v3 with: path: ~/.nuget/packages @@ -79,4 +79,8 @@ jobs: name: ShockOsc path: publish/ - \ No newline at end of file + - name: Create nsis installer + uses: joncloud/makensis-action@publish + with: + arguments: ${{ github.workspace }}/Installer/installer.nsi + additional-plugin-paths: ${{ github.workspace }}/Installer/Plugins \ No newline at end of file From 595b8b3b57712c18eb4345c6ad984521e670d4df Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 05:57:46 +0200 Subject: [PATCH 41/95] remove docker build step --- .github/workflows/ci-windows.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 61e215e..0ea9752 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -70,9 +70,6 @@ jobs: sparse-checkout: | Installer - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Download artifacts uses: actions/download-artifact@v4 with: From a88483c0aab471e416a25284fe22419634963097 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 06:13:36 +0200 Subject: [PATCH 42/95] correct installer path --- Installer/installer.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Installer/installer.nsi b/Installer/installer.nsi index b3460a0..3d8cdbe 100644 --- a/Installer/installer.nsi +++ b/Installer/installer.nsi @@ -152,7 +152,7 @@ Section "Install" SecInstall SetOutPath "$INSTDIR" - File /r /x *.log /x *.pdb /x *.mui "..\ShockOsc\bin\Release\net8.0-windows10.0.19041.0\win10-x64\*.*" + File /r /x *.log /x *.pdb /x *.mui "publish\*.*" WriteRegStr HKLM "Software\ShockOSC" "InstallDir" $INSTDIR WriteUninstaller "$INSTDIR\Uninstall.exe" From 15925e8424cb1a0fa69f5470775c0d65a208942e Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 06:51:37 +0200 Subject: [PATCH 43/95] path? --- Installer/installer.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Installer/installer.nsi b/Installer/installer.nsi index 3d8cdbe..1ac509e 100644 --- a/Installer/installer.nsi +++ b/Installer/installer.nsi @@ -152,7 +152,7 @@ Section "Install" SecInstall SetOutPath "$INSTDIR" - File /r /x *.log /x *.pdb /x *.mui "publish\*.*" + File /r /x *.log /x *.pdb /x *.mui "..\publish\*.*" WriteRegStr HKLM "Software\ShockOSC" "InstallDir" $INSTDIR WriteUninstaller "$INSTDIR\Uninstall.exe" From 2ce607036acac41d1732f5e28da43fce7df0cafe Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 08:14:57 +0200 Subject: [PATCH 44/95] debug --- .github/workflows/ci-windows.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 0ea9752..577463d 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -75,6 +75,9 @@ jobs: with: name: ShockOsc path: publish/ + + - name: Debug LS + run: ls -la - name: Create nsis installer uses: joncloud/makensis-action@publish From 91e9ee539d21e2bd8958d9db34c5b4b848a111b9 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 08:19:25 +0200 Subject: [PATCH 45/95] debug --- .github/workflows/ci-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 577463d..1330046 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -77,7 +77,7 @@ jobs: path: publish/ - name: Debug LS - run: ls -la + run: ls - name: Create nsis installer uses: joncloud/makensis-action@publish From 9c6b92a658fbcb365f5ebe060d5faa8a9c7d8022 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 09:29:44 +0200 Subject: [PATCH 46/95] installer script for github actions --- Installer/installer.nsi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/installer.nsi b/Installer/installer.nsi index 1ac509e..8fd7f21 100644 --- a/Installer/installer.nsi +++ b/Installer/installer.nsi @@ -49,13 +49,13 @@ ;-------------------------------- ;Icons - !define MUI_ICON "..\ShockOsc\Resources\openshock-icon.ico" - !define MUI_UNICON "..\ShockOsc\Resources\openshock-icon.ico" + !define MUI_ICON "..\publish\Resources\openshock-icon.ico" + !define MUI_UNICON "..\publish\Resources\openshock-icon.ico" ;-------------------------------- ;Pages - !insertmacro MUI_PAGE_LICENSE "..\LICENSE" + !insertmacro MUI_PAGE_LICENSE "LICENSE" !define MUI_PAGE_CUSTOMFUNCTION_PRE dirPre !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES From 55453f95005015036144c4ec2a888dbd405922e7 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 09:37:05 +0200 Subject: [PATCH 47/95] correct license path --- Installer/installer.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Installer/installer.nsi b/Installer/installer.nsi index 8fd7f21..559b2e4 100644 --- a/Installer/installer.nsi +++ b/Installer/installer.nsi @@ -55,7 +55,7 @@ ;-------------------------------- ;Pages - !insertmacro MUI_PAGE_LICENSE "LICENSE" + !insertmacro MUI_PAGE_LICENSE "..\LICENSE" !define MUI_PAGE_CUSTOMFUNCTION_PRE dirPre !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES From 0a852576e26e47a5df9ccc1b52ecf6e50afebab3 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 09:43:45 +0200 Subject: [PATCH 48/95] fix script file --- .github/workflows/ci-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 1330046..56d6cae 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -82,5 +82,5 @@ jobs: - name: Create nsis installer uses: joncloud/makensis-action@publish with: - arguments: ${{ github.workspace }}/Installer/installer.nsi + script-file: ${{ github.workspace }}/Installer/installer.nsi additional-plugin-paths: ${{ github.workspace }}/Installer/Plugins \ No newline at end of file From 93791c9858e8f4117e4f6d2a865218868121d64d Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 09:49:04 +0200 Subject: [PATCH 49/95] Publish setup exe --- .github/workflows/ci-windows.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 56d6cae..2d4fa36 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -75,12 +75,18 @@ jobs: with: name: ShockOsc path: publish/ - - - name: Debug LS - run: ls + - name: Create nsis installer uses: joncloud/makensis-action@publish with: script-file: ${{ github.workspace }}/Installer/installer.nsi - additional-plugin-paths: ${{ github.workspace }}/Installer/Plugins \ No newline at end of file + additional-plugin-paths: ${{ github.workspace }}/Installer/Plugins + + - name: Upload ShockOSC Windows Setup + uses: actions/upload-artifact@v4 + with: + name: ShockOsc_Setup + path: Installer/ShockOsc_Setup.exe + retention-days: 7 + if-no-files-found: error \ No newline at end of file From e04e14e31c745d7b54dd8a8ed830eb52477ebc05 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 20 Apr 2024 13:11:45 +0200 Subject: [PATCH 50/95] Failing at hiding titlebar --- ShockOsc/MauiProgram.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 3da63a1..d57c5c1 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,8 +1,5 @@ using System.Net; -using System.Windows.Controls; using Microsoft.Maui.LifecycleEvents; -using Microsoft.UI; -using Microsoft.UI.Windowing; using MudBlazor.Services; using OpenShock.SDK.CSharp.Hub; using OpenShock.ShockOsc.Backend; @@ -10,10 +7,8 @@ using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Services; -using OpenShock.ShockOsc.Ui; using Serilog; using MauiApp = OpenShock.ShockOsc.Ui.MauiApp; -using MenuItem = System.Windows.Controls.MenuItem; namespace OpenShock.ShockOsc; @@ -90,7 +85,15 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() var handle = WinRT.Interop.WindowNative.GetWindowHandle(window); var id = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(handle); var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(id); - + + // if(appWindow.Presenter is OverlappedPresenter presenter) + // { + // presenter.IsMaximizable = false; + // presenter.IsMinimizable = false; + // presenter.IsResizable = true; + // presenter.SetBorderAndTitleBar(false, false); + // } + //When user execute the closing method, we can push a display alert. If user click Yes, close this application, if click the cancel, display alert will dismiss. appWindow.Closing += async (s, e) => { From 933159ca9f0458fc12086736a04df756d36e52e6 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 22 Apr 2024 22:08:17 +0200 Subject: [PATCH 51/95] Sidebar refactor --- ShockOsc/MauiProgram.cs | 3 +- ShockOsc/Services/StatusHandler.cs | 67 ++++++ ShockOsc/Ui/Components/MainLayout.razor | 186 ++--------------- ShockOsc/Ui/Components/SideBar.razor | 134 ++++++++++++ .../Ui/Components/Tabs/AppSettingsTab.razor | 34 +++ ShockOsc/Ui/Components/Tabs/ChatboxTab.razor | 53 +++++ ShockOsc/Ui/Components/Tabs/ConfigTab.razor | 197 +++++------------- .../Ui/Components/Tabs/DashboardTab.razor | 5 + ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 34 +-- ShockOsc/Ui/Components/Tabs/TabType.cs | 13 ++ 10 files changed, 399 insertions(+), 327 deletions(-) create mode 100644 ShockOsc/Services/StatusHandler.cs create mode 100644 ShockOsc/Ui/Components/SideBar.razor create mode 100644 ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor create mode 100644 ShockOsc/Ui/Components/Tabs/ChatboxTab.razor create mode 100644 ShockOsc/Ui/Components/Tabs/DashboardTab.razor create mode 100644 ShockOsc/Ui/Components/Tabs/TabType.cs diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index d57c5c1..d1f14fe 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -121,7 +121,8 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() builder.Services.AddSingleton(); #endif - + builder.Services.AddSingleton(); + builder.Services.AddMudServices(); builder.Services.AddMauiBlazorWebView(); diff --git a/ShockOsc/Services/StatusHandler.cs b/ShockOsc/Services/StatusHandler.cs new file mode 100644 index 0000000..d735ce6 --- /dev/null +++ b/ShockOsc/Services/StatusHandler.cs @@ -0,0 +1,67 @@ +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Hub; +using OpenShock.SDK.CSharp.Utils; + +namespace OpenShock.ShockOsc.Services; + +public sealed class StatusHandler : IDisposable +{ + private readonly OpenShockHubClient _hubClient; + private readonly LiveControlManager _liveControlManager; + private readonly ILogger _logger; + + public StatusHandler(OpenShockHubClient hubClient, LiveControlManager liveControlManager, ILogger logger) + { + _hubClient = hubClient; + _liveControlManager = liveControlManager; + _logger = logger; + + _hubClient.Reconnecting += ApiHubClientOnReconnecting; + _hubClient.Reconnected += ApiHubClientOnReconnected; + _hubClient.Closed += ApiHubClientOnClosed; + _hubClient.Connected += AbiHubClientOnConnected; + + _liveControlManager.OnStateUpdated += LiveControlManagerOnOnStateUpdated; + } + + public event Func? OnWebsocketStatusChanged; + + private async Task AbiHubClientOnConnected(string? arg) + { + if(OnWebsocketStatusChanged != null) await OnWebsocketStatusChanged.Raise(); + _logger.LogDebug("Connected to hub"); + } + + private async Task LiveControlManagerOnOnStateUpdated() + { + if(OnWebsocketStatusChanged != null) await OnWebsocketStatusChanged.Raise(); + } + + private async Task ApiHubClientOnReconnected(string? arg) + { + if(OnWebsocketStatusChanged != null) await OnWebsocketStatusChanged.Raise(); + _logger.LogDebug("Reconnected to hub"); + } + + + private async Task ApiHubClientOnReconnecting(Exception? arg) + { + if(OnWebsocketStatusChanged != null) await OnWebsocketStatusChanged.Raise(); + _logger.LogDebug("Reconnecting to hub..."); + } + + private async Task ApiHubClientOnClosed(Exception? arg) + { + if(OnWebsocketStatusChanged != null) await OnWebsocketStatusChanged.Raise(); + _logger.LogDebug("Disconnected from hub"); + } + + public void Dispose() + { + _hubClient.Reconnecting -= ApiHubClientOnReconnecting; + _hubClient.Reconnected -= ApiHubClientOnReconnected; + _hubClient.Closed -= ApiHubClientOnClosed; + + _liveControlManager.OnStateUpdated -= LiveControlManagerOnOnStateUpdated; + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 224904c..30337b5 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -1,18 +1,9 @@ -@using Microsoft.AspNetCore.SignalR.Client -@using OpenShock.SDK.CSharp.Hub -@using OpenShock.SDK.CSharp.Live.LiveControlModels @using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Ui.Components.Tabs @using OpenShock.ShockOsc.Ui.Utils -@using OpenShock.ShockOsc.Backend -@using OpenShock.ShockOsc.Services -@using OpenShock.ShockOsc.Ui.Components.Parts @inject ISnackbar Snackbar @inherits LayoutComponentBase -@inject OpenShockHubClient ApiHubClient -@inject OpenShockApi Api -@inject LiveControlManager LiveControlManager -@implements IDisposable + @page "/main" @@ -30,114 +21,46 @@
- - -
- -
-
- - ShockOSC - -
- -
- - - - - Groups - Config - Shockers - Debug - Logs - - -
- -
- - ShockOSC v@(Version) - -
- - - @foreach (var device in Api.Devices) - { - if (LiveControlManager.LiveControlClients.TryGetValue(device.Id, out var client)) - { - - } - else - { - - } - } - - -
- - -
-
- -
-
- - + +
- @switch (_currentTab) + @switch (_currentTabType) { - case Tab.Groups: + case TabType.Dashboard: + + break; + case TabType.Groups: break; - case Tab.Config: + case TabType.Config: break; - case Tab.Shockers: + case TabType.Chatbox: + + break; + case TabType.AppSettings: + + break; + case TabType.Shockers: break; - case Tab.Debug: + case TabType.Debug: break; - case Tab.Logs: + case TabType.Logs: break; }
-
- -
@code { - private static readonly string Version = typeof(MainLayout).Assembly.GetName().Version!.ToString(); - - public enum Tab - { - Groups, - Config, - Shockers, - Debug, - Logs - } - - private Tab _currentTab = Tab.Groups; - - private void NavigateTab(Tab tab) - { - _currentTab = tab; - } - + private TabType _currentTabType = TabType.Dashboard; + private void MsgNoty(string msg, Severity severity) { Snackbar.Add(msg, severity); @@ -146,75 +69,6 @@ protected override void OnInitialized() { UiLogSink.NotificationAction = MsgNoty; - - ApiHubClient.Reconnecting += ApiHubClientOnReconnecting; - ApiHubClient.Reconnected += ApiHubClientOnReconnected; - ApiHubClient.Closed += ApiHubClientOnClosed; - ApiHubClient.Connected += AbiHubClientOnConnected; - - LiveControlManager.OnStateUpdated += LiveControlManagerOnOnStateUpdated; - } - - private Task AbiHubClientOnConnected(string? arg)=> InvokeAsync(() => - { - StateHasChanged(); - Snackbar.Add("Connected to hub", Severity.Warning); - }); - - private Task LiveControlManagerOnOnStateUpdated() - { - return InvokeAsync(StateHasChanged); - } - - private Task ApiHubClientOnReconnected(string? arg) => InvokeAsync(() => - { - StateHasChanged(); - Snackbar.Add("Reconnected to hub", Severity.Success); - }); - - - private Task ApiHubClientOnReconnecting(Exception? arg) => InvokeAsync(() => - { - StateHasChanged(); - Snackbar.Add("Reconnecting to hub...", Severity.Warning); - }); - - private Task ApiHubClientOnClosed(Exception? arg) => InvokeAsync(() => - { - StateHasChanged(); - Snackbar.Add("Disconnected from hub", Severity.Error); - }); - - - private Color GetConnectionStateColor(HubConnectionState state) => - state switch - { - HubConnectionState.Connected => Color.Success, - HubConnectionState.Reconnecting => Color.Warning, - HubConnectionState.Connecting => Color.Warning, - HubConnectionState.Disconnected => Color.Error, - _ => Color.Error - }; - - private Color GetConnectionStateColor(WebsocketConnectionState state) => - state switch - { - WebsocketConnectionState.Connected => Color.Success, - WebsocketConnectionState.Reconnecting => Color.Warning, - WebsocketConnectionState.Connecting => Color.Warning, - WebsocketConnectionState.Disconnected => Color.Error, - _ => Color.Error - }; - - public void Dispose() - { - Snackbar?.Dispose(); - - ApiHubClient.Reconnecting -= ApiHubClientOnReconnecting; - ApiHubClient.Reconnected -= ApiHubClientOnReconnected; - ApiHubClient.Closed -= ApiHubClientOnClosed; - - LiveControlManager.OnStateUpdated -= LiveControlManagerOnOnStateUpdated; } } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/SideBar.razor b/ShockOsc/Ui/Components/SideBar.razor new file mode 100644 index 0000000..558668b --- /dev/null +++ b/ShockOsc/Ui/Components/SideBar.razor @@ -0,0 +1,134 @@ +@using Microsoft.AspNetCore.SignalR.Client +@using OpenShock.SDK.CSharp.Hub +@using OpenShock.SDK.CSharp.Live.LiveControlModels +@using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Services +@using OpenShock.ShockOsc.Ui.Components.Tabs +@using OpenShock.ShockOsc.Ui.Utils +@inject OpenShockApi Api +@inject OpenShockHubClient ApiHubClient +@inject ISnackbar Snackbar +@inject LiveControlManager LiveControlManager +@inject StatusHandler StatusHandler + + + +
+ +
+
+ + ShockOSC + +
+ +
+ + + + + Dashboard + + Groups + Shockers + + Chatbox + + + Debug + Logs + + Config + App Settings + + +
+ +
+ + ShockOSC v@(Version) + +
+ + + + @foreach (var device in Api.Devices) + { + if (LiveControlManager.LiveControlClients.TryGetValue(device.Id, out var client)) + { + + } + else + { + + } + } + + +
+ + +
+
+ +
+
+ +@code { + + private static readonly string Version = typeof(MainLayout).Assembly.GetName().Version!.ToString(); + + [Parameter] public EventCallback CurrentTabChanged { get; set; } + + private TabType _currentTab = TabType.Dashboard; + + [Parameter] + public TabType CurrentTab + { + get => _currentTab; + set + { + if(_currentTab == value) return; + CurrentTabChanged.InvokeAsync(value); + _currentTab = value; + } + } + + protected override void OnInitialized() + { + StatusHandler.OnWebsocketStatusChanged += () => InvokeAsync(StateHasChanged); + } + + private void NavigateTab(TabType tab) + { + CurrentTab = tab; + } + + private static Color GetConnectionStateColor(HubConnectionState state) => + state switch + { + HubConnectionState.Connected => Color.Success, + HubConnectionState.Reconnecting => Color.Warning, + HubConnectionState.Connecting => Color.Warning, + HubConnectionState.Disconnected => Color.Error, + _ => Color.Error + }; + + private static Color GetConnectionStateColor(WebsocketConnectionState state) => + state switch + { + WebsocketConnectionState.Connected => Color.Success, + WebsocketConnectionState.Reconnecting => Color.Warning, + WebsocketConnectionState.Connecting => Color.Warning, + WebsocketConnectionState.Disconnected => Color.Error, + _ => Color.Error + }; + + + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor b/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor new file mode 100644 index 0000000..2ba53f3 --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor @@ -0,0 +1,34 @@ +@using OpenShock.ShockOsc.Config +@inject ConfigManager ConfigManager + + + + + ShockOSC App + + + + + + + + OSC Options + + + + @if (!ConfigManager.Config.Osc.OscQuery) + { +
+ + + } +
+ +@code { + + + private async Task OnSettingsValueChange() + { + await ConfigManager.SaveAsync(); + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/ChatboxTab.razor b/ShockOsc/Ui/Components/Tabs/ChatboxTab.razor new file mode 100644 index 0000000..d9762ca --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/ChatboxTab.razor @@ -0,0 +1,53 @@ +@using OpenShock.SDK.CSharp.Models +@using OpenShock.ShockOsc.Config +@inject ConfigManager ConfigManager + + + Chatbox Options + + + +
+
+ + + + + @foreach (ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(ChatboxConf.HoscyMessageType))) + { + @hoscyMessageType + } + + +
+
+ + + + + +
+ + @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) + { + + + + + + + } + + +
+
+ +@code { + + + private async Task OnSettingsValueChange() + { + await ConfigManager.SaveAsync(); + } + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor index c0c74f5..ebbdb4c 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Components/Tabs/ConfigTab.razor @@ -1,6 +1,7 @@ -@using OpenShock.SDK.CSharp.Models +@using System.Globalization @using System.Reactive.Subjects @using OpenShock.ShockOsc.Utils +@using OpenShock.ShockOsc.Ui.Components.Parts @using System.Reactive.Linq @using OpenShock.ShockOsc.Config @using OpenShock.ShockOsc.Services @@ -11,7 +12,7 @@ @inject UnderscoreConfig UnderscoreConfig - Shocker Options + Global Shocker Options (_All Shocker) @@ -21,41 +22,73 @@ } +


- +
- @if (intensity == "Fixed Intensity") + + + @if (!ConfigManager.Config.Behaviour.RandomIntensity) { - Intensity: @FixedIntensity.ToString()% + + Intensity: @ConfigManager.Config.Behaviour.FixedIntensity% + } else { - Intensity Min: @IntensityMin.ToString()% -
- Intensity Max: @IntensityMax.ToString()% + + Min Intensity: @ConfigManager.Config.Behaviour.IntensityRange.Min% + + + + Max Intensity: @ConfigManager.Config.Behaviour.IntensityRange.Max% + } - + + +
- @if (duration == "Fixed Duration") + + @if (!ConfigManager.Config.Behaviour.RandomDuration) { - + + Duration: @MathF.Round(ConfigManager.Config.Behaviour.FixedDuration / 1000f, 1).ToString(CultureInfo.InvariantCulture)s + } else { - - - + + Min Duration: @MathF.Round(ConfigManager.Config.Behaviour.DurationRange.Min / 1000f, 1).ToString(CultureInfo.InvariantCulture)s + + + + Max Duration: @MathF.Round(ConfigManager.Config.Behaviour.DurationRange.Max / 1000f, 1).ToString(CultureInfo.InvariantCulture)s + } +

@@ -68,121 +101,23 @@
- - Chatbox Options - - - -
-
- - - - - @foreach (ChatboxConf.HoscyMessageType hoscyMessageType in Enum.GetValues(typeof(ChatboxConf.HoscyMessageType))) - { - @hoscyMessageType - } - - -
-
- - @if (_advancedSettingsExpanded) - { - Advanced Settings - } - else - { - Advanced Settings - } - - - - - -
- - @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) - { - - - - - - - } - -
-
-
- - - OSC Options - - - - @if (!ConfigManager.Config.Osc.OscQuery) - { -
- - - } -
- @code { - private bool _advancedSettingsExpanded = false; - - private BehaviorSubject _fixedIntensitySubject = null!; - - private byte FixedIntensity - { - get => _fixedIntensitySubject.Value; - set => _fixedIntensitySubject.OnNext(value); - } - private BehaviorSubject _intensityMinSubject = null!; - - private byte IntensityMin + private string RandomIntensityString { - get => _intensityMinSubject.Value; - set - { - if (value > IntensityMax) IntensityMax = value; - _intensityMinSubject.OnNext(value); - } + get => ConfigManager.Config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; + set => ConfigManager.Config.Behaviour.RandomIntensity = value == "Random Intensity"; } - private BehaviorSubject _intensityMaxSubject = null!; - - private byte IntensityMax - { - get => _intensityMaxSubject.Value; - set - { - if (value < IntensityMin) IntensityMin = value; - _intensityMaxSubject.OnNext(value); - } - } - - - private string intensity = "Fixed Intensity"; - private string duration = "Fixed Duration"; - - private DateTime _lastSettingsSave = DateTime.Now; - private CancellationTokenSource _cts = new CancellationTokenSource(); - - private void OnAdvancedSettingsClick() + private string RandomDurationString { - _advancedSettingsExpanded = !_advancedSettingsExpanded; + get => ConfigManager.Config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; + set => ConfigManager.Config.Behaviour.RandomDuration = value == "Random Duration"; } + private async Task OnSettingsValueChange() { - _lastSettingsSave = DateTime.Now; - ConfigManager.Config.Behaviour.RandomIntensity = intensity == "Random Intensity"; - ConfigManager.Config.Behaviour.RandomDuration = duration == "Random Duration"; - await ConfigManager.SaveAsync(); await underscoreConfig.SendUpdateForAll(); } @@ -190,30 +125,6 @@ protected override void OnInitialized() { UnderscoreConfig.OnConfigUpdate += OnConfigUpdate; - - intensity = ConfigManager.Config.Behaviour.RandomIntensity ? "Random Intensity" : "Fixed Intensity"; - duration = ConfigManager.Config.Behaviour.RandomDuration ? "Random Duration" : "Fixed Duration"; - - _fixedIntensitySubject = new BehaviorSubject(ConfigManager.Config.Behaviour.FixedIntensity); - _fixedIntensitySubject.Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(b => - { - ConfigManager.Config.Behaviour.FixedIntensity = b; - OsTask.Run(OnSettingsValueChange); - }); - - _intensityMinSubject = new BehaviorSubject((byte)ConfigManager.Config.Behaviour.IntensityRange.Min); - _intensityMinSubject.Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(b => - { - ConfigManager.Config.Behaviour.IntensityRange.Min = b; - OsTask.Run(OnSettingsValueChange); - }); - - _intensityMaxSubject = new BehaviorSubject((byte)ConfigManager.Config.Behaviour.IntensityRange.Max); - _intensityMaxSubject.Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(b => - { - ConfigManager.Config.Behaviour.IntensityRange.Max = b; - OsTask.Run(OnSettingsValueChange); - }); } private void OnConfigUpdate() diff --git a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor new file mode 100644 index 0000000..0f41020 --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor @@ -0,0 +1,5 @@ +

DashboardTab

+ +@code { + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index 7c3c30b..088ccd6 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -141,9 +141,8 @@ - @if (CurrentGroup.OverrideIntensity) - { - + +
@@ -158,33 +157,33 @@
@if (!CurrentGroup.RandomIntensity) { - Intensity: @CurrentGroup.FixedIntensity% } else { - Min Intensity: @CurrentGroup.IntensityRange.Min% - Max Intensity: @CurrentGroup.IntensityRange.Max% } - } + + - @if (CurrentGroup.OverrideDuration) - { - + +
@@ -221,18 +220,19 @@ Max Duration: @MathF.Round(CurrentGroup.DurationRange.Max / 1000f, 1).ToString(CultureInfo.InvariantCulture)s } - } + + - + - @if (CurrentGroup.OverrideCooldownTime) - { - + +
- + - } + +
diff --git a/ShockOsc/Ui/Components/Tabs/TabType.cs b/ShockOsc/Ui/Components/Tabs/TabType.cs new file mode 100644 index 0000000..306c367 --- /dev/null +++ b/ShockOsc/Ui/Components/Tabs/TabType.cs @@ -0,0 +1,13 @@ +namespace OpenShock.ShockOsc.Ui.Components.Tabs; + +public enum TabType +{ + Dashboard, + Groups, + Config, + Chatbox, + AppSettings, + Shockers, + Debug, + Logs +} \ No newline at end of file From 8ac057780528cf5d85785b61b4ccc6fec287454a Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 23 Apr 2024 17:01:12 +0200 Subject: [PATCH 52/95] Login custom server --- .../Platforms/Windows/WindowsTrayService.cs | 11 +- .../Ui/Components/Tabs/DashboardTab.razor | 5 +- .../Pages/Authentication/Authenticate.razor | 21 +-- .../Ui/Pages/Authentication/LoginPart.razor | 149 ++++++++++++++++++ ShockOsc/Ui/Utils/ThemeDefinition.cs | 4 +- 5 files changed, 168 insertions(+), 22 deletions(-) create mode 100644 ShockOsc/Ui/Pages/Authentication/LoginPart.razor diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index eb0cb7b..27c8a93 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -71,11 +71,14 @@ private static void OnMainClick(object? sender, EventArgs eventArgs) var windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow); var windowId = Win32Interop.GetWindowIdFromWindow(windowHandle); var appWindow = AppWindow.GetFromWindowId(windowId); - - - if (appWindow.IsVisible) appWindow.Hide(); - else appWindow.Show(); + appWindow.Show(); + + if (appWindow.Presenter is OverlappedPresenter presenter) + { + presenter.IsAlwaysOnTop = true; + presenter.IsAlwaysOnTop = false; + } } private static void OnQuitClick(object? sender, EventArgs eventArgs) diff --git a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor index 0f41020..c2acc0e 100644 --- a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor @@ -1,4 +1,7 @@ -

DashboardTab

+@using OpenShock.ShockOsc.Services +@inject StatusHandler StatusHandler + +

DashboardTab

@code { diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index 7174c4b..c8015e6 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -1,5 +1,4 @@ @using OpenShock.ShockOsc.Ui.Utils -@using OpenShock.SDK.CSharp.Live @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config @using Microsoft.Extensions.Logging @@ -39,10 +38,7 @@ @switch (_currentState) { case State.Login: - -
- Continue -
+ break; case State.Loading: @@ -67,7 +63,7 @@ Authenticated, Failed } - + private State _currentState = State.Login; private bool Loading { get; set; } @@ -84,17 +80,11 @@ _currentState = State.Login; } - public async Task Login() - { - await ConfigManager.SaveAsync(); - ApiClient.SetupApiClient(); - await ProceedAuthenticated(); - } - private async Task ProceedAuthenticated() { Loading = true; _currentState = State.Loading; + await InvokeAsync(StateHasChanged); try { @@ -107,15 +97,16 @@ await ApiClient.RefreshShockers(); await LiveControlManager.RefreshConnections(); - - + _currentState = State.Authenticated; + await InvokeAsync(StateHasChanged); NavigationManager.NavigateTo("main"); } catch (Exception e) { _currentState = State.Failed; + await InvokeAsync(StateHasChanged); Snackbar.Add("Failed to authenticate", Severity.Error); } } diff --git a/ShockOsc/Ui/Pages/Authentication/LoginPart.razor b/ShockOsc/Ui/Pages/Authentication/LoginPart.razor new file mode 100644 index 0000000..0e43c79 --- /dev/null +++ b/ShockOsc/Ui/Pages/Authentication/LoginPart.razor @@ -0,0 +1,149 @@ +@using OneOf.Types +@using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Config + +@inject ConfigManager ConfigManager +@inject OpenShockApi ApiClient + + +
+Continue +
+
+
+ +
+ @if (_advancedSettingsExpanded) + { + Advanced Settings + } + else + { + Advanced Settings + } +
+ + + @if (!_useCustomServerDialog) + { + + https://api.shocklink.net/ (Production) + https://api-staging.shocklink.net/ (Staging) + @if (_customServerUri != null) + { + @_customServerUri (Custom) + } + + +
+ Use custom server + } + else + { + +
+ + Back + Save + } +
+ +
+ + + +@code { + private bool _useCustomServerDialog = false; + + private bool _advancedSettingsExpanded = false; + + private string? _server = null; + + [Parameter] public Func ProceedAuthenticated { get; set; } + + public async Task Login() + { + await ConfigManager.SaveAsync(); + ApiClient.SetupApiClient(); + await ProceedAuthenticated(); + } + + private bool ValidateCustomServerBool() => !ValídateCustomServer().IsT0; + + private OneOf.OneOf, StringIsNull, UriIsNotValid> ValídateCustomServer() + { + if (string.IsNullOrEmpty(_server)) return new StringIsNull(); + if (Uri.TryCreate(_server, UriKind.Absolute, out var uri)) + { + if (uri.Scheme != "http" && uri.Scheme != "https") return new UriIsNotValid(); + return new Success(uri); + } + + return new UriIsNotValid(); + } + + private void SaveCustomServer() + { + var validation = ValídateCustomServer(); + if (validation.IsT0) + { + _customServerUri = validation.AsT0.Value; + Server = BackendServer.Custom; + _useCustomServerDialog = false; + } + } + + + private void OnAdvancedSettingsClick() + { + _advancedSettingsExpanded = !_advancedSettingsExpanded; + } + + private enum BackendServer + { + Production, + Staging, + Custom + } + + private Uri? _customServerUri = null; + + private BackendServer Server + { + get => ConfigManager.Config.OpenShock.Backend.ToString() switch + { + ProductionServerString => BackendServer.Production, + StagingServerString => BackendServer.Staging, + _ => BackendServer.Custom + }; + set => ConfigManager.Config.OpenShock.Backend = value switch + { + BackendServer.Production => _productionServer, + BackendServer.Staging => _stagingServer, + BackendServer.Custom => _customServerUri, + }; + } + + private struct WrongSchema; + + private struct StringIsNull; + + private struct UriIsNotValid; + + private const string ProductionServerString = "https://api.shocklink.net/"; + private const string StagingServerString = "https://staging-api.shocklink.net/"; + + private static Uri _productionServer = new(ProductionServerString); + private static Uri _stagingServer = new(StagingServerString); + + protected override void OnInitialized() + { + if (Server == BackendServer.Custom) _customServerUri = ConfigManager.Config.OpenShock.Backend; + } + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Utils/ThemeDefinition.cs b/ShockOsc/Ui/Utils/ThemeDefinition.cs index 963a101..636ab1d 100644 --- a/ShockOsc/Ui/Utils/ThemeDefinition.cs +++ b/ShockOsc/Ui/Utils/ThemeDefinition.cs @@ -8,8 +8,8 @@ public static class ThemeDefinition { Palette = new PaletteDark { - Primary = "#8f38fd", - PrimaryDarken = "#722cca", + Primary = "#e14a6d", + PrimaryDarken = "#b31e40", Secondary = MudBlazor.Colors.Green.Accent4, AppbarBackground = MudBlazor.Colors.Red.Default, Background = "#2f2f2f", From 7e2d3e72b9a265648545064a2b15533b004da698 Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 23 Apr 2024 19:24:06 +0200 Subject: [PATCH 53/95] Fix import --- ShockOsc/Ui/Components/SideBar.razor | 1 + 1 file changed, 1 insertion(+) diff --git a/ShockOsc/Ui/Components/SideBar.razor b/ShockOsc/Ui/Components/SideBar.razor index 558668b..1aae13c 100644 --- a/ShockOsc/Ui/Components/SideBar.razor +++ b/ShockOsc/Ui/Components/SideBar.razor @@ -5,6 +5,7 @@ @using OpenShock.ShockOsc.Services @using OpenShock.ShockOsc.Ui.Components.Tabs @using OpenShock.ShockOsc.Ui.Utils +@using OpenShock.ShockOsc.Ui.Components.Parts @inject OpenShockApi Api @inject OpenShockHubClient ApiHubClient @inject ISnackbar Snackbar From ba280933b8b4ced474089d3c6e313646293aab15 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 24 Apr 2024 01:59:53 +0200 Subject: [PATCH 54/95] Exclusive control for physbone release --- ShockOsc/Backend/BackendHubManager.cs | 8 +++++--- ShockOsc/Services/ShockOsc.cs | 11 +++++++---- ShockOsc/ShockOsc.csproj | 10 +++++----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/ShockOsc/Backend/BackendHubManager.cs b/ShockOsc/Backend/BackendHubManager.cs index 00ee516..d69dce6 100644 --- a/ShockOsc/Backend/BackendHubManager.cs +++ b/ShockOsc/Backend/BackendHubManager.cs @@ -82,7 +82,7 @@ public Task CancelControl(ProgramGroup programGroup) /// /// /// - public async Task ControlGroup(Guid groupId, uint duration, byte intensity, ControlType type) + public async Task ControlGroup(Guid groupId, uint duration, byte intensity, ControlType type, bool exclusive = false) { if (groupId == Guid.Empty) { @@ -93,7 +93,8 @@ public async Task ControlGroup(Guid groupId, uint duration, byte intensity Id = x.Key, Duration = duration, Intensity = intensity, - Type = type + Type = type, + Exclusive = exclusive }); await _openShockHubClient.Control(controlCommandsAll); return true; @@ -107,7 +108,8 @@ public async Task ControlGroup(Guid groupId, uint duration, byte intensity Id = x, Duration = duration, Intensity = intensity, - Type = type + Type = type, + Exclusive = exclusive }); await _openShockHubClient.Control(controlCommands); diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index a398905..00df867 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -380,7 +380,7 @@ private async Task SenderLoopAsync() } } - private async Task InstantShock(ProgramGroup programGroup, uint duration, byte intensity) + private async Task InstantShock(ProgramGroup programGroup, uint duration, byte intensity, bool exclusive = false) { programGroup.LastExecuted = DateTime.UtcNow; programGroup.LastDuration = duration; @@ -396,7 +396,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i "Sending shock to {GroupName} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s", programGroup.Name, intensity, intensityPercentage, inSeconds); - await _backendHubManager.ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock); + await _backendHubManager.ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock, exclusive); if (!_configManager.Config.Osc.Chatbox) return; // Chatbox message local @@ -511,7 +511,8 @@ private async Task CheckLogic() } byte intensity; - + var exclusive = false; + if (programGroup.TriggerMethod == TriggerMethod.PhysBoneRelease) { intensity = programGroup.ConfigGroup is { OverrideIntensity: true } @@ -521,10 +522,12 @@ private async Task CheckLogic() : (byte)MathUtils.LerpFloat(config.IntensityRange.Min, config.IntensityRange.Max, programGroup.LastStretchValue); programGroup.LastStretchValue = 0; + + exclusive = true; } else intensity = GetIntensity(programGroup); - InstantShock(programGroup, GetDuration(programGroup), intensity); + InstantShock(programGroup, GetDuration(programGroup), intensity, exclusive); } } diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 380c583..1958aaf 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -49,17 +49,17 @@ - + - - + + - - + + From 716eae46cb802caf9a8ee93a5e3f0019604f5ad4 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 24 Apr 2024 22:47:29 +0200 Subject: [PATCH 55/95] Check cooldown for IShock --- ShockOsc/Services/ShockOsc.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index 00df867..a91b9f1 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -293,7 +293,6 @@ private async Task ReceiveLogic() switch (action) { case "IShock": - // TODO: check Cooldowns if (value is not true) return; if (_underscoreConfig.KillSwitch) { @@ -308,6 +307,21 @@ private async Task ReceiveLogic() await LogIgnoredAfk(); return; } + + var cooldownTime = _configManager.Config.Behaviour.CooldownTime; + if(programGroup.ConfigGroup is { OverrideCooldownTime: true }) + cooldownTime = programGroup.ConfigGroup.CooldownTime; + + var isActiveOrOnCooldown = + programGroup.LastExecuted.AddMilliseconds(cooldownTime) + .AddMilliseconds(programGroup.LastDuration) > DateTime.UtcNow; + + if (isActiveOrOnCooldown) + { + programGroup.TriggerMethod = TriggerMethod.None; + _logger.LogInformation("Ignoring IShock, group {Group} is on cooldown", programGroup.Name); + return; + } OsTask.Run(() => InstantShock(programGroup, GetDuration(programGroup), GetIntensity(programGroup))); From 142a93507465fa68d6035cbb6a66c1cc8a271d9d Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 24 Apr 2024 23:56:00 +0200 Subject: [PATCH 56/95] requires restart notice on osc options --- ShockOsc/Ui/Components/SideBar.razor | 4 +--- ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ShockOsc/Ui/Components/SideBar.razor b/ShockOsc/Ui/Components/SideBar.razor index 1aae13c..27cf124 100644 --- a/ShockOsc/Ui/Components/SideBar.razor +++ b/ShockOsc/Ui/Components/SideBar.razor @@ -48,8 +48,7 @@
ShockOSC v@(Version) - -
+
@@ -72,7 +71,6 @@
-
diff --git a/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor b/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor index 2ba53f3..5b254b3 100644 --- a/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor @@ -12,10 +12,10 @@
- OSC Options + OSC Options (changing requires restart) - + @if (!ConfigManager.Config.Osc.OscQuery) {
From aa6fb7a5664213459ce32bbe3d23ba64b4ef5b34 Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 25 Apr 2024 02:10:32 +0200 Subject: [PATCH 57/95] Updater refactor and update variable standard?! ayo? --- ShockOsc/Services/Updater.cs | 61 +++++++++++++++-------- ShockOsc/Ui/Components/UpdateLogout.razor | 14 ++++-- ShockOsc/Ui/Utils/UpdateableVariable.cs | 22 ++++++++ 3 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 ShockOsc/Ui/Utils/UpdateableVariable.cs diff --git a/ShockOsc/Services/Updater.cs b/ShockOsc/Services/Updater.cs index 978219a..a6bd2c2 100644 --- a/ShockOsc/Services/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; +using OpenShock.ShockOsc.Ui.Utils; namespace OpenShock.ShockOsc.Services; @@ -11,19 +12,23 @@ public sealed class Updater { private const string GithubLatest = "https://api.github.com/repos/OpenShock/ShockOsc/releases/latest"; private const string SetupFileName = "ShockOSC_Setup.exe"; // OpenShock.ShockOsc.exe - + private static readonly HttpClient HttpClient = new(); - + private readonly string _setupFilePath = Path.Combine(Environment.CurrentDirectory, SetupFileName); - private readonly Version _currentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? throw new Exception("Could not determine ShockOsc version"); + + private readonly Version _currentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? + throw new Exception("Could not determine ShockOsc version"); + private Uri? LatestDownloadUrl { get; set; } - + private readonly ILogger _logger; private readonly ConfigManager _configManager; - public bool UpdateAvailable { get; private set; } + + public UpdateableVariable UpdateAvailable { get; } = new(false); public Version? LatestVersion { get; private set; } - + public Updater(ILogger logger, ConfigManager configManager) { @@ -45,7 +50,7 @@ private static bool TryDeleteFile(string fileName) return false; } } - + private async Task<(Version, GithubReleaseResponse.Asset)?> GetLatestRelease() { _logger.LogInformation("Checking GitHub for updates..."); @@ -55,18 +60,24 @@ private static bool TryDeleteFile(string fileName) var res = await HttpClient.GetAsync(GithubLatest); if (!res.IsSuccessStatusCode) { - _logger.LogWarning("Failed to get latest version information from GitHub. {StatusCode}", res.StatusCode); + _logger.LogWarning("Failed to get latest version information from GitHub. {StatusCode}", + res.StatusCode); return null; } - var json = await JsonSerializer.DeserializeAsync(await res.Content.ReadAsStreamAsync()); + var json = + await JsonSerializer.DeserializeAsync(await res.Content.ReadAsStreamAsync()); if (json == null) { _logger.LogWarning("Could not deserialize json"); return null; } - if (!Version.TryParse(json.TagName[1..], out var version)) + var tagName = json.TagName; + if (!string.IsNullOrEmpty(tagName) && tagName[0] == 'v') + tagName = tagName[1..]; + + if (!Version.TryParse(tagName, out var version)) { _logger.LogWarning("Failed to parse version. Value: {Version}", json.TagName); return null; @@ -75,7 +86,8 @@ private static bool TryDeleteFile(string fileName) var asset = json.Assets.FirstOrDefault(x => x.Name == SetupFileName); if (asset == null) { - _logger.LogWarning("Could not find asset with {@SetupName}. Assets found: {@Assets}", SetupFileName, json.Assets); + _logger.LogWarning("Could not find asset with {@SetupName}. Assets found: {@Assets}", SetupFileName, + json.Assets); return null; } @@ -88,32 +100,39 @@ private static bool TryDeleteFile(string fileName) } } - public async Task CheckUpdate() + public async Task CheckUpdate() { var latestVersion = await GetLatestRelease(); - if (latestVersion is null) return false; + if (latestVersion == null) + { + UpdateAvailable.Value = false; + return; + } if (latestVersion.Value.Item1 <= _currentVersion) { - _logger.LogInformation("ShockOsc is up to date ([{Version}] >= [{LatestVersion}])", _currentVersion, latestVersion.Value.Item1); - UpdateAvailable = false; - return false; + _logger.LogInformation("ShockOsc is up to date ([{Version}] >= [{LatestVersion}])", _currentVersion, + latestVersion.Value.Item1); + UpdateAvailable.Value = false; + return; } - UpdateAvailable = true; + UpdateAvailable.Value = true; LatestVersion = latestVersion.Value.Item1; LatestDownloadUrl = latestVersion.Value.Item2.BrowserDownloadUrl; if (_configManager.Config.LastIgnoredVersion != null && _configManager.Config.LastIgnoredVersion >= latestVersion.Value.Item1) { - _logger.LogInformation("ShockOsc is not up to date. Skipping update due to previous postpone. You can reenable the updater by setting 'LastIgnoredVersion' to null"); - return false; + _logger.LogInformation( + "ShockOsc is not up to date. Skipping update due to previous postpone. You can reenable the updater by setting 'LastIgnoredVersion' to null"); + UpdateAvailable.Value = false; + return; } - + _logger.LogWarning( "ShockOsc is not up to date. Newest version is [{NewVersion}] but you are on [{CurrentVersion}]!", latestVersion.Value.Item1, _currentVersion); - return true; + UpdateAvailable.Value = true; } public async Task DoUpdate() diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Components/UpdateLogout.razor index 365490f..111b613 100644 --- a/ShockOsc/Ui/Components/UpdateLogout.razor +++ b/ShockOsc/Ui/Components/UpdateLogout.razor @@ -16,7 +16,13 @@ protected override async Task OnInitializedAsync() { - if (await Updater.CheckUpdate()) OpenUpdateDialog(); + Updater.UpdateAvailable.OnValueChanged += v => + { + InvokeAsync(StateHasChanged); + OpenUpdateDialog(); + }; + + await Updater.CheckUpdate(); } private async Task Logout() @@ -37,9 +43,9 @@ - - - + + + diff --git a/ShockOsc/Ui/Utils/UpdateableVariable.cs b/ShockOsc/Ui/Utils/UpdateableVariable.cs new file mode 100644 index 0000000..bfee9fd --- /dev/null +++ b/ShockOsc/Ui/Utils/UpdateableVariable.cs @@ -0,0 +1,22 @@ +namespace OpenShock.ShockOsc.Ui.Utils; + +public sealed class UpdateableVariable(T internalValue) +{ + public T Value + { + get => internalValue; + set + { + if (internalValue!.Equals(value)) return; + internalValue = value; + OnValueChanged?.Invoke(value); + } + } + + public event Action? OnValueChanged; + + public void UpdateWithoutNotify(T newValue) + { + internalValue = newValue; + } +} \ No newline at end of file From 82cf4b5bfebf2a4df99c4c82bcf0ac3263a8f62a Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 25 Apr 2024 02:18:47 +0200 Subject: [PATCH 58/95] Simplify build, and no need for tests for now, surely... --- .github/workflows/ci-windows.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 2d4fa36..807b3ba 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -39,15 +39,6 @@ jobs: restore-keys: | ${{ runner.os }}-nuget- - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore - - - name: Test - run: dotnet test --no-restore --verbosity normal - - name: Publish ShockOSC Windows run: dotnet publish ShockOsc/ShockOsc.csproj -c Release -f net8.0-windows10.0.19041.0 -o ./publish/ShockOsc From a3154d562bde9f2dd314208955799194f44018a5 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 26 Apr 2024 23:06:31 +0200 Subject: [PATCH 59/95] State part rework, error handling on blazor exceptions --- ShockOsc/MauiProgram.cs | 2 +- ShockOsc/Services/LiveControlManager.cs | 2 +- ShockOsc/Services/ShockOsc.cs | 4 +- ShockOsc/ShockOsc.csproj | 6 +-- ShockOsc/Ui/Components/MainLayout.razor | 4 -- ShockOsc/Ui/Components/Parts/StatePart.razor | 11 +++- .../Ui/Components/Parts/StatePart.razor.cs | 50 ++++++++++++++++--- ShockOsc/Ui/Components/SideBar.razor | 38 ++++++-------- .../Ui/Components/Tabs/DashboardTab.razor | 2 + ShockOsc/Ui/Components/Tabs/LogsTab.razor | 16 +++--- ShockOsc/Ui/ErrorHandling/CodeBlock.razor | 45 +++++++++++++++++ .../ContainerWithExceptionHandling.razor | 20 ++++++++ ShockOsc/Ui/ErrorHandling/ExceptionView.razor | 41 +++++++++++++++ ShockOsc/Ui/Main.razor | 41 +++++++++------ .../Pages/Authentication/Authenticate.razor | 4 -- ShockOsc/wwwroot/index.html | 4 +- 16 files changed, 222 insertions(+), 68 deletions(-) create mode 100644 ShockOsc/Ui/ErrorHandling/CodeBlock.razor create mode 100644 ShockOsc/Ui/ErrorHandling/ContainerWithExceptionHandling.razor create mode 100644 ShockOsc/Ui/ErrorHandling/ExceptionView.razor diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index d1f14fe..0ba2b94 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -125,7 +125,7 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() builder.Services.AddMudServices(); builder.Services.AddMauiBlazorWebView(); - + // <---- App ----> builder diff --git a/ShockOsc/Services/LiveControlManager.cs b/ShockOsc/Services/LiveControlManager.cs index 2d5fd78..3fc501e 100644 --- a/ShockOsc/Services/LiveControlManager.cs +++ b/ShockOsc/Services/LiveControlManager.cs @@ -101,7 +101,7 @@ private async Task RefreshInternal() _configManager.Config.OpenShock.Token, _liveControlLogger); LiveControlClients.Add(device.Id, client); - client.OnStateUpdate += async (state) => + client.State.OnValueChanged += async state => { _logger.LogTrace("Live control client for device [{DeviceId}] status updated {Status}", device.Id, state); diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index a91b9f1..066f770 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -407,8 +407,8 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i programGroup.TriggerMethod = TriggerMethod.None; var inSeconds = MathF.Round(duration / 1000f, 1).ToString(CultureInfo.InvariantCulture); _logger.LogInformation( - "Sending shock to {GroupName} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s", - programGroup.Name, intensity, intensityPercentage, inSeconds); + "Sending shock to {GroupName} Intensity: {Intensity} IntensityPercentage: {IntensityPercentage}% Length:{Length}s Exclusive: {Exclusive}", + programGroup.Name, intensity, intensityPercentage, inSeconds, exclusive); await _backendHubManager.ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock, exclusive); diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 1958aaf..9efdfa7 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -57,9 +57,9 @@ - - - + + + diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor index 30337b5..6976c49 100644 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ b/ShockOsc/Ui/Components/MainLayout.razor @@ -14,10 +14,6 @@ - - - -
diff --git a/ShockOsc/Ui/Components/Parts/StatePart.razor b/ShockOsc/Ui/Components/Parts/StatePart.razor index c118b52..65de815 100644 --- a/ShockOsc/Ui/Components/Parts/StatePart.razor +++ b/ShockOsc/Ui/Components/Parts/StatePart.razor @@ -1,7 +1,14 @@ 
@Text - - + + + + + + @Client.State.Value.ToString() + @Client.Gateway
+ Latency: @(Client.Latency.Value)ms +
diff --git a/ShockOsc/Ui/Components/Parts/StatePart.razor.cs b/ShockOsc/Ui/Components/Parts/StatePart.razor.cs index cc8eacf..b0f62e2 100644 --- a/ShockOsc/Ui/Components/Parts/StatePart.razor.cs +++ b/ShockOsc/Ui/Components/Parts/StatePart.razor.cs @@ -1,19 +1,55 @@ using Microsoft.AspNetCore.Components; +using OpenShock.SDK.CSharp.Live; +using OpenShock.SDK.CSharp.Live.LiveControlModels; using Color = MudBlazor.Color; namespace OpenShock.ShockOsc.Ui.Components.Parts; -public partial class StatePart : ComponentBase +public partial class StatePart : ComponentBase, IDisposable { [Parameter] - public required Color IconColor { get; set; } + public required IOpenShockLiveControlClient Client { get; set; } [Parameter] - public required string Icon { get; set; } + public required string Text { get; set; } + - [Parameter] - public required string Tooltip { get; set; } + private Task StateOnValueChanged(WebsocketConnectionState state) + { + return InvokeAsync(StateHasChanged); + } - [Parameter] - public required string Text { get; set; } + private Color GetConnectionStateColor() => + Client.State.Value switch + { + WebsocketConnectionState.Connected => Color.Success, + WebsocketConnectionState.Reconnecting => Color.Warning, + WebsocketConnectionState.Connecting => Color.Warning, + WebsocketConnectionState.Disconnected => Color.Error, + _ => Color.Error + }; + + protected override void OnInitialized() + { + Client.State.OnValueChanged += StateOnValueChanged; + Client.Latency.OnValueChanged += LatencyOnValueChanged; + } + + private Task LatencyOnValueChanged(ulong arg) + { + return InvokeAsync(StateHasChanged); + } + + private bool _disposed = false; + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + Client.State.OnValueChanged -= StateOnValueChanged; + Client.Latency.OnValueChanged -= LatencyOnValueChanged; + + GC.SuppressFinalize(this); + } } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/SideBar.razor b/ShockOsc/Ui/Components/SideBar.razor index 27cf124..688c1be 100644 --- a/ShockOsc/Ui/Components/SideBar.razor +++ b/ShockOsc/Ui/Components/SideBar.razor @@ -49,23 +49,28 @@ ShockOSC v@(Version)
- - + +
+ Hub + + + +
@foreach (var device in Api.Devices) { if (LiveControlManager.LiveControlClients.TryGetValue(device.Id, out var client)) { - + } else { - +
+ @device.Name.Truncate(13) + + + +
} } @@ -92,7 +97,7 @@ get => _currentTab; set { - if(_currentTab == value) return; + if (_currentTab == value) return; CurrentTabChanged.InvokeAsync(value); _currentTab = value; } @@ -118,16 +123,5 @@ _ => Color.Error }; - private static Color GetConnectionStateColor(WebsocketConnectionState state) => - state switch - { - WebsocketConnectionState.Connected => Color.Success, - WebsocketConnectionState.Reconnecting => Color.Warning, - WebsocketConnectionState.Connecting => Color.Warning, - WebsocketConnectionState.Disconnected => Color.Error, - _ => Color.Error - }; - - +} -} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor index c2acc0e..fdb46be 100644 --- a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor @@ -3,6 +3,8 @@

DashboardTab

+Yes + @code { } \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Tabs/LogsTab.razor b/ShockOsc/Ui/Components/Tabs/LogsTab.razor index 285663d..90295c1 100644 --- a/ShockOsc/Ui/Components/Tabs/LogsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/LogsTab.razor @@ -1,6 +1,7 @@ @using OpenShock.ShockOsc.Logging @using OpenShock.ShockOsc.Utils @using Serilog.Events +@using OpenShock.ShockOsc.Ui.ErrorHandling @implements IDisposable @@ -17,8 +18,8 @@ @if (context.IsExpanded) { - - + +
@@ -27,9 +28,10 @@

- - @context.Message - + @* HACK, thank you html tables *@ +
+ +


@@ -44,10 +46,12 @@ private void LogRowClick(TableRowClickEventArgs rowClickEventArgs) { + if(rowClickEventArgs.Item == null) return; rowClickEventArgs.Item.IsExpanded = !rowClickEventArgs.Item.IsExpanded; } - private string RowClassFunc(LogStore.LogEntry? log, int arg2) => log == null ? string.Empty : GetLogClass(log.Level) + (log.IsExpanded ? " expanded" : ""); + private string RowClassFunc(LogStore.LogEntry? log, int arg2) => + log == null ? string.Empty : GetLogClass(log.Level) + (log.IsExpanded ? " expanded" : ""); protected override void OnInitialized() { diff --git a/ShockOsc/Ui/ErrorHandling/CodeBlock.razor b/ShockOsc/Ui/ErrorHandling/CodeBlock.razor new file mode 100644 index 0000000..1bbc256 --- /dev/null +++ b/ShockOsc/Ui/ErrorHandling/CodeBlock.razor @@ -0,0 +1,45 @@ +@inject ISnackbar Snackbar + + +
+                @Value
+    
+ +
+ + + +@code { + [Parameter] public string Value { get; set; } + + private async Task CopyTextToClipboard() + { + await Clipboard.SetTextAsync(Value); + Snackbar.Add("Copied to clipboard", Severity.Success); + } + +} \ No newline at end of file diff --git a/ShockOsc/Ui/ErrorHandling/ContainerWithExceptionHandling.razor b/ShockOsc/Ui/ErrorHandling/ContainerWithExceptionHandling.razor new file mode 100644 index 0000000..83fcd0b --- /dev/null +++ b/ShockOsc/Ui/ErrorHandling/ContainerWithExceptionHandling.razor @@ -0,0 +1,20 @@ +
+ + + @ChildContent + + + + + +
+ +@code { + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary Attributes { get; set; } + + [Parameter] public RenderFragment ChildContent { get; set; } + + private ErrorBoundary _boundary = null!; +} \ No newline at end of file diff --git a/ShockOsc/Ui/ErrorHandling/ExceptionView.razor b/ShockOsc/Ui/ErrorHandling/ExceptionView.razor new file mode 100644 index 0000000..d63aa3e --- /dev/null +++ b/ShockOsc/Ui/ErrorHandling/ExceptionView.razor @@ -0,0 +1,41 @@ +@using System.Diagnostics + + + + An unknown error occurred +
+ Please report this to us on GitHub + + +
+ + + Open Github + + + Resume + + +
+
+ +@code { + + /// + /// Resume button click event + /// + [Parameter] + public EventCallback OnResume { get; set; } + + /// + /// Exception to display + /// + [Parameter] + public Exception Exception { get; set; } + + private void OpenGithub() + { + Process.Start(new ProcessStartInfo("https://github.com/OpenShock/ShockOsc/issues/new/choose") { UseShellExecute = true }); + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Main.razor b/ShockOsc/Ui/Main.razor index 5b60fbb..c9001fa 100644 --- a/ShockOsc/Ui/Main.razor +++ b/ShockOsc/Ui/Main.razor @@ -1,16 +1,29 @@ @using OpenShock.ShockOsc.Ui.Components +@using OpenShock.ShockOsc.Ui.ErrorHandling +@using OpenShock.ShockOsc.Ui.Utils - - - - - - - -

Sorry, there's nothing at this address.

-
-
- -

Page is loading...

-
-
\ No newline at end of file + + + + + + + + + + + + +

Sorry, there's nothing at this address.

+
+
+ +

Page is loading...

+
+
+
+ +@code { + + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index c8015e6..1b8b9a4 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -17,10 +17,6 @@ @page "/" - - - - diff --git a/ShockOsc/wwwroot/index.html b/ShockOsc/wwwroot/index.html index 5ed7b49..f665534 100644 --- a/ShockOsc/wwwroot/index.html +++ b/ShockOsc/wwwroot/index.html @@ -17,11 +17,11 @@
Loading...
-
+ From d2bfc31c0a804ab65245ece712f785629a92ae99 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 27 Apr 2024 00:34:35 +0200 Subject: [PATCH 60/95] Updater semver support --- ShockOsc/Config/ConfigManager.cs | 5 +-- ShockOsc/Config/ShockOscConfig.cs | 6 ++-- ShockOsc/MauiProgram.cs | 4 +++ ShockOsc/Services/Updater.cs | 42 ++++++++++++----------- ShockOsc/ShockOsc.csproj | 7 ++-- ShockOsc/Ui/Components/UpdateDialog.razor | 6 ++++ ShockOsc/Ui/Components/UpdateLogout.razor | 5 +-- ShockOsc/Ui/Utils/UpdateableVariable.cs | 22 ------------ 8 files changed, 46 insertions(+), 51 deletions(-) delete mode 100644 ShockOsc/Ui/Utils/UpdateableVariable.cs diff --git a/ShockOsc/Config/ConfigManager.cs b/ShockOsc/Config/ConfigManager.cs index 724e1eb..5277cd1 100644 --- a/ShockOsc/Config/ConfigManager.cs +++ b/ShockOsc/Config/ConfigManager.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Hub.Utils; using OpenShock.ShockOsc.Utils; namespace OpenShock.ShockOsc.Config; @@ -31,7 +32,7 @@ public ConfigManager(ILogger logger) { config = JsonSerializer.Deserialize(json, Options); } - catch (JsonException e) + catch (Exception e) { _logger.LogCritical(e, "Error during deserialization/loading of config"); _logger.LogWarning("Attempting to move old config and generate a new one"); @@ -55,7 +56,7 @@ public ConfigManager(ILogger logger) private static readonly JsonSerializerOptions Options = new() { WriteIndented = true, - Converters = { new JsonStringEnumConverter() } + Converters = { new JsonStringEnumConverter(), new SemVersionJsonConverter() } }; private readonly SemaphoreSlim _saveLock = new(1, 1); diff --git a/ShockOsc/Config/ShockOscConfig.cs b/ShockOsc/Config/ShockOscConfig.cs index 845e530..71dcdbe 100644 --- a/ShockOsc/Config/ShockOscConfig.cs +++ b/ShockOsc/Config/ShockOscConfig.cs @@ -1,4 +1,6 @@ -namespace OpenShock.ShockOsc.Config; +using Semver; + +namespace OpenShock.ShockOsc.Config; public sealed class ShockOscConfig { @@ -7,7 +9,7 @@ public sealed class ShockOscConfig public OpenShockConf OpenShock { get; set; } = new(); public ChatboxConf Chatbox { get; set; } = new(); public IDictionary Groups { get; set; } = new Dictionary(); - public Version? LastIgnoredVersion { get; set; } = null; + public SemVersion? LastIgnoredVersion { get; set; } = null; public AppConfig App { get; set; } = new(); } \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 0ba2b94..0e6500c 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -7,6 +7,7 @@ using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Utils; using Serilog; using MauiApp = OpenShock.ShockOsc.Ui.MauiApp; @@ -147,6 +148,9 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() app.Services.GetRequiredService(); app.Services.GetRequiredService().Start(); + var updater = app.Services.GetRequiredService(); + OsTask.Run(updater.CheckUpdate); + return app; } } \ No newline at end of file diff --git a/ShockOsc/Services/Updater.cs b/ShockOsc/Services/Updater.cs index a6bd2c2..a12a989 100644 --- a/ShockOsc/Services/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -2,23 +2,24 @@ using System.Reflection; using System.Text.Json; using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Updatables; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.Ui.Utils; +using Semver; namespace OpenShock.ShockOsc.Services; public sealed class Updater { - private const string GithubLatest = "https://api.github.com/repos/OpenShock/ShockOsc/releases/latest"; + private const string GithubLatest = "https://api.github.com/repos/OpenShock/ShockOsc/releases/152715042"; private const string SetupFileName = "ShockOSC_Setup.exe"; // OpenShock.ShockOsc.exe private static readonly HttpClient HttpClient = new(); private readonly string _setupFilePath = Path.Combine(Environment.CurrentDirectory, SetupFileName); - private readonly Version _currentVersion = Assembly.GetEntryAssembly()?.GetName().Version ?? - throw new Exception("Could not determine ShockOsc version"); + private readonly SemVersion _currentVersion = SemVersion.Parse(typeof(Updater).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); private Uri? LatestDownloadUrl { get; set; } @@ -26,8 +27,9 @@ public sealed class Updater private readonly ConfigManager _configManager; - public UpdateableVariable UpdateAvailable { get; } = new(false); - public Version? LatestVersion { get; private set; } + public UpdatableVariable UpdateAvailable { get; } = new(false); + public bool IsPostponed { get; private set; } + public SemVersion? LatestVersion { get; private set; } public Updater(ILogger logger, ConfigManager configManager) @@ -51,7 +53,7 @@ private static bool TryDeleteFile(string fileName) } } - private async Task<(Version, GithubReleaseResponse.Asset)?> GetLatestRelease() + private async Task<(SemVersion, GithubReleaseResponse.Asset)?> GetLatestRelease() { _logger.LogInformation("Checking GitHub for updates..."); @@ -74,23 +76,21 @@ private static bool TryDeleteFile(string fileName) } var tagName = json.TagName; - if (!string.IsNullOrEmpty(tagName) && tagName[0] == 'v') - tagName = tagName[1..]; - if (!Version.TryParse(tagName, out var version)) + if (!SemVersion.TryParse(tagName, SemVersionStyles.AllowV, out var version)) { _logger.LogWarning("Failed to parse version. Value: {Version}", json.TagName); return null; } - var asset = json.Assets.FirstOrDefault(x => x.Name == SetupFileName); + var asset = json.Assets.FirstOrDefault(x => x.Name.Equals(SetupFileName, StringComparison.InvariantCultureIgnoreCase)); if (asset == null) { _logger.LogWarning("Could not find asset with {@SetupName}. Assets found: {@Assets}", SetupFileName, json.Assets); return null; } - + return (version, asset); } catch (Exception e) @@ -102,16 +102,21 @@ private static bool TryDeleteFile(string fileName) public async Task CheckUpdate() { + IsPostponed = false; + UpdateAvailable.Value = false; + var latestVersion = await GetLatestRelease(); if (latestVersion == null) { UpdateAvailable.Value = false; return; } - if (latestVersion.Value.Item1 <= _currentVersion) + + var comparison = _currentVersion.ComparePrecedenceTo(latestVersion.Value.Item1); + if (comparison >= 0) { - _logger.LogInformation("ShockOsc is up to date ([{Version}] >= [{LatestVersion}])", _currentVersion, - latestVersion.Value.Item1); + _logger.LogInformation("ShockOsc is up to date ([{Version}] >= [{LatestVersion}]) ({Comp})", _currentVersion, + latestVersion.Value.Item1, comparison); UpdateAvailable.Value = false; return; } @@ -119,20 +124,17 @@ public async Task CheckUpdate() UpdateAvailable.Value = true; LatestVersion = latestVersion.Value.Item1; LatestDownloadUrl = latestVersion.Value.Item2.BrowserDownloadUrl; - if (_configManager.Config.LastIgnoredVersion != null && - _configManager.Config.LastIgnoredVersion >= latestVersion.Value.Item1) + if (_configManager.Config.LastIgnoredVersion != null && _configManager.Config.LastIgnoredVersion.ComparePrecedenceTo(latestVersion.Value.Item1) >= 0) { _logger.LogInformation( - "ShockOsc is not up to date. Skipping update due to previous postpone. You can reenable the updater by setting 'LastIgnoredVersion' to null"); - UpdateAvailable.Value = false; + "ShockOsc is not up to date. Skipping update due to previous postpone"); + IsPostponed = true; return; } _logger.LogWarning( "ShockOsc is not up to date. Newest version is [{NewVersion}] but you are on [{CurrentVersion}]!", latestVersion.Value.Item1, _currentVersion); - - UpdateAvailable.Value = true; } public async Task DoUpdate() diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 9efdfa7..afe2407 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -10,14 +10,13 @@ false enable ShockOsc - com.companyname.shockosc + org.openshock.shockosc 2C147618-324E-4C37-B4B6-C50C8A9BD5ED - 1.0 - 1 OpenShock.ShockOsc OpenShock.ShockOsc OpenShock 2.0.0 + 2.0.0-rc.2 Resources\openshock-icon.ico true ShockOsc @@ -34,6 +33,7 @@ en en-US;en + false @@ -60,6 +60,7 @@ + diff --git a/ShockOsc/Ui/Components/UpdateDialog.razor b/ShockOsc/Ui/Components/UpdateDialog.razor index 01f07d0..85aede1 100644 --- a/ShockOsc/Ui/Components/UpdateDialog.razor +++ b/ShockOsc/Ui/Components/UpdateDialog.razor @@ -16,6 +16,11 @@ await ConfigManager.SaveAsync(); MudDialog.Close(DialogResult.Ok(true)); } + + private void Dismiss() + { + MudDialog.Close(DialogResult.Ok(true)); + } private void DownloadUpdate() { @@ -44,6 +49,7 @@ @if (!_isDownloading) { + Dismiss Skip Update } diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Components/UpdateLogout.razor index 111b613..d663f99 100644 --- a/ShockOsc/Ui/Components/UpdateLogout.razor +++ b/ShockOsc/Ui/Components/UpdateLogout.razor @@ -19,10 +19,11 @@ Updater.UpdateAvailable.OnValueChanged += v => { InvokeAsync(StateHasChanged); - OpenUpdateDialog(); + if (v && !Updater.IsPostponed) OpenUpdateDialog(); }; - await Updater.CheckUpdate(); + if (Updater.UpdateAvailable.Value && !Updater.IsPostponed) OpenUpdateDialog(); + } private async Task Logout() diff --git a/ShockOsc/Ui/Utils/UpdateableVariable.cs b/ShockOsc/Ui/Utils/UpdateableVariable.cs deleted file mode 100644 index bfee9fd..0000000 --- a/ShockOsc/Ui/Utils/UpdateableVariable.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace OpenShock.ShockOsc.Ui.Utils; - -public sealed class UpdateableVariable(T internalValue) -{ - public T Value - { - get => internalValue; - set - { - if (internalValue!.Equals(value)) return; - internalValue = value; - OnValueChanged?.Invoke(value); - } - } - - public event Action? OnValueChanged; - - public void UpdateWithoutNotify(T newValue) - { - internalValue = newValue; - } -} \ No newline at end of file From e1f03d50589e0e50d8b0be3dc685dc926ed873cc Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 27 Apr 2024 00:47:58 +0200 Subject: [PATCH 61/95] Use version attribute in side bar --- ShockOsc/Services/Updater.cs | 10 +++++----- ShockOsc/Ui/Components/SideBar.razor | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ShockOsc/Services/Updater.cs b/ShockOsc/Services/Updater.cs index a12a989..175988d 100644 --- a/ShockOsc/Services/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -19,7 +19,7 @@ public sealed class Updater private readonly string _setupFilePath = Path.Combine(Environment.CurrentDirectory, SetupFileName); - private readonly SemVersion _currentVersion = SemVersion.Parse(typeof(Updater).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); + private static readonly SemVersion CurrentVersion = SemVersion.Parse(typeof(Updater).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); private Uri? LatestDownloadUrl { get; set; } @@ -36,7 +36,7 @@ public Updater(ILogger logger, ConfigManager configManager) { _logger = logger; _configManager = configManager; - HttpClient.DefaultRequestHeaders.Add("User-Agent", $"ShockOsc/{_currentVersion}"); + HttpClient.DefaultRequestHeaders.Add("User-Agent", $"ShockOsc/{CurrentVersion}"); } private static bool TryDeleteFile(string fileName) @@ -112,10 +112,10 @@ public async Task CheckUpdate() return; } - var comparison = _currentVersion.ComparePrecedenceTo(latestVersion.Value.Item1); + var comparison = CurrentVersion.ComparePrecedenceTo(latestVersion.Value.Item1); if (comparison >= 0) { - _logger.LogInformation("ShockOsc is up to date ([{Version}] >= [{LatestVersion}]) ({Comp})", _currentVersion, + _logger.LogInformation("ShockOsc is up to date ([{Version}] >= [{LatestVersion}]) ({Comp})", CurrentVersion, latestVersion.Value.Item1, comparison); UpdateAvailable.Value = false; return; @@ -134,7 +134,7 @@ public async Task CheckUpdate() _logger.LogWarning( "ShockOsc is not up to date. Newest version is [{NewVersion}] but you are on [{CurrentVersion}]!", - latestVersion.Value.Item1, _currentVersion); + latestVersion.Value.Item1, CurrentVersion); } public async Task DoUpdate() diff --git a/ShockOsc/Ui/Components/SideBar.razor b/ShockOsc/Ui/Components/SideBar.razor index 688c1be..deb116d 100644 --- a/ShockOsc/Ui/Components/SideBar.razor +++ b/ShockOsc/Ui/Components/SideBar.razor @@ -1,11 +1,12 @@ -@using Microsoft.AspNetCore.SignalR.Client +@using System.Reflection +@using Microsoft.AspNetCore.SignalR.Client @using OpenShock.SDK.CSharp.Hub -@using OpenShock.SDK.CSharp.Live.LiveControlModels @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Services @using OpenShock.ShockOsc.Ui.Components.Tabs @using OpenShock.ShockOsc.Ui.Utils @using OpenShock.ShockOsc.Ui.Components.Parts +@using Semver @inject OpenShockApi Api @inject OpenShockHubClient ApiHubClient @inject ISnackbar Snackbar @@ -47,7 +48,7 @@
- ShockOSC v@(Version) + ShockOSC v@(Version.WithoutMetadata().ToString())
@@ -85,7 +86,7 @@ @code { - private static readonly string Version = typeof(MainLayout).Assembly.GetName().Version!.ToString(); + private static readonly SemVersion Version = SemVersion.Parse(typeof(SideBar).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); [Parameter] public EventCallback CurrentTabChanged { get; set; } From 559d29b3f233645f2c9dc40e08debda5a729e868 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 27 Apr 2024 01:01:21 +0200 Subject: [PATCH 62/95] Change config folder to be under openshock --- ShockOsc/Config/ConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShockOsc/Config/ConfigManager.cs b/ShockOsc/Config/ConfigManager.cs index 5277cd1..302a83c 100644 --- a/ShockOsc/Config/ConfigManager.cs +++ b/ShockOsc/Config/ConfigManager.cs @@ -8,7 +8,7 @@ namespace OpenShock.ShockOsc.Config; public sealed class ConfigManager { - private static readonly string Path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\ShockOSC\config.json"; + private static readonly string Path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\OpenShock\ShockOSC\config.json"; private readonly ILogger _logger; public ShockOscConfig Config { get; } From 3b94dfa953623d9fc12f2c710089cece103d4ecb Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 27 Apr 2024 01:01:47 +0200 Subject: [PATCH 63/95] Change updater temp file location --- ShockOsc/Services/Updater.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShockOsc/Services/Updater.cs b/ShockOsc/Services/Updater.cs index 175988d..1c47055 100644 --- a/ShockOsc/Services/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -17,7 +17,7 @@ public sealed class Updater private static readonly HttpClient HttpClient = new(); - private readonly string _setupFilePath = Path.Combine(Environment.CurrentDirectory, SetupFileName); + private readonly string _setupFilePath = Path.Combine(Path.GetTempPath(), SetupFileName); private static readonly SemVersion CurrentVersion = SemVersion.Parse(typeof(Updater).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); From 7db7e2d8b28a57180586c50e10a6c734bac699ca Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 27 Apr 2024 01:33:23 +0200 Subject: [PATCH 64/95] Remove the error button xD --- ShockOsc/Ui/Components/Tabs/DashboardTab.razor | 1 - 1 file changed, 1 deletion(-) diff --git a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor index fdb46be..51bb6c0 100644 --- a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor @@ -3,7 +3,6 @@

DashboardTab

-Yes @code { From 3ca8cb30a1ba18ae5ce8844cca0489ae92d99edb Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 27 Apr 2024 01:50:36 +0200 Subject: [PATCH 65/95] fix updater url, ffs --- ShockOsc/Services/Updater.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShockOsc/Services/Updater.cs b/ShockOsc/Services/Updater.cs index 1c47055..6346eaf 100644 --- a/ShockOsc/Services/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -12,7 +12,7 @@ namespace OpenShock.ShockOsc.Services; public sealed class Updater { - private const string GithubLatest = "https://api.github.com/repos/OpenShock/ShockOsc/releases/152715042"; + private const string GithubLatest = "https://api.github.com/repos/OpenShock/ShockOsc/releases/latest"; private const string SetupFileName = "ShockOSC_Setup.exe"; // OpenShock.ShockOsc.exe private static readonly HttpClient HttpClient = new(); From 1442da7787bd956537b87f5b76c85df9d6d77007 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 27 Apr 2024 20:19:01 +0200 Subject: [PATCH 66/95] Fix groups tab not refreshing shockers properly --- ShockOsc/Services/UnderscoreConfig.cs | 7 ++++--- ShockOsc/Ui/Components/Tabs/GroupsTab.razor | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/ShockOsc/Services/UnderscoreConfig.cs b/ShockOsc/Services/UnderscoreConfig.cs index d978113..4b7401f 100644 --- a/ShockOsc/Services/UnderscoreConfig.cs +++ b/ShockOsc/Services/UnderscoreConfig.cs @@ -85,7 +85,7 @@ public void HandleCommand(string parameterName, object?[] arguments) if (value is float durationFloat) { var currentDuration = MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f); - if (durationFloat == currentDuration) return; + if (Math.Abs(durationFloat - currentDuration) < 0.001) return; _configManager.Config.Behaviour.FixedDuration = MathUtils.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); ValidateSettings(); @@ -99,7 +99,7 @@ public void HandleCommand(string parameterName, object?[] arguments) if (value is float cooldownTimeFloat) { var currentCooldownTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f); - if (cooldownTimeFloat == currentCooldownTime) return; + if (Math.Abs(cooldownTimeFloat - currentCooldownTime) < 0.001) return; _configManager.Config.Behaviour.CooldownTime = MathUtils.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); ValidateSettings(); @@ -114,7 +114,7 @@ public void HandleCommand(string parameterName, object?[] arguments) if (value is float holdTimeFloat) { var currentHoldTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f); - if (holdTimeFloat == currentHoldTime) return; + if (Math.Abs(holdTimeFloat - currentHoldTime) < 0.001) return; _configManager.Config.Behaviour.HoldTime = MathUtils.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); ValidateSettings(); @@ -133,6 +133,7 @@ public void HandleCommand(string parameterName, object?[] arguments) if (KillSwitch == stateBool) return; KillSwitch = stateBool; + OnConfigUpdate?.Invoke(); // update Ui _logger.LogInformation("Paused state set to: {KillSwitch}", KillSwitch); } break; diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor index 088ccd6..d24168b 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Components/Tabs/GroupsTab.razor @@ -12,7 +12,15 @@ @code { - public Guid? Group { get; set; } = null; + private Guid? _group = null; + public Guid? Group + { + get => _group; + set + { + _group = value; + OnGroupSelect(); + } } public async Task AddGroup() { @@ -44,11 +52,11 @@ private Task OnGroupSettingsValueChange() => ConfigManager.SaveAsync(); - private async Task OnGroupSelect() + private void OnGroupSelect() { if (CurrentGroup != null) _selectedShockers = [..CurrentGroup.Shockers]; - await InvokeAsync(StateHasChanged); + InvokeAsync(StateHasChanged); } private async Task OnSelectedShockersUpdate() @@ -122,7 +130,7 @@ Add New Group Delete Group

- + @foreach (var group in ConfigManager.Config.Groups) { @group.Value.Name From 15e6cb3d45c2368c06919a976f43558b66652a97 Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 30 Apr 2024 23:57:03 +0200 Subject: [PATCH 67/95] Dashboard tab stuff --- .../Ui/Components/Tabs/DashboardTab.razor | 161 +++++++++++++++++- 1 file changed, 158 insertions(+), 3 deletions(-) diff --git a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor index 51bb6c0..a79a43a 100644 --- a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor +++ b/ShockOsc/Ui/Components/Tabs/DashboardTab.razor @@ -1,9 +1,164 @@ -@using OpenShock.ShockOsc.Services +@using System.Diagnostics +@using System.Reflection +@using Microsoft.AspNetCore.SignalR.Client +@using OpenShock.SDK.CSharp.Hub +@using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Services +@using Semver +@using OpenShock.ShockOsc.Ui.Components.Parts +@using OpenShock.ShockOsc.Ui.Utils @inject StatusHandler StatusHandler +@inject LiveControlManager LiveControlManager +@inject OpenShockApi Api +@inject OpenShockHubClient ApiHubClient +@inject ISnackbar Snackbar +@implements IDisposable -

DashboardTab

+
+ + Welcome to ShockOSC + Version @(Version.WithoutMetadata().ToString()) +
+
+ GitHub + OpenShock +
+ + Connection Status -@code { +
+ +
+ Hub + + + +
+ + @foreach (var device in Api.Devices) + { + if (LiveControlManager.LiveControlClients.TryGetValue(device.Id, out var client)) + { + + } + else + { +
+ @device.Name.Truncate(13) + + + +
+ } + } +
+
+ + @* *@ + @* Discord *@ + @* *@ + @* *@ + @* *@ + + + + + + + + Placeholder + Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna + + + + + Placeholder + Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna + + + + Placeholder + Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna + + + + Placeholder + Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna + + + + Placeholder + Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna + +
+ + + +@code { + private static readonly SemVersion Version = SemVersion.Parse(typeof(SideBar).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); + + private void OpenOpenShock() => OpenUrl("https://openshock.org"); + private void OpenGithub() => OpenUrl("https://github.com/OpenShock/ShockOsc"); + + private void OpenUrl(string url) + { + Snackbar.Add("Opened URL in browser", Severity.Info); + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + + protected override void OnInitialized() + { + StatusHandler.OnWebsocketStatusChanged += StatusHandlerOnWebsocketStatusChanged; + } + + private Task StatusHandlerOnWebsocketStatusChanged() + { + return InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + StatusHandler.OnWebsocketStatusChanged -= StatusHandlerOnWebsocketStatusChanged; + } + + private static Color GetConnectionStateColor(HubConnectionState state) => + state switch + { + HubConnectionState.Connected => Color.Success, + HubConnectionState.Reconnecting => Color.Warning, + HubConnectionState.Connecting => Color.Warning, + HubConnectionState.Disconnected => Color.Error, + _ => Color.Error + }; + } \ No newline at end of file From 880d2d376197a74c3fcd11a121b553e2f4ac41cd Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 1 May 2024 17:17:43 +0200 Subject: [PATCH 68/95] Window schenanigans --- ShockOsc/MauiProgram.cs | 42 +++++++++++++++++++++++----- ShockOsc/Utils/WindowUtils.cs | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 ShockOsc/Utils/WindowUtils.cs diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 0e6500c..325738b 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Diagnostics; +using System.Net; using Microsoft.Maui.LifecycleEvents; using MudBlazor.Services; using OpenShock.SDK.CSharp.Hub; @@ -10,11 +11,21 @@ using OpenShock.ShockOsc.Utils; using Serilog; using MauiApp = OpenShock.ShockOsc.Ui.MauiApp; +using Rect = OpenShock.ShockOsc.Utils.Rect; namespace OpenShock.ShockOsc; public static class MauiProgram { + private const int WS_CAPTION = 0x00C00000; + private const int WS_BORDER = 0x00800000; + private const int WS_SYSMENU = 0x00080000; + private const int WS_SIZEBOX = 0x00040000; + private const int WS_MINIMIZEBOX = 0x00020000; + private const int WS_MAXIMIZEBOX = 0x00010000; + private const int WS_THICKFRAME = 0x00040000; + + private static ShockOscConfig? _config; public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() @@ -87,13 +98,30 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() var id = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(handle); var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(id); - // if(appWindow.Presenter is OverlappedPresenter presenter) + // var style = WindowUtils.GetWindowLongPtrA(handle, (int)WindowLongFlags.GWL_STYLE); + // + // style &= ~WS_CAPTION; // Remove the title bar + // style |= WS_THICKFRAME; // Add thick frame for resizing + // + // WindowUtils.SetWindowLongPtrA(handle, (int)WindowLongFlags.GWL_STYLE, style); + // + // var reff = new Rect(); + // WindowUtils.AdjustWindowRectEx(ref reff, style, false, 0); + // reff.top = 6000; + // reff.left *= -1; + // + // var margins = new Margins // { - // presenter.IsMaximizable = false; - // presenter.IsMinimizable = false; - // presenter.IsResizable = true; - // presenter.SetBorderAndTitleBar(false, false); - // } + // cxLeftWidth = 0, + // cxRightWidth = 0, + // cyTopHeight = 0, + // cyBottomHeight = 0 + // }; + // + // WindowUtils.DwmExtendFrameIntoClientArea(handle, ref margins); + // + // WindowUtils.SetWindowPos(handle, 0, 0, 0, 0, 0x0040 | 0x0002 | 0x0001 | 0x0020); + // //When user execute the closing method, we can push a display alert. If user click Yes, close this application, if click the cancel, display alert will dismiss. appWindow.Closing += async (s, e) => diff --git a/ShockOsc/Utils/WindowUtils.cs b/ShockOsc/Utils/WindowUtils.cs new file mode 100644 index 0000000..9d9ef0b --- /dev/null +++ b/ShockOsc/Utils/WindowUtils.cs @@ -0,0 +1,52 @@ +using System.Drawing; +using System.Runtime.InteropServices; + +namespace OpenShock.ShockOsc.Utils; + +public static class WindowUtils +{ + [DllImport("user32.dll")] + public static extern IntPtr GetWindowLongPtrA(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + public static extern IntPtr SetWindowLongPtrA(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern bool AdjustWindowRectEx(ref Rect lpRect, IntPtr dwStyle, bool bMenu, uint dwExStyle); + + [DllImport("dwmapi.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset); + + [DllImport("user32.dll")] + public static extern bool SetWindowPos(IntPtr hWnd, int X, int Y, int cx, int cy, uint uFlags); +} + +public enum WindowLongFlags : int +{ + GWL_EXSTYLE = -20, + GWLP_HINSTANCE = -6, + GWLP_HWNDPARENT = -8, + GWL_ID = -12, + GWL_STYLE = -16, + GWL_USERDATA = -21, + GWL_WNDPROC = -4, + DWLP_USER = 0x8, + DWLP_MSGRESULT = 0x0, + DWLP_DLGPROC = 0x4 +} + +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct Rect { + public int left; + public int top; + public int right; + public int bottom; +} + +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct Margins { + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; +} \ No newline at end of file From 319ad0dcbb991fcc416d101088faeb93b60bd5e6 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 1 May 2024 19:53:17 +0200 Subject: [PATCH 69/95] Router rework --- ShockOsc/ShockOsc.csproj | 11 +++ ShockOsc/Ui/Components/MainLayout.razor | 70 ------------------- ShockOsc/Ui/Components/MudNavLinkFix.razor | 62 ---------------- ShockOsc/Ui/Main.razor | 13 ++-- .../Pages/Authentication/Authenticate.razor | 11 ++- .../Authentication/NotAuthedLayout.razor | 7 ++ .../Dash/Components}/StatePart.razor | 0 .../Dash/Components}/StatePart.razor.cs | 2 +- .../Dash}/Components/UpdateDialog.razor | 0 .../Dash}/Components/UpdateLogout.razor | 0 ShockOsc/Ui/Pages/Dash/DashLayout.razor | 37 ++++++++++ .../{Components => Pages/Dash}/SideBar.razor | 42 +++-------- .../Dash}/Tabs/AppSettingsTab.razor | 2 +- .../Dash}/Tabs/ChatboxTab.razor | 2 + .../Dash}/Tabs/ConfigTab.razor | 7 +- .../Dash}/Tabs/DashboardTab.razor | 4 +- .../Dash}/Tabs/DebugTab.razor | 2 +- .../Dash}/Tabs/GroupsTab.razor | 3 +- .../Dash}/Tabs/LogsTab.razor | 2 + .../Dash}/Tabs/ShockersTab.razor | 2 + .../Dash}/Tabs/TabType.cs | 0 .../Parts => Utils}/DebouncedSlider.razor | 0 .../Parts => Utils}/DebouncedSlider.razor.cs | 2 +- 23 files changed, 93 insertions(+), 188 deletions(-) delete mode 100644 ShockOsc/Ui/Components/MainLayout.razor delete mode 100644 ShockOsc/Ui/Components/MudNavLinkFix.razor create mode 100644 ShockOsc/Ui/Pages/Authentication/NotAuthedLayout.razor rename ShockOsc/Ui/{Components/Parts => Pages/Dash/Components}/StatePart.razor (100%) rename ShockOsc/Ui/{Components/Parts => Pages/Dash/Components}/StatePart.razor.cs (96%) rename ShockOsc/Ui/{ => Pages/Dash}/Components/UpdateDialog.razor (100%) rename ShockOsc/Ui/{ => Pages/Dash}/Components/UpdateLogout.razor (100%) create mode 100644 ShockOsc/Ui/Pages/Dash/DashLayout.razor rename ShockOsc/Ui/{Components => Pages/Dash}/SideBar.razor (66%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/AppSettingsTab.razor (98%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/ChatboxTab.razor (99%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/ConfigTab.razor (98%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/DashboardTab.razor (98%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/DebugTab.razor (97%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/GroupsTab.razor (99%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/LogsTab.razor (99%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/ShockersTab.razor (98%) rename ShockOsc/Ui/{Components => Pages/Dash}/Tabs/TabType.cs (100%) rename ShockOsc/Ui/{Components/Parts => Utils}/DebouncedSlider.razor (100%) rename ShockOsc/Ui/{Components/Parts => Utils}/DebouncedSlider.razor.cs (97%) diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index afe2407..a62b52a 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -83,4 +83,15 @@ + + + + + + + + + + + diff --git a/ShockOsc/Ui/Components/MainLayout.razor b/ShockOsc/Ui/Components/MainLayout.razor deleted file mode 100644 index 6976c49..0000000 --- a/ShockOsc/Ui/Components/MainLayout.razor +++ /dev/null @@ -1,70 +0,0 @@ -@using OpenShock.ShockOsc.Logging -@using OpenShock.ShockOsc.Ui.Components.Tabs -@using OpenShock.ShockOsc.Ui.Utils -@inject ISnackbar Snackbar -@inherits LayoutComponentBase - - -@page "/main" - - - - -
- - - - - -
- @switch (_currentTabType) - { - case TabType.Dashboard: - - break; - case TabType.Groups: - - break; - case TabType.Config: - - break; - case TabType.Chatbox: - - break; - case TabType.AppSettings: - - break; - case TabType.Shockers: - - break; - case TabType.Debug: - - break; - case TabType.Logs: - - break; - } -
-
-
- -@code { - - private TabType _currentTabType = TabType.Dashboard; - - private void MsgNoty(string msg, Severity severity) - { - Snackbar.Add(msg, severity); - } - - protected override void OnInitialized() - { - UiLogSink.NotificationAction = MsgNoty; - } - -} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/MudNavLinkFix.razor b/ShockOsc/Ui/Components/MudNavLinkFix.razor deleted file mode 100644 index 196e493..0000000 --- a/ShockOsc/Ui/Components/MudNavLinkFix.razor +++ /dev/null @@ -1,62 +0,0 @@ -@namespace OpenShock.ShockOsc.Ui.Components -@using MudBlazor.Utilities -@using MudBlazor.Interfaces -@inherits MudBaseSelectItem - -
- @{ -
- @if (!string.IsNullOrEmpty(Icon)) - { - - } - -
} -
- -@code { - - protected string Classname => - new CssBuilder("mud-nav-item") - .AddClass(Class) - .Build(); - - protected string LinkClassname => - new CssBuilder("mud-nav-link") - .AddClass($"mud-nav-link-disabled", Disabled) - .AddClass($"mud-ripple", !DisableRipple && !Disabled) - .AddClass(ActiveClass, IsActive) - .Build(); - - protected string IconClassname => - new CssBuilder("mud-nav-link-icon") - .AddClass($"mud-nav-link-icon-default", IconColor == Color.Default) - .Build(); - - /// - /// Icon to use if set. - /// - [Parameter] - [Category(CategoryTypes.NavMenu.Behavior)] - public string? Icon { get; set; } - - /// - /// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color. - /// - [Parameter] - [Category(CategoryTypes.NavMenu.Appearance)] - public Color IconColor { get; set; } = Color.Default; - - /// - /// User class names when active, separated by space. - /// - [Parameter] - [Category(CategoryTypes.ComponentBase.Common)] - public string ActiveClass { get; set; } = "active"; - - [Parameter] public bool IsActive { get; set; } -} \ No newline at end of file diff --git a/ShockOsc/Ui/Main.razor b/ShockOsc/Ui/Main.razor index c9001fa..1afeda0 100644 --- a/ShockOsc/Ui/Main.razor +++ b/ShockOsc/Ui/Main.razor @@ -1,5 +1,5 @@ -@using OpenShock.ShockOsc.Ui.Components -@using OpenShock.ShockOsc.Ui.ErrorHandling +@using OpenShock.ShockOsc.Ui.ErrorHandling +@using OpenShock.ShockOsc.Ui.Pages.Dash @using OpenShock.ShockOsc.Ui.Utils @@ -9,17 +9,14 @@ - + - -

Sorry, there's nothing at this address.

+ +

Sorry, there's nothing at this address. How did you get here?

- -

Page is loading...

-
diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index 1b8b9a4..ac24ab0 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -1,11 +1,8 @@ -@using OpenShock.ShockOsc.Ui.Utils @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config @using Microsoft.Extensions.Logging @using OpenShock.SDK.CSharp.Hub @using OpenShock.ShockOsc.Services -@inherits LayoutComponentBase - @inject ConfigManager ConfigManager @inject NavigationManager NavigationManager @inject BackendHubManager HubManager @@ -14,6 +11,8 @@ @inject ILogger Logger @inject LiveControlManager LiveControlManager @inject ISnackbar Snackbar +@layout NotAuthedLayout +@inherits LayoutComponentBase @page "/" @@ -59,7 +58,7 @@ Authenticated, Failed } - + private State _currentState = State.Login; private bool Loading { get; set; } @@ -93,11 +92,11 @@ await ApiClient.RefreshShockers(); await LiveControlManager.RefreshConnections(); - + _currentState = State.Authenticated; await InvokeAsync(StateHasChanged); - NavigationManager.NavigateTo("main"); + NavigationManager.NavigateTo("dash/dashboard"); } catch (Exception e) { diff --git a/ShockOsc/Ui/Pages/Authentication/NotAuthedLayout.razor b/ShockOsc/Ui/Pages/Authentication/NotAuthedLayout.razor new file mode 100644 index 0000000..708043f --- /dev/null +++ b/ShockOsc/Ui/Pages/Authentication/NotAuthedLayout.razor @@ -0,0 +1,7 @@ +@inherits LayoutComponentBase + +@Body + +@code { + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/Parts/StatePart.razor b/ShockOsc/Ui/Pages/Dash/Components/StatePart.razor similarity index 100% rename from ShockOsc/Ui/Components/Parts/StatePart.razor rename to ShockOsc/Ui/Pages/Dash/Components/StatePart.razor diff --git a/ShockOsc/Ui/Components/Parts/StatePart.razor.cs b/ShockOsc/Ui/Pages/Dash/Components/StatePart.razor.cs similarity index 96% rename from ShockOsc/Ui/Components/Parts/StatePart.razor.cs rename to ShockOsc/Ui/Pages/Dash/Components/StatePart.razor.cs index b0f62e2..633bfce 100644 --- a/ShockOsc/Ui/Components/Parts/StatePart.razor.cs +++ b/ShockOsc/Ui/Pages/Dash/Components/StatePart.razor.cs @@ -3,7 +3,7 @@ using OpenShock.SDK.CSharp.Live.LiveControlModels; using Color = MudBlazor.Color; -namespace OpenShock.ShockOsc.Ui.Components.Parts; +namespace OpenShock.ShockOsc.Ui.Pages.Dash.Components; public partial class StatePart : ComponentBase, IDisposable { diff --git a/ShockOsc/Ui/Components/UpdateDialog.razor b/ShockOsc/Ui/Pages/Dash/Components/UpdateDialog.razor similarity index 100% rename from ShockOsc/Ui/Components/UpdateDialog.razor rename to ShockOsc/Ui/Pages/Dash/Components/UpdateDialog.razor diff --git a/ShockOsc/Ui/Components/UpdateLogout.razor b/ShockOsc/Ui/Pages/Dash/Components/UpdateLogout.razor similarity index 100% rename from ShockOsc/Ui/Components/UpdateLogout.razor rename to ShockOsc/Ui/Pages/Dash/Components/UpdateLogout.razor diff --git a/ShockOsc/Ui/Pages/Dash/DashLayout.razor b/ShockOsc/Ui/Pages/Dash/DashLayout.razor new file mode 100644 index 0000000..0cfef81 --- /dev/null +++ b/ShockOsc/Ui/Pages/Dash/DashLayout.razor @@ -0,0 +1,37 @@ +@using OpenShock.ShockOsc.Logging +@inject ISnackbar Snackbar +@inherits LayoutComponentBase + + + + +
+ + + + + +
+ @Body +
+
+
+ +@code { + + private void MsgNoty(string msg, Severity severity) + { + Snackbar.Add(msg, severity); + } + + protected override void OnInitialized() + { + UiLogSink.NotificationAction = MsgNoty; + } + +} \ No newline at end of file diff --git a/ShockOsc/Ui/Components/SideBar.razor b/ShockOsc/Ui/Pages/Dash/SideBar.razor similarity index 66% rename from ShockOsc/Ui/Components/SideBar.razor rename to ShockOsc/Ui/Pages/Dash/SideBar.razor index deb116d..758e129 100644 --- a/ShockOsc/Ui/Components/SideBar.razor +++ b/ShockOsc/Ui/Pages/Dash/SideBar.razor @@ -3,9 +3,8 @@ @using OpenShock.SDK.CSharp.Hub @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Services -@using OpenShock.ShockOsc.Ui.Components.Tabs +@using OpenShock.ShockOsc.Ui.Pages.Dash.Components @using OpenShock.ShockOsc.Ui.Utils -@using OpenShock.ShockOsc.Ui.Components.Parts @using Semver @inject OpenShockApi Api @inject OpenShockHubClient ApiHubClient @@ -29,19 +28,19 @@ - Dashboard + Dashboard - Groups - Shockers + Groups + Shockers - Chatbox + Chatbox - Debug - Logs + Debug + Logs - Config - App Settings + Config + App Settings
@@ -87,33 +86,12 @@ @code { private static readonly SemVersion Version = SemVersion.Parse(typeof(SideBar).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); - - [Parameter] public EventCallback CurrentTabChanged { get; set; } - - private TabType _currentTab = TabType.Dashboard; - - [Parameter] - public TabType CurrentTab - { - get => _currentTab; - set - { - if (_currentTab == value) return; - CurrentTabChanged.InvokeAsync(value); - _currentTab = value; - } - } - + protected override void OnInitialized() { StatusHandler.OnWebsocketStatusChanged += () => InvokeAsync(StateHasChanged); } - private void NavigateTab(TabType tab) - { - CurrentTab = tab; - } - private static Color GetConnectionStateColor(HubConnectionState state) => state switch { diff --git a/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor similarity index 98% rename from ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor rename to ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor index 5b254b3..ff6de6d 100644 --- a/ShockOsc/Ui/Components/Tabs/AppSettingsTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor @@ -1,7 +1,7 @@ @using OpenShock.ShockOsc.Config @inject ConfigManager ConfigManager - +@page "/dash/appsettings" ShockOSC App diff --git a/ShockOsc/Ui/Components/Tabs/ChatboxTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor similarity index 99% rename from ShockOsc/Ui/Components/Tabs/ChatboxTab.razor rename to ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor index d9762ca..743a841 100644 --- a/ShockOsc/Ui/Components/Tabs/ChatboxTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor @@ -2,6 +2,8 @@ @using OpenShock.ShockOsc.Config @inject ConfigManager ConfigManager +@page "/dash/chatbox" + Chatbox Options diff --git a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor similarity index 98% rename from ShockOsc/Ui/Components/Tabs/ConfigTab.razor rename to ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor index ebbdb4c..129964c 100644 --- a/ShockOsc/Ui/Components/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor @@ -1,16 +1,15 @@ @using System.Globalization -@using System.Reactive.Subjects -@using OpenShock.ShockOsc.Utils -@using OpenShock.ShockOsc.Ui.Components.Parts -@using System.Reactive.Linq @using OpenShock.ShockOsc.Config @using OpenShock.ShockOsc.Services +@using OpenShock.ShockOsc.Ui.Utils @implements IDisposable @inject UnderscoreConfig underscoreConfig @inject ConfigManager ConfigManager @inject ShockOsc ShockOsc @inject UnderscoreConfig UnderscoreConfig +@page "/dash/config" + Global Shocker Options (_All Shocker) diff --git a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor similarity index 98% rename from ShockOsc/Ui/Components/Tabs/DashboardTab.razor rename to ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor index a79a43a..35ac764 100644 --- a/ShockOsc/Ui/Components/Tabs/DashboardTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor @@ -5,8 +5,8 @@ @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Services @using Semver -@using OpenShock.ShockOsc.Ui.Components.Parts @using OpenShock.ShockOsc.Ui.Utils +@using OpenShock.ShockOsc.Ui.Pages.Dash.Components @inject StatusHandler StatusHandler @inject LiveControlManager LiveControlManager @inject OpenShockApi Api @@ -14,6 +14,8 @@ @inject ISnackbar Snackbar @implements IDisposable +@page "/dash/dashboard" +
Welcome to ShockOSC diff --git a/ShockOsc/Ui/Components/Tabs/DebugTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor similarity index 97% rename from ShockOsc/Ui/Components/Tabs/DebugTab.razor rename to ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor index 05808d2..9626b2b 100644 --- a/ShockOsc/Ui/Components/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor @@ -1,9 +1,9 @@ @using OpenShock.ShockOsc.Services @using OpenShock.ShockOsc.Utils -@using Serilog.Sinks.SystemConsole.Themes @implements IAsyncDisposable @inject ShockOsc ShockOsc +@page "/dash/debug" Avatar ID: @ShockOsc.AvatarId diff --git a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/GroupsTab.razor similarity index 99% rename from ShockOsc/Ui/Components/Tabs/GroupsTab.razor rename to ShockOsc/Ui/Pages/Dash/Tabs/GroupsTab.razor index d24168b..597bd5f 100644 --- a/ShockOsc/Ui/Components/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/GroupsTab.razor @@ -2,13 +2,14 @@ @using System.Text.RegularExpressions @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config -@using OpenShock.ShockOsc.Ui.Components.Parts @using OpenShock.ShockOsc.Services +@using OpenShock.ShockOsc.Ui.Utils @using Group = OpenShock.ShockOsc.Config.Group @inject OpenShockApi OpenShockApi @inject ShockOsc ShockOsc @inject ConfigManager ConfigManager +@page "/dash/groups" @code { diff --git a/ShockOsc/Ui/Components/Tabs/LogsTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/LogsTab.razor similarity index 99% rename from ShockOsc/Ui/Components/Tabs/LogsTab.razor rename to ShockOsc/Ui/Pages/Dash/Tabs/LogsTab.razor index 90295c1..e7b1c71 100644 --- a/ShockOsc/Ui/Components/Tabs/LogsTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/LogsTab.razor @@ -4,6 +4,8 @@ @using OpenShock.ShockOsc.Ui.ErrorHandling @implements IDisposable +@page "/dash/logs" + Time diff --git a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/ShockersTab.razor similarity index 98% rename from ShockOsc/Ui/Components/Tabs/ShockersTab.razor rename to ShockOsc/Ui/Pages/Dash/Tabs/ShockersTab.razor index a710787..e507c38 100644 --- a/ShockOsc/Ui/Components/Tabs/ShockersTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/ShockersTab.razor @@ -3,6 +3,8 @@ @inject OpenShockApi OpenShockApi @inject ConfigManager ConfigManager +@page "/dash/shockers" + Refresh diff --git a/ShockOsc/Ui/Components/Tabs/TabType.cs b/ShockOsc/Ui/Pages/Dash/Tabs/TabType.cs similarity index 100% rename from ShockOsc/Ui/Components/Tabs/TabType.cs rename to ShockOsc/Ui/Pages/Dash/Tabs/TabType.cs diff --git a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor b/ShockOsc/Ui/Utils/DebouncedSlider.razor similarity index 100% rename from ShockOsc/Ui/Components/Parts/DebouncedSlider.razor rename to ShockOsc/Ui/Utils/DebouncedSlider.razor diff --git a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs b/ShockOsc/Ui/Utils/DebouncedSlider.razor.cs similarity index 97% rename from ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs rename to ShockOsc/Ui/Utils/DebouncedSlider.razor.cs index 38b6205..678454f 100644 --- a/ShockOsc/Ui/Components/Parts/DebouncedSlider.razor.cs +++ b/ShockOsc/Ui/Utils/DebouncedSlider.razor.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Components; using Size = MudBlazor.Size; -namespace OpenShock.ShockOsc.Ui.Components.Parts; +namespace OpenShock.ShockOsc.Ui.Utils; public partial class DebouncedSlider : ComponentBase { From 649cda5a9e587ae8379ef5d9ae473af07e5f341f Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 4 May 2024 00:39:25 +0200 Subject: [PATCH 70/95] Add search to parameter list --- ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor | 26 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor index 9626b2b..a958834 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor @@ -12,20 +12,37 @@ OSC Parameters +
@if (_showAllAvatarParams) { - @foreach (var param in ShockOsc.AllAvatarParams) + if (ShockOsc.AllAvatarParams.Count > 0) { - + @foreach (var param in ShockOsc.AllAvatarParams + .Where(x => x.Key.Contains(_search, StringComparison.InvariantCultureIgnoreCase))) + { + + } + } + else + { + No parameters available } } else { - @foreach (var param in ShockOsc.ShockOscParams) + if (ShockOsc.ShockOscParams.Count > 0) + { + @foreach (var param in ShockOsc.ShockOscParams + .Where(x => x.Key.Contains(_search, StringComparison.InvariantCultureIgnoreCase))) + { + + } + } + else { - + No parameters available } } @@ -33,6 +50,7 @@ @code { private bool _showAllAvatarParams = false; + private string _search = ""; private void OnParamsChange(bool shockOscParam) { From b6910179a1fdb4567472b36671844d90678b8a96 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 4 May 2024 00:39:37 +0200 Subject: [PATCH 71/95] Add _All/Paused --- ShockOsc/Services/UnderscoreConfig.cs | 92 +++++++++++++++++++-------- 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/ShockOsc/Services/UnderscoreConfig.cs b/ShockOsc/Services/UnderscoreConfig.cs index 4b7401f..c959924 100644 --- a/ShockOsc/Services/UnderscoreConfig.cs +++ b/ShockOsc/Services/UnderscoreConfig.cs @@ -12,21 +12,22 @@ public sealed class UnderscoreConfig private readonly ShockOscData _dataLayer; public event Action? OnConfigUpdate; - - public UnderscoreConfig(ILogger logger, OscClient oscClient, ConfigManager configManager, ShockOscData dataLayer) + + public UnderscoreConfig(ILogger logger, OscClient oscClient, ConfigManager configManager, + ShockOscData dataLayer) { _logger = logger; _oscClient = oscClient; _configManager = configManager; _dataLayer = dataLayer; } - + public bool KillSwitch { get; set; } = false; - + public void HandleCommand(string parameterName, object?[] arguments) { var settingName = parameterName[8..]; - + var settingPath = settingName.Split('/'); if (settingPath.Length > 2) { @@ -38,14 +39,23 @@ public void HandleCommand(string parameterName, object?[] arguments) { var groupName = settingPath[0]; var action = settingPath[1]; - if (!_dataLayer.ProgramGroups.Any(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)) && groupName != "_All") + if (!_dataLayer.ProgramGroups.Any(x => + x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)) && groupName != "_All") { _logger.LogWarning("Unknown shocker {Shocker}", groupName); _logger.LogDebug("Param: {Param}", action); return; } - - var group = _dataLayer.ProgramGroups.First(x => x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)); + + if (groupName == "_All") + { + } + else + { + } + + var group = _dataLayer.ProgramGroups.First(x => + x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)); var value = arguments.ElementAtOrDefault(0); // TODO: support groups @@ -56,58 +66,69 @@ public void HandleCommand(string parameterName, object?[] arguments) // 0..100% if (value is float minIntensityFloat) { - var currentMinIntensity = MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f); + var currentMinIntensity = + MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f); if (minIntensityFloat == currentMinIntensity) return; - _configManager.Config.Behaviour.IntensityRange.Min = MathUtils.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); + _configManager.Config.Behaviour.IntensityRange.Min = + MathUtils.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); ValidateSettings(); _configManager.Save(); OnConfigUpdate?.Invoke(); // update Ui } + break; case "MaxIntensity": // 0..100% if (value is float maxIntensityFloat) { - var currentMaxIntensity = MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f); + var currentMaxIntensity = + MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f); if (maxIntensityFloat == currentMaxIntensity) return; - _configManager.Config.Behaviour.IntensityRange.Max = MathUtils.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); + _configManager.Config.Behaviour.IntensityRange.Max = + MathUtils.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); ValidateSettings(); _configManager.Save(); OnConfigUpdate?.Invoke(); // update Ui } + break; - + case "Duration": // 0..10sec if (value is float durationFloat) { - var currentDuration = MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f); + var currentDuration = + MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f); if (Math.Abs(durationFloat - currentDuration) < 0.001) return; - _configManager.Config.Behaviour.FixedDuration = MathUtils.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); + _configManager.Config.Behaviour.FixedDuration = + MathUtils.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); ValidateSettings(); _configManager.Save(); OnConfigUpdate?.Invoke(); // update Ui } + break; case "CooldownTime": // 0..100sec if (value is float cooldownTimeFloat) { - var currentCooldownTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f); + var currentCooldownTime = + MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f); if (Math.Abs(cooldownTimeFloat - currentCooldownTime) < 0.001) return; - _configManager.Config.Behaviour.CooldownTime = MathUtils.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); + _configManager.Config.Behaviour.CooldownTime = + MathUtils.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); ValidateSettings(); _configManager.Save(); OnConfigUpdate?.Invoke(); // update Ui } - break; + break; case "HoldTime": // 0..1sec @@ -116,11 +137,25 @@ public void HandleCommand(string parameterName, object?[] arguments) var currentHoldTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f); if (Math.Abs(holdTimeFloat - currentHoldTime) < 0.001) return; - _configManager.Config.Behaviour.HoldTime = MathUtils.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); + _configManager.Config.Behaviour.HoldTime = + MathUtils.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); ValidateSettings(); _configManager.Save(); OnConfigUpdate?.Invoke(); // update Ui } + + break; + + case "Paused": + if (value is bool stateBool) + { + if (KillSwitch == stateBool) return; + + KillSwitch = stateBool; + OnConfigUpdate?.Invoke(); // update Ui + _logger.LogInformation("Paused state set to: {KillSwitch}", KillSwitch); + } + break; } } @@ -136,6 +171,7 @@ public void HandleCommand(string parameterName, object?[] arguments) OnConfigUpdate?.Invoke(); // update Ui _logger.LogInformation("Paused state set to: {KillSwitch}", KillSwitch); } + break; } } @@ -144,17 +180,21 @@ private void ValidateSettings() { var intensityRange = _configManager.Config.Behaviour.IntensityRange; if (intensityRange.Min > intensityRange.Max) intensityRange.Max = intensityRange.Min; - if(intensityRange.Max < intensityRange.Min) intensityRange.Min = intensityRange.Max; - + if (intensityRange.Max < intensityRange.Min) intensityRange.Min = intensityRange.Max; } public async Task SendUpdateForAll() { await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/Paused", KillSwitch); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f)); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f)); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f)); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f)); - await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", + MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", + MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", + MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", + MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", + MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f)); } } \ No newline at end of file From d864310dc7c738f4d1401a6965cb6c72b3d40d63 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 4 May 2024 00:54:51 +0200 Subject: [PATCH 72/95] Refactor --- ShockOsc/Services/UnderscoreConfig.cs | 259 +++++++++++++------------- 1 file changed, 133 insertions(+), 126 deletions(-) diff --git a/ShockOsc/Services/UnderscoreConfig.cs b/ShockOsc/Services/UnderscoreConfig.cs index c959924..3c0a191 100644 --- a/ShockOsc/Services/UnderscoreConfig.cs +++ b/ShockOsc/Services/UnderscoreConfig.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using OpenShock.ShockOsc.Config; +using OpenShock.ShockOsc.Models; using OpenShock.ShockOsc.Utils; namespace OpenShock.ShockOsc.Services; @@ -29,141 +30,142 @@ public void HandleCommand(string parameterName, object?[] arguments) var settingName = parameterName[8..]; var settingPath = settingName.Split('/'); - if (settingPath.Length > 2) + if (settingPath.Length is > 2 or <= 0) { - _logger.LogWarning("Invalid setting path: {SettingPath}", settingPath); + _logger.LogWarning("Invalid setting path: {SettingName}", settingName); return; } - if (settingPath.Length == 2) + var value = arguments.ElementAtOrDefault(0); + + #region Legacy + + // Legacy Paused setting + if (settingPath.Length == 1) { - var groupName = settingPath[0]; - var action = settingPath[1]; - if (!_dataLayer.ProgramGroups.Any(x => - x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)) && groupName != "_All") - { - _logger.LogWarning("Unknown shocker {Shocker}", groupName); - _logger.LogDebug("Param: {Param}", action); - return; - } - - if (groupName == "_All") - { - } - else - { - } - - var group = _dataLayer.ProgramGroups.First(x => - x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)); - var value = arguments.ElementAtOrDefault(0); - - // TODO: support groups - - switch (action) - { - case "MinIntensity": - // 0..100% - if (value is float minIntensityFloat) - { - var currentMinIntensity = - MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f); - if (minIntensityFloat == currentMinIntensity) return; - - _configManager.Config.Behaviour.IntensityRange.Min = - MathUtils.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); - ValidateSettings(); - _configManager.Save(); - OnConfigUpdate?.Invoke(); // update Ui - } - - break; - - case "MaxIntensity": - // 0..100% - if (value is float maxIntensityFloat) - { - var currentMaxIntensity = - MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f); - if (maxIntensityFloat == currentMaxIntensity) return; - - _configManager.Config.Behaviour.IntensityRange.Max = - MathUtils.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); - ValidateSettings(); - _configManager.Save(); - OnConfigUpdate?.Invoke(); // update Ui - } - - break; - - case "Duration": - // 0..10sec - if (value is float durationFloat) - { - var currentDuration = - MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f); - if (Math.Abs(durationFloat - currentDuration) < 0.001) return; - - _configManager.Config.Behaviour.FixedDuration = - MathUtils.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); - ValidateSettings(); - _configManager.Save(); - OnConfigUpdate?.Invoke(); // update Ui - } - - break; - - case "CooldownTime": - // 0..100sec - if (value is float cooldownTimeFloat) - { - var currentCooldownTime = - MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f); - if (Math.Abs(cooldownTimeFloat - currentCooldownTime) < 0.001) return; - - _configManager.Config.Behaviour.CooldownTime = - MathUtils.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); - ValidateSettings(); - _configManager.Save(); - OnConfigUpdate?.Invoke(); // update Ui - } - - break; - - case "HoldTime": - // 0..1sec - if (value is float holdTimeFloat) - { - var currentHoldTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f); - if (Math.Abs(holdTimeFloat - currentHoldTime) < 0.001) return; - - _configManager.Config.Behaviour.HoldTime = - MathUtils.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); - ValidateSettings(); - _configManager.Save(); - OnConfigUpdate?.Invoke(); // update Ui - } - - break; - - case "Paused": - if (value is bool stateBool) - { - if (KillSwitch == stateBool) return; - - KillSwitch = stateBool; - OnConfigUpdate?.Invoke(); // update Ui - _logger.LogInformation("Paused state set to: {KillSwitch}", KillSwitch); - } - - break; - } + if (settingName != "Paused" || value is not bool stateBool || KillSwitch == stateBool) return; + + KillSwitch = stateBool; + OnConfigUpdate?.Invoke(); // update Ui + _logger.LogInformation("Paused state set to: {KillSwitch}", KillSwitch); + return; + } + + #endregion + + var groupName = settingPath[0]; + var action = settingPath[1]; + if (!_dataLayer.ProgramGroups.Any(x => + x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)) && groupName != "_All") + { + _logger.LogWarning("Unknown shocker {Shocker}", groupName); + _logger.LogDebug("Param: {Param}", action); + return; + } + + // Handle global config commands + if (groupName == "_All") + { + HandleGlobalConfigCommand(action, value); + return; } - switch (settingName) + var group = _dataLayer.ProgramGroups.First(x => + x.Value.Name.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)); + + HandleGroupConfigCommand(group.Value, action, value); + } + + private void HandleGlobalConfigCommand(string action, object? value) + { + switch (action) { + case "MinIntensity": + // 0..100% + if (value is float minIntensityFloat) + { + var currentMinIntensity = + MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f); + if (Math.Abs(minIntensityFloat - currentMinIntensity) < 0.001) return; + + _configManager.Config.Behaviour.IntensityRange.Min = + MathUtils.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); + ValidateSettings(); + _configManager.Save(); + OnConfigUpdate?.Invoke(); // update Ui + } + + break; + + case "MaxIntensity": + // 0..100% + if (value is float maxIntensityFloat) + { + var currentMaxIntensity = + MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f); + if (Math.Abs(maxIntensityFloat - currentMaxIntensity) < 0.001) return; + + _configManager.Config.Behaviour.IntensityRange.Max = + MathUtils.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); + ValidateSettings(); + _configManager.Save(); + OnConfigUpdate?.Invoke(); // update Ui + } + + break; + + case "Duration": + // 0..10sec + if (value is float durationFloat) + { + var currentDuration = + MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f); + if (Math.Abs(durationFloat - currentDuration) < 0.001) return; + + _configManager.Config.Behaviour.FixedDuration = + MathUtils.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); + ValidateSettings(); + _configManager.Save(); + OnConfigUpdate?.Invoke(); // update Ui + } + + break; + + case "CooldownTime": + // 0..100sec + if (value is float cooldownTimeFloat) + { + var currentCooldownTime = + MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f); + if (Math.Abs(cooldownTimeFloat - currentCooldownTime) < 0.001) return; + + _configManager.Config.Behaviour.CooldownTime = + MathUtils.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); + ValidateSettings(); + _configManager.Save(); + OnConfigUpdate?.Invoke(); // update Ui + } + + break; + + case "HoldTime": + // 0..1sec + if (value is float holdTimeFloat) + { + var currentHoldTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f); + if (Math.Abs(holdTimeFloat - currentHoldTime) < 0.001) return; + + _configManager.Config.Behaviour.HoldTime = + MathUtils.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); + ValidateSettings(); + _configManager.Save(); + OnConfigUpdate?.Invoke(); // update Ui + } + + break; + case "Paused": - if (arguments.ElementAtOrDefault(0) is bool stateBool) + if (value is bool stateBool) { if (KillSwitch == stateBool) return; @@ -176,6 +178,11 @@ public void HandleCommand(string parameterName, object?[] arguments) } } + private void HandleGroupConfigCommand(ProgramGroup group, string action, object? value) + { + // TODO: support groups + } + private void ValidateSettings() { var intensityRange = _configManager.Config.Behaviour.IntensityRange; From 9ee6edac3433f54bf44faa6a762a99687d84ff98 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 4 May 2024 01:13:56 +0200 Subject: [PATCH 73/95] Update ci-windows.yml --- .github/workflows/ci-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 807b3ba..4b50e17 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -11,7 +11,7 @@ on: workflow_call: workflow_dispatch: -name: ci-windowsl +name: ci-windows env: DOTNET_VERSION: 8.0.x @@ -80,4 +80,4 @@ jobs: name: ShockOsc_Setup path: Installer/ShockOsc_Setup.exe retention-days: 7 - if-no-files-found: error \ No newline at end of file + if-no-files-found: error From 8e9ae1fe503f3c7e6a4ee2a6e9d30ef0407dc9a4 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 4 May 2024 01:49:44 +0200 Subject: [PATCH 74/95] Paused fix, and send updates to game on config update --- ShockOsc/Services/UnderscoreConfig.cs | 1 + ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor | 40 ++++++++++++--------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/ShockOsc/Services/UnderscoreConfig.cs b/ShockOsc/Services/UnderscoreConfig.cs index 3c0a191..8fda63a 100644 --- a/ShockOsc/Services/UnderscoreConfig.cs +++ b/ShockOsc/Services/UnderscoreConfig.cs @@ -193,6 +193,7 @@ private void ValidateSettings() public async Task SendUpdateForAll() { await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/Paused", KillSwitch); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Paused", KillSwitch); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor index 129964c..86f094e 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor @@ -2,6 +2,7 @@ @using OpenShock.ShockOsc.Config @using OpenShock.ShockOsc.Services @using OpenShock.ShockOsc.Ui.Utils +@using OpenShock.ShockOsc.Utils @implements IDisposable @inject UnderscoreConfig underscoreConfig @inject ConfigManager ConfigManager @@ -14,7 +15,7 @@ Global Shocker Options (_All Shocker) - + @foreach (BehaviourConf.BoneHeldAction boneHeldAction in Enum.GetValues(typeof(BehaviourConf.BoneHeldAction))) { @boneHeldAction @@ -24,10 +25,10 @@

- - + +
- + @@ -37,7 +38,7 @@ @if (!ConfigManager.Config.Behaviour.RandomIntensity) { + OnSaveAction="_ => OnSettingsValueChange()"> Intensity: @ConfigManager.Config.Behaviour.FixedIntensity% } @@ -45,19 +46,19 @@ { + OnSaveAction="_ => OnSettingsValueChange()"> Min Intensity: @ConfigManager.Config.Behaviour.IntensityRange.Min% + OnSaveAction="_ => OnSettingsValueChange()"> Max Intensity: @ConfigManager.Config.Behaviour.IntensityRange.Max% } - + @@ -66,7 +67,7 @@ @if (!ConfigManager.Config.Behaviour.RandomDuration) { Duration: @MathF.Round(ConfigManager.Config.Behaviour.FixedDuration / 1000f, 1).ToString(CultureInfo.InvariantCulture)s @@ -75,14 +76,14 @@ { Min Duration: @MathF.Round(ConfigManager.Config.Behaviour.DurationRange.Min / 1000f, 1).ToString(CultureInfo.InvariantCulture)s Max Duration: @MathF.Round(ConfigManager.Config.Behaviour.DurationRange.Max / 1000f, 1).ToString(CultureInfo.InvariantCulture)s @@ -95,9 +96,9 @@ Game Options - - - + + + @code { @@ -114,11 +115,14 @@ set => ConfigManager.Config.Behaviour.RandomDuration = value == "Random Duration"; } + private void OnSettingsValueChange() + { + OsTask.Run(OnSettingsValueChangeAsync); + } - private async Task OnSettingsValueChange() + private Task OnSettingsValueChangeAsync() { - await ConfigManager.SaveAsync(); - await underscoreConfig.SendUpdateForAll(); + return Task.WhenAll(ConfigManager.SaveAsync(), underscoreConfig.SendUpdateForAll()); } protected override void OnInitialized() @@ -135,5 +139,7 @@ { UnderscoreConfig.OnConfigUpdate -= OnConfigUpdate; } + + } \ No newline at end of file From a9c2b06227f56fc14a8c3466a35cbc48c20b829a Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 4 May 2024 01:55:47 +0200 Subject: [PATCH 75/95] run on any branch --- .github/workflows/ci-windows.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 4b50e17..9e65de0 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -1,12 +1,10 @@ on: push: branches: - - master - - develop + - '*' pull_request: branches: - - master - - develop + - '*' types: [opened, reopened, synchronize] workflow_call: workflow_dispatch: From ccfba62762a0a0d41321fed0e9ee97e09641f36f Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 4 May 2024 03:30:01 +0200 Subject: [PATCH 76/95] debounce input for search --- ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor index a958834..f02fd19 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor @@ -12,7 +12,7 @@ OSC Parameters - +
@if (_showAllAvatarParams) From b6138c66243f1a1c31f3c388cee7b4a46424a4d6 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 5 May 2024 00:26:18 +0200 Subject: [PATCH 77/95] Download channel selection --- ShockOsc/Config/AppConfig.cs | 13 +- ShockOsc/Config/ShockOscConfig.cs | 1 - ShockOsc/Models/GithubReleaseResponse.cs | 10 ++ ShockOsc/Services/ShockOsc.cs | 2 + ShockOsc/Services/Updater.cs | 150 ++++++++++++++---- ShockOsc/ShockOsc.csproj | 2 +- .../Pages/Dash/Components/UpdateDialog.razor | 72 ++++++--- .../Pages/Dash/Components/UpdateLogout.razor | 46 ++++-- .../Ui/Pages/Dash/Tabs/AppSettingsTab.razor | 56 ++++++- ShockOsc/Utils/StreamExtensions.cs | 24 +++ 10 files changed, 303 insertions(+), 73 deletions(-) create mode 100644 ShockOsc/Utils/StreamExtensions.cs diff --git a/ShockOsc/Config/AppConfig.cs b/ShockOsc/Config/AppConfig.cs index 267d166..bcf3867 100644 --- a/ShockOsc/Config/AppConfig.cs +++ b/ShockOsc/Config/AppConfig.cs @@ -1,6 +1,17 @@ -namespace OpenShock.ShockOsc.Config; +using Semver; + +namespace OpenShock.ShockOsc.Config; public sealed class AppConfig { public bool CloseToTray { get; set; } = true; + + public UpdateChannel UpdateChannel { get; set; } = UpdateChannel.Release; + public SemVersion? LastIgnoredVersion { get; set; } = null; +} + +public enum UpdateChannel +{ + Release, + PreRelease } \ No newline at end of file diff --git a/ShockOsc/Config/ShockOscConfig.cs b/ShockOsc/Config/ShockOscConfig.cs index 71dcdbe..02c4eae 100644 --- a/ShockOsc/Config/ShockOscConfig.cs +++ b/ShockOsc/Config/ShockOscConfig.cs @@ -9,7 +9,6 @@ public sealed class ShockOscConfig public OpenShockConf OpenShock { get; set; } = new(); public ChatboxConf Chatbox { get; set; } = new(); public IDictionary Groups { get; set; } = new Dictionary(); - public SemVersion? LastIgnoredVersion { get; set; } = null; public AppConfig App { get; set; } = new(); } \ No newline at end of file diff --git a/ShockOsc/Models/GithubReleaseResponse.cs b/ShockOsc/Models/GithubReleaseResponse.cs index fb73f07..11dac2d 100644 --- a/ShockOsc/Models/GithubReleaseResponse.cs +++ b/ShockOsc/Models/GithubReleaseResponse.cs @@ -6,6 +6,16 @@ public class GithubReleaseResponse { [JsonPropertyName("tag_name")] public required string TagName { get; set; } + + [JsonPropertyName("id")] + public required ulong Id { get; set; } + + [JsonPropertyName("draft")] + public required bool Draft { get; set; } + + [JsonPropertyName("prerelease")] + public required bool Prerelease { get; set; } + [JsonPropertyName("assets")] public required ICollection Assets { get; set; } diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index 066f770..59a13eb 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -129,6 +129,8 @@ public async Task FoundVrcClient(IPEndPoint? oscClient) _logger.LogInformation("Ready"); OsTask.Run(_underscoreConfig.SendUpdateForAll); + + _oscClient.SendChatboxMessage($"{_configManager.Config.Chatbox.Prefix} Game Connected"); } public async Task OnAvatarChange(Dictionary parameters, string avatarId) diff --git a/ShockOsc/Services/Updater.cs b/ShockOsc/Services/Updater.cs index 6346eaf..2906ead 100644 --- a/ShockOsc/Services/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -5,31 +5,35 @@ using OpenShock.SDK.CSharp.Updatables; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; -using OpenShock.ShockOsc.Ui.Utils; +using OpenShock.ShockOsc.Utils; using Semver; namespace OpenShock.ShockOsc.Services; public sealed class Updater { - private const string GithubLatest = "https://api.github.com/repos/OpenShock/ShockOsc/releases/latest"; + private const string GithubReleasesUrl = "https://api.github.com/repos/OpenShock/ShockOsc/releases"; + private const string GithubLatest = $"{GithubReleasesUrl}/latest"; private const string SetupFileName = "ShockOSC_Setup.exe"; // OpenShock.ShockOsc.exe private static readonly HttpClient HttpClient = new(); private readonly string _setupFilePath = Path.Combine(Path.GetTempPath(), SetupFileName); - private static readonly SemVersion CurrentVersion = SemVersion.Parse(typeof(Updater).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); + private static readonly SemVersion CurrentVersion = SemVersion.Parse( + typeof(Updater).Assembly.GetCustomAttribute()!.InformationalVersion, + SemVersionStyles.Strict); - private Uri? LatestDownloadUrl { get; set; } + private Uri? ReleaseDownloadUrl { get; set; } private readonly ILogger _logger; private readonly ConfigManager _configManager; - + public UpdatableVariable CheckingForUpdate { get; } = new(false); public UpdatableVariable UpdateAvailable { get; } = new(false); public bool IsPostponed { get; private set; } public SemVersion? LatestVersion { get; private set; } + public UpdatableVariable DownloadProgress { get; } = new(0); public Updater(ILogger logger, ConfigManager configManager) @@ -53,44 +57,41 @@ private static bool TryDeleteFile(string fileName) } } - private async Task<(SemVersion, GithubReleaseResponse.Asset)?> GetLatestRelease() + private async Task<(SemVersion, GithubReleaseResponse.Asset)?> GetRelease() { - _logger.LogInformation("Checking GitHub for updates..."); + var updateChannel = _configManager.Config.App.UpdateChannel; + _logger.LogInformation("Checking GitHub for updates on channel {UpdateChannel}", updateChannel); try { - var res = await HttpClient.GetAsync(GithubLatest); - if (!res.IsSuccessStatusCode) + var release = updateChannel switch { - _logger.LogWarning("Failed to get latest version information from GitHub. {StatusCode}", - res.StatusCode); - return null; - } + UpdateChannel.Release => await GetLatestRelease(), + UpdateChannel.PreRelease => await GetPreRelease(), + _ => null + }; - var json = - await JsonSerializer.DeserializeAsync(await res.Content.ReadAsStreamAsync()); - if (json == null) + if (release == null) { - _logger.LogWarning("Could not deserialize json"); + _logger.LogError("Failed to get latest version information from GitHub"); return null; } - var tagName = json.TagName; - - if (!SemVersion.TryParse(tagName, SemVersionStyles.AllowV, out var version)) + if (!SemVersion.TryParse(release.TagName, SemVersionStyles.AllowV, out var version)) { - _logger.LogWarning("Failed to parse version. Value: {Version}", json.TagName); + _logger.LogWarning("Failed to parse version. Value: {Version}", release.TagName); return null; } - var asset = json.Assets.FirstOrDefault(x => x.Name.Equals(SetupFileName, StringComparison.InvariantCultureIgnoreCase)); + var asset = release.Assets.FirstOrDefault(x => + x.Name.Equals(SetupFileName, StringComparison.InvariantCultureIgnoreCase)); if (asset == null) { _logger.LogWarning("Could not find asset with {@SetupName}. Assets found: {@Assets}", SetupFileName, - json.Assets); + release.Assets); return null; } - + return (version, asset); } catch (Exception e) @@ -100,12 +101,88 @@ private static bool TryDeleteFile(string fileName) } } + private async Task GetPreRelease() + { + using var res = await HttpClient.GetAsync(GithubReleasesUrl); + if (!res.IsSuccessStatusCode) + { + _logger.LogWarning("Failed to get latest version information from GitHub. {StatusCode}", + res.StatusCode); + return null; + } + + var json = + await JsonSerializer.DeserializeAsync>( + await res.Content.ReadAsStreamAsync()); + if (json == null) + { + _logger.LogWarning("Could not deserialize json"); + return null; + } + + var listOfValid = new List<(GithubReleaseResponse, SemVersion)>(); + foreach (var release in json.Where(x => x.Prerelease)) + { + var tagName = release.TagName; + if (!SemVersion.TryParse(tagName, SemVersionStyles.AllowV, out var version)) + { + _logger.LogDebug("Failed to parse version. Value: {Version}", tagName); + continue; + } + + listOfValid.Add((release, version)); + } + + var newestPreRelease = listOfValid.OrderByDescending(x => x.Item2).FirstOrDefault(); + + return newestPreRelease.Item1; + } + + private async Task GetLatestRelease() + { + using var res = await HttpClient.GetAsync(GithubLatest); + if (!res.IsSuccessStatusCode) + { + _logger.LogWarning("Failed to get latest version information from GitHub. {StatusCode}", + res.StatusCode); + return null; + } + + var json = + await JsonSerializer.DeserializeAsync(await res.Content.ReadAsStreamAsync()); + if (json == null) + { + _logger.LogWarning("Could not deserialize json"); + return null; + } + + return json; + } + + private readonly SemaphoreSlim _updateLock = new(1, 1); + public async Task CheckUpdate() + { + await _updateLock.WaitAsync(); + + try + { + CheckingForUpdate.Value = true; + await CheckUpdateInternal(); + } + finally + { + _updateLock.Release(); + CheckingForUpdate.Value = false; + } + } + + private async Task CheckUpdateInternal() { IsPostponed = false; UpdateAvailable.Value = false; - - var latestVersion = await GetLatestRelease(); + + var latestVersion = await GetRelease(); if (latestVersion == null) { UpdateAvailable.Value = false; @@ -123,8 +200,9 @@ public async Task CheckUpdate() UpdateAvailable.Value = true; LatestVersion = latestVersion.Value.Item1; - LatestDownloadUrl = latestVersion.Value.Item2.BrowserDownloadUrl; - if (_configManager.Config.LastIgnoredVersion != null && _configManager.Config.LastIgnoredVersion.ComparePrecedenceTo(latestVersion.Value.Item1) >= 0) + ReleaseDownloadUrl = latestVersion.Value.Item2.BrowserDownloadUrl; + if (_configManager.Config.App.LastIgnoredVersion != null && + _configManager.Config.App.LastIgnoredVersion.ComparePrecedenceTo(latestVersion.Value.Item1) >= 0) { _logger.LogInformation( "ShockOsc is not up to date. Skipping update due to previous postpone"); @@ -140,7 +218,9 @@ public async Task CheckUpdate() public async Task DoUpdate() { _logger.LogInformation("Starting update..."); - if (LatestVersion == null || LatestDownloadUrl == null) + + DownloadProgress.Value = 0; + if (LatestVersion == null || ReleaseDownloadUrl == null) { _logger.LogError("LatestVersion or LatestDownloadUrl is null. Cannot update"); return; @@ -150,12 +230,20 @@ public async Task DoUpdate() _logger.LogDebug("Downloading new release..."); var sp = Stopwatch.StartNew(); - await using (var stream = await HttpClient.GetStreamAsync(LatestDownloadUrl)) + var download = await HttpClient.GetAsync(ReleaseDownloadUrl, HttpCompletionOption.ResponseHeadersRead); + var totalBytes = download.Content.Headers.ContentLength ?? 1; + + await using (var stream = await download.Content.ReadAsStreamAsync()) { await using var fStream = new FileStream(_setupFilePath, FileMode.OpenOrCreate); - await stream.CopyToAsync(fStream); + var relativeProgress = new Progress(downloadedBytes => + DownloadProgress.Value = ((double)downloadedBytes / totalBytes) * 100); + + // Use extension method to report progress while downloading + await stream.CopyToAsync(fStream, 81920, relativeProgress); } + DownloadProgress.Value = 100; _logger.LogDebug("Downloaded file within {TimeTook}ms", sp.ElapsedMilliseconds); _logger.LogInformation("Download complete, now restarting to newer application in one second"); await Task.Delay(1000); diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index a62b52a..2e85344 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -16,7 +16,7 @@ OpenShock.ShockOsc OpenShock 2.0.0 - 2.0.0-rc.2 + 2.0.0-rc.3 Resources\openshock-icon.ico true ShockOsc diff --git a/ShockOsc/Ui/Pages/Dash/Components/UpdateDialog.razor b/ShockOsc/Ui/Pages/Dash/Components/UpdateDialog.razor index 85aede1..ae1acd5 100644 --- a/ShockOsc/Ui/Pages/Dash/Components/UpdateDialog.razor +++ b/ShockOsc/Ui/Pages/Dash/Components/UpdateDialog.razor @@ -3,20 +3,31 @@ @using OpenShock.ShockOsc.Utils @inject ConfigManager ConfigManager @inject Updater Updater +@implements IDisposable @code { - [CascadingParameter] - MudDialogInstance MudDialog { get; set; } + [CascadingParameter] MudDialogInstance MudDialog { get; set; } private bool _isDownloading = false; + protected override void OnInitialized() + { + Updater.DownloadProgress.OnValueChanged += OnDownloadProgressChanged; + base.OnInitialized(); + } + + private void OnDownloadProgressChanged(double progress) + { + InvokeAsync(StateHasChanged); + } + private async Task Skip() { - ConfigManager.Config.LastIgnoredVersion = Updater.LatestVersion; + ConfigManager.Config.App.LastIgnoredVersion = Updater.LatestVersion; await ConfigManager.SaveAsync(); MudDialog.Close(DialogResult.Ok(true)); } - + private void Dismiss() { MudDialog.Close(DialogResult.Ok(true)); @@ -27,31 +38,46 @@ _isDownloading = true; OsTask.Run(Updater.DoUpdate); } + + public void Dispose() + { + Updater.DownloadProgress.OnValueChanged -= OnDownloadProgressChanged; + } + } +
+ @if (!_isDownloading) + { + Update Available + @Updater.LatestVersion?.ToString() + +
+ A new version of ShockOSC is available.Would you like to update ? + } + else + { + Downloading Update +
+ Please wait while the update is downloaded. +
+ + + @if (Math.Abs(Updater.DownloadProgress.Value - 100) < 0.0001) + { + Update downloaded successfully. Restarting in one second... + } + } +
+
+ @if (!_isDownloading) { - Update Available -
- A new version of ShockOSC is available. Would you like to update? - } - else - { - Downloading Update -
- Please wait while the update is downloaded. -
- + Dismiss + Skip + Update } - - - @if (!_isDownloading) - { - Dismiss - Skip - Update - }
\ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Dash/Components/UpdateLogout.razor b/ShockOsc/Ui/Pages/Dash/Components/UpdateLogout.razor index d663f99..f1c262c 100644 --- a/ShockOsc/Ui/Pages/Dash/Components/UpdateLogout.razor +++ b/ShockOsc/Ui/Pages/Dash/Components/UpdateLogout.razor @@ -5,6 +5,7 @@ @inject ConfigManager ConfigManager @inject Updater Updater @inject NavigationManager NavigationManager +@implements IDisposable @code { private readonly DialogOptions _dialogOptions = new() { NoHeader = true, DisableBackdropClick = true }; @@ -16,23 +17,43 @@ protected override async Task OnInitializedAsync() { - Updater.UpdateAvailable.OnValueChanged += v => - { - InvokeAsync(StateHasChanged); - if (v && !Updater.IsPostponed) OpenUpdateDialog(); - }; + Updater.UpdateAvailable.OnValueChanged += UpdateAvailableOnValueChanged; + Updater.CheckingForUpdate.OnValueChanged += CheckingForUpdateOnValueChanged; if (Updater.UpdateAvailable.Value && !Updater.IsPostponed) OpenUpdateDialog(); - + } + + private void UpdateAvailableOnValueChanged(bool v) + { + InvokeAsync(StateHasChanged); + if (v && !Updater.IsPostponed) OpenUpdateDialog(); + } + + private void CheckingForUpdateOnValueChanged(bool v) + { + InvokeAsync(StateHasChanged); } private async Task Logout() { ConfigManager.Config.OpenShock.Token = string.Empty; await ConfigManager.SaveAsync(); - + NavigationManager.NavigateTo("/"); } + + public void Dispose() + { + Updater.CheckingForUpdate.OnValueChanged -= CheckingForUpdateOnValueChanged; + Updater.UpdateAvailable.OnValueChanged -= UpdateAvailableOnValueChanged; + } + + private string GetUpdateTooltip() + { + if(Updater.CheckingForUpdate.Value) return "Checking for updates..."; + return Updater.UpdateAvailable.Value ? "Update available!" : "You are up-to-date!"; + } + }
@@ -44,9 +65,16 @@ - + - + @if (Updater.CheckingForUpdate.Value) + { + + } + else + { + + } diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor index ff6de6d..26eae0a 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor @@ -1,5 +1,8 @@ @using OpenShock.ShockOsc.Config +@using OpenShock.ShockOsc.Services @inject ConfigManager ConfigManager +@inject Updater Updater +@implements IDisposable @page "/dash/appsettings" @@ -7,8 +10,26 @@ ShockOSC App - - +
+ + + +
+ + @foreach (UpdateChannel channel in Enum.GetValues(typeof(UpdateChannel))) + { + @channel + } + +
+ + @if (Updater.CheckingForUpdate.Value) + { + + + + } +
@@ -18,17 +39,38 @@ @if (!ConfigManager.Config.Osc.OscQuery) { -
- - +
+ + }
@code { - - + + protected override void OnInitialized() + { + Updater.CheckingForUpdate.OnValueChanged += OnCheckingForUpdateChange; + } + + private void OnCheckingForUpdateChange(bool value) + { + InvokeAsync(StateHasChanged); + } + private async Task OnSettingsValueChange() { await ConfigManager.SaveAsync(); } + + private async Task UpdateChannelChanged() + { + await OnSettingsValueChange(); + await Updater.CheckUpdate(); + } + + public void Dispose() + { + Updater.CheckingForUpdate.OnValueChanged -= OnCheckingForUpdateChange; + } + } \ No newline at end of file diff --git a/ShockOsc/Utils/StreamExtensions.cs b/ShockOsc/Utils/StreamExtensions.cs new file mode 100644 index 0000000..d901069 --- /dev/null +++ b/ShockOsc/Utils/StreamExtensions.cs @@ -0,0 +1,24 @@ +namespace OpenShock.ShockOsc.Utils; + +public static class StreamExtensions +{ + public static async Task CopyToAsync(this Stream? source, Stream? destination, uint bufferSize, IProgress? progress = null, CancellationToken cancellationToken = default) { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + throw new ArgumentException("Has to be readable", nameof(source)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new ArgumentException("Has to be writable", nameof(destination)); + + var buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) { + await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report(totalBytesRead); + } + } +} \ No newline at end of file From 59b72b4930526613898703ba5933a2521d31bac4 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 5 May 2024 00:27:36 +0200 Subject: [PATCH 78/95] refactor --- ShockOsc/Services/Updater.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ShockOsc/Services/Updater.cs b/ShockOsc/Services/Updater.cs index 2906ead..b1064db 100644 --- a/ShockOsc/Services/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -133,9 +133,9 @@ await JsonSerializer.DeserializeAsync>( listOfValid.Add((release, version)); } - var newestPreRelease = listOfValid.OrderByDescending(x => x.Item2).FirstOrDefault(); + var newestPreRelease = listOfValid.OrderByDescending(x => x.Item2); - return newestPreRelease.Item1; + return newestPreRelease.FirstOrDefault().Item1; } private async Task GetLatestRelease() From 5d3c2cb00db85fc75f1778d00a6f5f7d94d995b7 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 5 May 2024 01:34:50 +0200 Subject: [PATCH 79/95] add widget bot, add level --- ShockOsc/Backend/BackendHubManager.cs | 2 +- ShockOsc/Config/ChatboxConf.cs | 1 + ShockOsc/Config/OscConf.cs | 1 - ShockOsc/Services/OscClient.cs | 1 + ShockOsc/Services/ShockOsc.cs | 2 +- ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor | 2 +- .../Ui/Pages/Dash/Tabs/DashboardTab.razor | 20 ++----------------- ShockOsc/Ui/Pages/Dash/Tabs/LogsTab.razor | 2 ++ 8 files changed, 9 insertions(+), 22 deletions(-) diff --git a/ShockOsc/Backend/BackendHubManager.cs b/ShockOsc/Backend/BackendHubManager.cs index d69dce6..ecc77ba 100644 --- a/ShockOsc/Backend/BackendHubManager.cs +++ b/ShockOsc/Backend/BackendHubManager.cs @@ -141,7 +141,7 @@ private async Task RemoteActivateShocker(ControlLogSender sender, ControlLog log log.Type, log.Shocker.Name, log.Intensity, inSeconds, sender.CustomName, sender.Name); var template = _configManager.Config.Chatbox.Types[log.Type]; - if (_configManager.Config.Osc.Chatbox && + if (_configManager.Config.Chatbox.Enabled && _configManager.Config.Chatbox.DisplayRemoteControl && template.Enabled) { // Chatbox message remote diff --git a/ShockOsc/Config/ChatboxConf.cs b/ShockOsc/Config/ChatboxConf.cs index adb21cd..37895c4 100644 --- a/ShockOsc/Config/ChatboxConf.cs +++ b/ShockOsc/Config/ChatboxConf.cs @@ -4,6 +4,7 @@ namespace OpenShock.ShockOsc.Config; public sealed class ChatboxConf { + public bool Enabled { get; set; } = true; public string Prefix { get; set; } = "[ShockOsc] "; public bool DisplayRemoteControl { get; set; } = true; diff --git a/ShockOsc/Config/OscConf.cs b/ShockOsc/Config/OscConf.cs index 895ff80..dfec3ae 100644 --- a/ShockOsc/Config/OscConf.cs +++ b/ShockOsc/Config/OscConf.cs @@ -2,7 +2,6 @@ public sealed class OscConf { - public bool Chatbox { get; set; } = true; public bool Hoscy { get; set; } = false; public ushort HoscySendPort { get; set; } = 9001; public bool QuestSupport { get; set; } = false; diff --git a/ShockOsc/Services/OscClient.cs b/ShockOsc/Services/OscClient.cs index 9cc5138..b3bc666 100644 --- a/ShockOsc/Services/OscClient.cs +++ b/ShockOsc/Services/OscClient.cs @@ -49,6 +49,7 @@ public ValueTask SendGameMessage(string address, params object?[]?arguments) public ValueTask SendChatboxMessage(string message) { + if(!_configManager.Config.Chatbox.Enabled) return ValueTask.CompletedTask; if (_configManager.Config.Osc.Hoscy) return _hoscySenderChannel.Writer.WriteAsync(new OscMessage("/hoscy/message", message)); return _gameSenderChannel.Writer.WriteAsync(new OscMessage("/chatbox/input", message, true)); diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index 59a13eb..ed79b0e 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -414,7 +414,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i await _backendHubManager.ControlGroup(programGroup.Id, duration, intensity, ControlType.Shock, exclusive); - if (!_configManager.Config.Osc.Chatbox) return; + if (!_configManager.Config.Chatbox.Enabled) return; // Chatbox message local var dat = new { diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor index 743a841..a7321cc 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor @@ -7,7 +7,7 @@ Chatbox Options - +

diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor index 35ac764..ff67ea5 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor @@ -57,14 +57,8 @@
- @* *@ - @* Discord *@ - @* *@ - @* *@ - @* *@ - - + @@ -74,16 +68,6 @@
- - Placeholder - Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna - - - - Placeholder - Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna - - Placeholder Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna @@ -118,7 +102,7 @@ } .dashboard-box .discord { - grid-area: 1 / 3 / 3 / 3; + grid-area: 2 / 2 / 4 / 4; } .dashboard-box .item:hover { diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/LogsTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/LogsTab.razor index e7b1c71..56457bf 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/LogsTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/LogsTab.razor @@ -9,11 +9,13 @@ Time + Level Source Message @context.Time.ToString("HH:mm:ss") + @context.Level.ToString() @context.SourceContextShort @context.Message.TruncateAtChar(120) From 864ac73f991b47c06329a8991c3031db5603a1de Mon Sep 17 00:00:00 2001 From: Natsumi Date: Sun, 5 May 2024 23:44:53 +1200 Subject: [PATCH 80/95] Fix intensity float and force unmute --- ShockOsc/Services/OscHandler.cs | 12 ++++++------ ShockOsc/Services/ShockOsc.cs | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ShockOsc/Services/OscHandler.cs b/ShockOsc/Services/OscHandler.cs index 2bbcd05..be004ef 100644 --- a/ShockOsc/Services/OscHandler.cs +++ b/ShockOsc/Services/OscHandler.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.OscChangeTracker; using OpenShock.ShockOsc.Utils; @@ -63,7 +63,7 @@ await Task.Delay(50) await _oscClient.SendGameMessage("/input/Voice", false) .ConfigureAwait(false); } - + /// /// Send parameter updates to osc /// @@ -84,8 +84,8 @@ public async Task SendParams() if (!isActiveOrOnCooldown && shocker.LastIntensity > 0) shocker.LastIntensity = 0; + var intensity = MathUtils.ClampFloat(shocker.LastIntensity / 100f); var onCoolDown = !isActive && isActiveOrOnCooldown; - var cooldownPercentage = 0f; if (onCoolDown) cooldownPercentage = MathUtils.ClampFloat(1 - @@ -97,12 +97,12 @@ public async Task SendParams() await shocker.ParamActive.SetValue(isActive); await shocker.ParamCooldown.SetValue(onCoolDown); await shocker.ParamCooldownPercentage.SetValue(cooldownPercentage); - await shocker.ParamIntensity.SetValue(MathUtils.ClampFloat(shocker.LastIntensity)); + await shocker.ParamIntensity.SetValue(intensity); if (isActive) anyActive = true; if (onCoolDown) anyCooldown = true; - anyCooldownPercentage = Math.Max(anyCooldownPercentage, cooldownPercentage); - anyIntensity = Math.Max(anyIntensity, MathUtils.ClampFloat(shocker.LastIntensity)); + anyCooldownPercentage = MathF.Max(anyCooldownPercentage, cooldownPercentage); + anyIntensity = MathF.Max(anyIntensity, intensity); } await _paramAnyActive.SetValue(anyActive); diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index ed79b0e..edfe912 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -29,7 +29,6 @@ public sealed class ShockOsc private bool _oscServerActive; private bool _isAfk; - private bool _isMuted; public string AvatarId = string.Empty; private readonly Random Random = new(); @@ -244,8 +243,8 @@ private async Task ReceiveLogic() _logger.LogDebug("Afk: {State}", _isAfk); return; case "/avatar/parameters/MuteSelf": - _isMuted = received.Arguments.ElementAtOrDefault(0) is true; - _logger.LogDebug("Muted: {State}", _isMuted); + _dataLayer.IsMuted = received.Arguments.ElementAtOrDefault(0) is true; + _logger.LogDebug("Muted: {State}", _dataLayer.IsMuted); return; } @@ -400,7 +399,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i { programGroup.LastExecuted = DateTime.UtcNow; programGroup.LastDuration = duration; - var intensityPercentage = Math.Round(MathUtils.ClampFloat(intensity) * 100f); + var intensityPercentage = MathF.Round(MathUtils.ClampFloat(intensity) * 100f); programGroup.LastIntensity = intensity; _oscHandler.ForceUnmute(); From a17d4c4df9cc9cc84c0e1374688b14bc51bc5ae2 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 May 2024 04:47:42 +0200 Subject: [PATCH 81/95] Remove unused envs, and get msbuild for linux to work --- ShockOsc/Config/AppConfig.cs | 1 + ShockOsc/MauiProgram.cs | 18 +- .../Platforms/Android/AndroidManifest.xml | 6 - ShockOsc/Platforms/Android/MainActivity.cs | 12 -- ShockOsc/Platforms/Android/MainApplication.cs | 15 -- .../Android/Resources/values/colors.xml | 6 - ShockOsc/Platforms/Linux/LinuxApp.cs | 10 ++ ShockOsc/Platforms/MacCatalyst/AppDelegate.cs | 9 - ShockOsc/Platforms/MacCatalyst/Info.plist | 30 ---- ShockOsc/Platforms/MacCatalyst/Program.cs | 15 -- ShockOsc/Platforms/Tizen/Main.cs | 16 -- ShockOsc/Platforms/Tizen/tizen-manifest.xml | 15 -- ShockOsc/Platforms/Windows/App.xaml.cs | 5 +- .../Platforms/Windows/WindowsTrayService.cs | 8 +- ShockOsc/Platforms/iOS/AppDelegate.cs | 9 - ShockOsc/Platforms/iOS/Info.plist | 32 ---- ShockOsc/Platforms/iOS/Program.cs | 15 -- ShockOsc/ShockOsc.csproj | 124 +++++++------- ShockOsc/Ui/ErrorHandling/CodeBlock.razor | 2 + ShockOsc/Ui/MainPage.xaml.cs | 6 +- ShockOsc/Ui/MauiApp.xaml.cs | 6 +- .../Pages/Dash/Components/DiscordInvite.razor | 154 ++++++++++++++++++ .../Dash/Components/DiscordInvite.razor.cs | 106 ++++++++++++ ShockOsc/Ui/Pages/Dash/SideBar.razor | 2 +- .../Ui/Pages/Dash/Tabs/AppSettingsTab.razor | 1 + .../Ui/Pages/Dash/Tabs/DashboardTab.razor | 48 +++--- ShockOsc/Utils/UiUtils.cs | 11 +- ShockOsc/wwwroot/app.css | 15 ++ ShockOsc/wwwroot/images/IconSlowSpin.svg | 21 +++ 29 files changed, 437 insertions(+), 281 deletions(-) delete mode 100644 ShockOsc/Platforms/Android/AndroidManifest.xml delete mode 100644 ShockOsc/Platforms/Android/MainActivity.cs delete mode 100644 ShockOsc/Platforms/Android/MainApplication.cs delete mode 100644 ShockOsc/Platforms/Android/Resources/values/colors.xml create mode 100644 ShockOsc/Platforms/Linux/LinuxApp.cs delete mode 100644 ShockOsc/Platforms/MacCatalyst/AppDelegate.cs delete mode 100644 ShockOsc/Platforms/MacCatalyst/Info.plist delete mode 100644 ShockOsc/Platforms/MacCatalyst/Program.cs delete mode 100644 ShockOsc/Platforms/Tizen/Main.cs delete mode 100644 ShockOsc/Platforms/Tizen/tizen-manifest.xml delete mode 100644 ShockOsc/Platforms/iOS/AppDelegate.cs delete mode 100644 ShockOsc/Platforms/iOS/Info.plist delete mode 100644 ShockOsc/Platforms/iOS/Program.cs create mode 100644 ShockOsc/Ui/Pages/Dash/Components/DiscordInvite.razor create mode 100644 ShockOsc/Ui/Pages/Dash/Components/DiscordInvite.razor.cs create mode 100644 ShockOsc/wwwroot/images/IconSlowSpin.svg diff --git a/ShockOsc/Config/AppConfig.cs b/ShockOsc/Config/AppConfig.cs index bcf3867..152f37b 100644 --- a/ShockOsc/Config/AppConfig.cs +++ b/ShockOsc/Config/AppConfig.cs @@ -5,6 +5,7 @@ namespace OpenShock.ShockOsc.Config; public sealed class AppConfig { public bool CloseToTray { get; set; } = true; + public bool DiscordPreview { get; set; } = true; public UpdateChannel UpdateChannel { get; set; } = UpdateChannel.Release; public SemVersion? LastIgnoredVersion { get; set; } = null; diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 325738b..1c4857b 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +#if WINDOWS +using System.Diagnostics; using System.Net; using Microsoft.Maui.LifecycleEvents; using MudBlazor.Services; @@ -13,6 +14,14 @@ using MauiApp = OpenShock.ShockOsc.Ui.MauiApp; using Rect = OpenShock.ShockOsc.Utils.Rect; + +using Microsoft.UI; + + +#if M + +#endif + namespace OpenShock.ShockOsc; public static class MauiProgram @@ -61,6 +70,8 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() builder.Services.AddSerilog(Log.Logger); + builder.Services.AddMemoryCache(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -86,7 +97,6 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); -#if WINDOWS builder.ConfigureLifecycleEvents(lifecycleBuilder => { lifecycleBuilder.AddWindows(windowsLifecycleBuilder => @@ -148,7 +158,6 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() builder.Services.AddSingleton(); -#endif builder.Services.AddSingleton(); @@ -181,4 +190,5 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() return app; } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/Android/AndroidManifest.xml b/ShockOsc/Platforms/Android/AndroidManifest.xml deleted file mode 100644 index dbf9e7e..0000000 --- a/ShockOsc/Platforms/Android/AndroidManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ShockOsc/Platforms/Android/MainActivity.cs b/ShockOsc/Platforms/Android/MainActivity.cs deleted file mode 100644 index 552f15e..0000000 --- a/ShockOsc/Platforms/Android/MainActivity.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Android.App; -using Android.Content.PM; -using Android.OS; - -namespace MauiApp1; - -[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, - ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | - ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] -public class MainActivity : MauiAppCompatActivity -{ -} \ No newline at end of file diff --git a/ShockOsc/Platforms/Android/MainApplication.cs b/ShockOsc/Platforms/Android/MainApplication.cs deleted file mode 100644 index 347b31d..0000000 --- a/ShockOsc/Platforms/Android/MainApplication.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Android.App; -using Android.Runtime; - -namespace ShockOsc; - -[Application] -public class MainApplication : MauiApplication -{ - public MainApplication(IntPtr handle, JniHandleOwnership ownership) - : base(handle, ownership) - { - } - - protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); -} \ No newline at end of file diff --git a/ShockOsc/Platforms/Android/Resources/values/colors.xml b/ShockOsc/Platforms/Android/Resources/values/colors.xml deleted file mode 100644 index c04d749..0000000 --- a/ShockOsc/Platforms/Android/Resources/values/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #512BD4 - #2B0B98 - #2B0B98 - \ No newline at end of file diff --git a/ShockOsc/Platforms/Linux/LinuxApp.cs b/ShockOsc/Platforms/Linux/LinuxApp.cs new file mode 100644 index 0000000..a609cec --- /dev/null +++ b/ShockOsc/Platforms/Linux/LinuxApp.cs @@ -0,0 +1,10 @@ +namespace OpenShock.ShockOsc.Linux; +#if !WINDOWS +public static class LinuxApp +{ + public static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/MacCatalyst/AppDelegate.cs b/ShockOsc/Platforms/MacCatalyst/AppDelegate.cs deleted file mode 100644 index 3fca2bd..0000000 --- a/ShockOsc/Platforms/MacCatalyst/AppDelegate.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Foundation; - -namespace ShockOsc; - -[Register("AppDelegate")] -public class AppDelegate : MauiUIApplicationDelegate -{ - protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); -} \ No newline at end of file diff --git a/ShockOsc/Platforms/MacCatalyst/Info.plist b/ShockOsc/Platforms/MacCatalyst/Info.plist deleted file mode 100644 index 403ce9c..0000000 --- a/ShockOsc/Platforms/MacCatalyst/Info.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - UIDeviceFamily - - 1 - 2 - - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - XSAppIconAssets - Assets.xcassets/appicon.appiconset - - diff --git a/ShockOsc/Platforms/MacCatalyst/Program.cs b/ShockOsc/Platforms/MacCatalyst/Program.cs deleted file mode 100644 index efe47bf..0000000 --- a/ShockOsc/Platforms/MacCatalyst/Program.cs +++ /dev/null @@ -1,15 +0,0 @@ -using ObjCRuntime; -using UIKit; - -namespace ShockOsc; - -public class Program -{ - // This is the main entry point of the application. - static void Main(string[] args) - { - // if you want to use a different Application Delegate class from "AppDelegate" - // you can specify it here. - UIApplication.Main(args, null, typeof(AppDelegate)); - } -} \ No newline at end of file diff --git a/ShockOsc/Platforms/Tizen/Main.cs b/ShockOsc/Platforms/Tizen/Main.cs deleted file mode 100644 index 07507af..0000000 --- a/ShockOsc/Platforms/Tizen/Main.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Microsoft.Maui; -using Microsoft.Maui.Hosting; - -namespace MauiApp1; - -class Program : MauiApplication -{ - protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); - - static void Main(string[] args) - { - var app = new Program(); - app.Run(args); - } -} diff --git a/ShockOsc/Platforms/Tizen/tizen-manifest.xml b/ShockOsc/Platforms/Tizen/tizen-manifest.xml deleted file mode 100644 index 0835032..0000000 --- a/ShockOsc/Platforms/Tizen/tizen-manifest.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - maui-appicon-placeholder - - - - - http://tizen.org/privilege/internet - - - - \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/App.xaml.cs b/ShockOsc/Platforms/Windows/App.xaml.cs index 3edcae2..d144b91 100644 --- a/ShockOsc/Platforms/Windows/App.xaml.cs +++ b/ShockOsc/Platforms/Windows/App.xaml.cs @@ -2,7 +2,7 @@ // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. - +#if WINDOWS namespace OpenShock.ShockOsc.Platforms.Windows; /// @@ -20,4 +20,5 @@ public App() } protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index 27c8a93..861a924 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -1,4 +1,6 @@ -using System.Drawing; +#if WINDOWS + +using System.Drawing; using System.Windows.Forms; using Microsoft.UI; using Microsoft.UI.Windowing; @@ -85,4 +87,6 @@ private static void OnQuitClick(object? sender, EventArgs eventArgs) { Application.Current?.Quit(); } -} \ No newline at end of file +} + +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/iOS/AppDelegate.cs b/ShockOsc/Platforms/iOS/AppDelegate.cs deleted file mode 100644 index 3fca2bd..0000000 --- a/ShockOsc/Platforms/iOS/AppDelegate.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Foundation; - -namespace ShockOsc; - -[Register("AppDelegate")] -public class AppDelegate : MauiUIApplicationDelegate -{ - protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); -} \ No newline at end of file diff --git a/ShockOsc/Platforms/iOS/Info.plist b/ShockOsc/Platforms/iOS/Info.plist deleted file mode 100644 index ecb7f71..0000000 --- a/ShockOsc/Platforms/iOS/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - LSRequiresIPhoneOS - - UIDeviceFamily - - 1 - 2 - - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - XSAppIconAssets - Assets.xcassets/appicon.appiconset - - diff --git a/ShockOsc/Platforms/iOS/Program.cs b/ShockOsc/Platforms/iOS/Program.cs deleted file mode 100644 index efe47bf..0000000 --- a/ShockOsc/Platforms/iOS/Program.cs +++ /dev/null @@ -1,15 +0,0 @@ -using ObjCRuntime; -using UIKit; - -namespace ShockOsc; - -public class Program -{ - // This is the main entry point of the application. - static void Main(string[] args) - { - // if you want to use a different Application Delegate class from "AppDelegate" - // you can specify it here. - UIApplication.Main(args, null, typeof(AppDelegate)); - } -} \ No newline at end of file diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 2e85344..4d533c8 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -2,16 +2,15 @@ Exe - + net8.0 $(TargetFrameworks);net8.0-windows10.0.19041.0 - true + true enable false enable ShockOsc - org.openshock.shockosc - 2C147618-324E-4C37-B4B6-C50C8A9BD5ED + OpenShock.ShockOsc OpenShock.ShockOsc OpenShock @@ -21,14 +20,6 @@ true ShockOsc - 14.2 - 14.0 - 24.0 - 10.0.17763.0 - 10.0.17763.0 - 6.5 - - None Resources\Icon512.png en @@ -36,62 +27,75 @@ false + + true + 10.0.17763.0 + 10.0.17763.0 + None + + + + OpenShock.ShockOsc.Linux.LinuxApp + + - - - - - - - - - + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - + + + - - - - - - - - + + + + + + + + - + diff --git a/ShockOsc/Ui/ErrorHandling/CodeBlock.razor b/ShockOsc/Ui/ErrorHandling/CodeBlock.razor index 1bbc256..6972acf 100644 --- a/ShockOsc/Ui/ErrorHandling/CodeBlock.razor +++ b/ShockOsc/Ui/ErrorHandling/CodeBlock.razor @@ -38,7 +38,9 @@ private async Task CopyTextToClipboard() { +#if WINDOWS await Clipboard.SetTextAsync(Value); +#endif Snackbar.Add("Copied to clipboard", Severity.Success); } diff --git a/ShockOsc/Ui/MainPage.xaml.cs b/ShockOsc/Ui/MainPage.xaml.cs index d33e113..741aff4 100644 --- a/ShockOsc/Ui/MainPage.xaml.cs +++ b/ShockOsc/Ui/MainPage.xaml.cs @@ -1,4 +1,5 @@ -namespace OpenShock.ShockOsc.Ui; +#if WINDOWS +namespace OpenShock.ShockOsc.Ui; public partial class MainPage : ContentPage { @@ -6,4 +7,5 @@ public MainPage() { InitializeComponent(); } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/ShockOsc/Ui/MauiApp.xaml.cs b/ShockOsc/Ui/MauiApp.xaml.cs index 0a2bf3e..186d706 100644 --- a/ShockOsc/Ui/MauiApp.xaml.cs +++ b/ShockOsc/Ui/MauiApp.xaml.cs @@ -1,4 +1,5 @@ -namespace OpenShock.ShockOsc.Ui; +#if WINDOWS +namespace OpenShock.ShockOsc.Ui; public partial class MauiApp { @@ -17,4 +18,5 @@ protected override Window CreateWindow(IActivationState? activationState) return window; } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Dash/Components/DiscordInvite.razor b/ShockOsc/Ui/Pages/Dash/Components/DiscordInvite.razor new file mode 100644 index 0000000..5b490d5 --- /dev/null +++ b/ShockOsc/Ui/Pages/Dash/Components/DiscordInvite.razor @@ -0,0 +1,154 @@ +@namespace OpenShock.ShockOsc.Ui.Pages.Dash.Components + +
+ @if(!_loading) { + @if(_invite != null) { +
+ Splash +
+

Join the @(_invite.Guild.Name) Discord Server!

+ +
+
+ Discord Server Icon +
+

@(_invite.Guild.Name)

+
+
+ + @(_invite.ApproximatePresenceCount) Online +
+
+ + @(_invite.ApproximateMemberCount) Members +
+
+ +
+
+ Join +
+ + } else { +

Failed to load invite

+ } + + } else { +
+ +
+ + +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ Join +
+ } +
+ + \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Dash/Components/DiscordInvite.razor.cs b/ShockOsc/Ui/Pages/Dash/Components/DiscordInvite.razor.cs new file mode 100644 index 0000000..97f828c --- /dev/null +++ b/ShockOsc/Ui/Pages/Dash/Components/DiscordInvite.razor.cs @@ -0,0 +1,106 @@ +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using OpenShock.ShockOsc.Utils; + +namespace OpenShock.ShockOsc.Ui.Pages.Dash.Components; + +public partial class DiscordInvite : ComponentBase +{ + [Inject] + private IMemoryCache MemoryCache { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + private static HttpClient _httpClient = new HttpClient(); + + static DiscordInvite() + { + _httpClient.BaseAddress = new Uri("https://discord.com/api/v10/"); + } + + [Parameter] + public required string InviteCode { get; set; } + + private DiscordInviteResponse? _invite; + private bool _loading = true; + + protected override async Task OnInitializedAsync() + { + try + { + _invite = await GetInvite(); + } catch (Exception e) + { + Logger.LogError(e, "Failed to load discord invite {InviteCode}", InviteCode); + } + + _loading = false; + } + + public void JoinDiscord() + { + UiUtils.OpenUrl($"https://discord.gg/{InviteCode}"); + } + + private async Task GetInvite() + { + Logger.LogDebug("Loading discord invite {InviteCode}", InviteCode); +#pragma warning disable CS8600 + if (MemoryCache.TryGetValue($"discord_invite_{InviteCode}", out DiscordInviteResponse cachedInvite)) + { + Logger.LogDebug("Returning cached invite {InviteCode}", InviteCode); + return cachedInvite; + } +#pragma warning restore CS8600 + + Logger.LogDebug("Fetching invite {InviteCode}", InviteCode); + var response = await _httpClient.GetAsync($"invites/{InviteCode}?with_counts=true"); + if (!response.IsSuccessStatusCode) + { + Logger.LogError("Failed to fetch invite {InviteCode}. API returned {StatusCode}", InviteCode, response.StatusCode); + return null; + } + + var invite = await response.Content.ReadFromJsonAsync(); + + MemoryCache.Set($"discord_invite_{InviteCode}", invite, TimeSpan.FromMinutes(5)); + + Logger.LogDebug("Fetched invite {InviteCode}", InviteCode); + + return invite; + } +} + +public sealed class DiscordInviteResponse +{ + [JsonPropertyName("approximate_member_count")] + public required uint ApproximateMemberCount { get; set; } + + [JsonPropertyName("approximate_presence_count")] + public required uint ApproximatePresenceCount { get; set; } + + [JsonPropertyName("guild")] + public required DiscordGuild Guild { get; set; } +} + +public sealed class DiscordGuild +{ + [JsonPropertyName("id")] + public required ulong Id { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("icon")] + public required string Icon { get; set; } + + [JsonPropertyName("splash")] + public required string Splash { get; set; } + + [JsonPropertyName("banner")] + public required string Banner { get; set; } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Dash/SideBar.razor b/ShockOsc/Ui/Pages/Dash/SideBar.razor index 758e129..1aedbde 100644 --- a/ShockOsc/Ui/Pages/Dash/SideBar.razor +++ b/ShockOsc/Ui/Pages/Dash/SideBar.razor @@ -18,7 +18,7 @@
- + ShockOSC
diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor index 26eae0a..55ed8df 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/AppSettingsTab.razor @@ -13,6 +13,7 @@
+
diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor index ff67ea5..0a7638f 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/DashboardTab.razor @@ -3,15 +3,18 @@ @using Microsoft.AspNetCore.SignalR.Client @using OpenShock.SDK.CSharp.Hub @using OpenShock.ShockOsc.Backend +@using OpenShock.ShockOsc.Config @using OpenShock.ShockOsc.Services @using Semver @using OpenShock.ShockOsc.Ui.Utils @using OpenShock.ShockOsc.Ui.Pages.Dash.Components +@using OpenShock.ShockOsc.Utils @inject StatusHandler StatusHandler @inject LiveControlManager LiveControlManager @inject OpenShockApi Api @inject OpenShockHubClient ApiHubClient @inject ISnackbar Snackbar +@inject ConfigManager ConfigManager @implements IDisposable @page "/dash/dashboard" @@ -57,25 +60,20 @@
- - - - - - - Placeholder - Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna - - - - - Placeholder - Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna - + @if (ConfigManager.Config.App.DiscordPreview) + { + + + + } - - Placeholder - Duo no lorem aliquyam dolor voluptua minim dolor diam sed dolores accusam est dolore voluptua takimata vel ullamcorper erat takimata erat at consectetuer amet magna + + Discord +
+
+ +
@@ -83,7 +81,6 @@ .dashboard-box { display: grid; - align-items: stretch; grid-template-columns: repeat(3, 1fr); @@ -95,6 +92,9 @@ width: 100%; height: 100%; + + overflow: auto; + } .dashboard-box .item { @@ -113,14 +113,8 @@ @code { private static readonly SemVersion Version = SemVersion.Parse(typeof(SideBar).Assembly.GetCustomAttribute()!.InformationalVersion, SemVersionStyles.Strict); - private void OpenOpenShock() => OpenUrl("https://openshock.org"); - private void OpenGithub() => OpenUrl("https://github.com/OpenShock/ShockOsc"); - - private void OpenUrl(string url) - { - Snackbar.Add("Opened URL in browser", Severity.Info); - Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); - } + private void OpenOpenShock() => UiUtils.OpenUrl("https://openshock.org", Snackbar); + private void OpenGithub() => UiUtils.OpenUrl("https://github.com/OpenShock/ShockOsc", Snackbar); protected override void OnInitialized() { diff --git a/ShockOsc/Utils/UiUtils.cs b/ShockOsc/Utils/UiUtils.cs index a8b5ca8..a89e050 100644 --- a/ShockOsc/Utils/UiUtils.cs +++ b/ShockOsc/Utils/UiUtils.cs @@ -1,4 +1,7 @@ -namespace OpenShock.ShockOsc.Utils; +using System.Diagnostics; +using MudBlazor; + +namespace OpenShock.ShockOsc.Utils; public static class UiUtils { @@ -21,4 +24,10 @@ public static class UiUtils return $"{input[..max].Trim()}..."; } + + public static void OpenUrl(string url, ISnackbar? snackbar = null) + { + snackbar?.Add("Opened URL in browser", Severity.Info); + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } } \ No newline at end of file diff --git a/ShockOsc/wwwroot/app.css b/ShockOsc/wwwroot/app.css index 6ac3583..4e95185 100644 --- a/ShockOsc/wwwroot/app.css +++ b/ShockOsc/wwwroot/app.css @@ -47,4 +47,19 @@ body { .openshock-slider-length { width: 300px !important; margin-left: 30px; +} + +.row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; +} + + +.column { + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; } \ No newline at end of file diff --git a/ShockOsc/wwwroot/images/IconSlowSpin.svg b/ShockOsc/wwwroot/images/IconSlowSpin.svg new file mode 100644 index 0000000..3a4a8c2 --- /dev/null +++ b/ShockOsc/wwwroot/images/IconSlowSpin.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file From 2a8cbd7079d4abd5c173a518e711fbc8090693f8 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 May 2024 09:27:16 +0200 Subject: [PATCH 82/95] Headless mode --- ShockOsc/Cli/CliOptions.cs | 9 ++ ShockOsc/HeadlessProgram.cs | 24 +++ ShockOsc/MauiProgram.cs | 145 ++---------------- .../Linux/{LinuxApp.cs => LinuxEntryPoint.cs} | 4 +- ShockOsc/Platforms/Windows/App.xaml.cs | 5 +- .../Platforms/Windows/WindowsEntryPoint.cs | 56 +++++++ ShockOsc/Platforms/Windows/WindowsServices.cs | 13 ++ .../Platforms/Windows/WindowsTrayService.cs | 6 +- ShockOsc/Properties/launchSettings.json | 7 +- ShockOsc/Services/AuthService.cs | 36 +++++ ShockOsc/Services/BackendControlService.cs | 14 -- ShockOsc/ShockOsc.csproj | 8 +- ShockOsc/ShockOscBootstrap.cs | 114 ++++++++++++++ .../Pages/Authentication/Authenticate.razor | 19 +-- 14 files changed, 289 insertions(+), 171 deletions(-) create mode 100644 ShockOsc/Cli/CliOptions.cs create mode 100644 ShockOsc/HeadlessProgram.cs rename ShockOsc/Platforms/Linux/{LinuxApp.cs => LinuxEntryPoint.cs} (58%) create mode 100644 ShockOsc/Platforms/Windows/WindowsEntryPoint.cs create mode 100644 ShockOsc/Platforms/Windows/WindowsServices.cs create mode 100644 ShockOsc/Services/AuthService.cs delete mode 100644 ShockOsc/Services/BackendControlService.cs create mode 100644 ShockOsc/ShockOscBootstrap.cs diff --git a/ShockOsc/Cli/CliOptions.cs b/ShockOsc/Cli/CliOptions.cs new file mode 100644 index 0000000..7829b28 --- /dev/null +++ b/ShockOsc/Cli/CliOptions.cs @@ -0,0 +1,9 @@ +using CommandLine; + +namespace OpenShock.ShockOsc.Cli; + +public sealed class CliOptions +{ + [Option('h', "headless", Required = false, Default = false, HelpText = "Run the application in headless mode.")] + public bool Headless { get; set; } +} \ No newline at end of file diff --git a/ShockOsc/HeadlessProgram.cs b/ShockOsc/HeadlessProgram.cs new file mode 100644 index 0000000..be5f6e5 --- /dev/null +++ b/ShockOsc/HeadlessProgram.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Hosting; + +namespace OpenShock.ShockOsc; + +public static class HeadlessProgram +{ + public static IHost SetupHeadlessHost() + { + var builder = Host.CreateDefaultBuilder(); + builder.ConfigureServices(services => + { + services.AddShockOscServices(); + +#if WINDOWS + services.AddWindowsServices(); +#endif + }); + + var app = builder.Build(); + app.Services.StartShockOscServices(true); + + return app; + } +} \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 1c4857b..7ab4e45 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,40 +1,13 @@ -#if WINDOWS -using System.Diagnostics; -using System.Net; +#if MAUI using Microsoft.Maui.LifecycleEvents; -using MudBlazor.Services; -using OpenShock.SDK.CSharp.Hub; -using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Config; -using OpenShock.ShockOsc.Logging; -using OpenShock.ShockOsc.OscQueryLibrary; -using OpenShock.ShockOsc.Services; -using OpenShock.ShockOsc.Utils; -using Serilog; using MauiApp = OpenShock.ShockOsc.Ui.MauiApp; -using Rect = OpenShock.ShockOsc.Utils.Rect; - - using Microsoft.UI; - -#if M - -#endif - namespace OpenShock.ShockOsc; public static class MauiProgram { - private const int WS_CAPTION = 0x00C00000; - private const int WS_BORDER = 0x00800000; - private const int WS_SYSMENU = 0x00080000; - private const int WS_SIZEBOX = 0x00040000; - private const int WS_MINIMIZEBOX = 0x00020000; - private const int WS_MAXIMIZEBOX = 0x00010000; - private const int WS_THICKFRAME = 0x00040000; - - private static ShockOscConfig? _config; public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() @@ -43,96 +16,21 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() // <---- Services ----> - var loggerConfiguration = new LoggerConfiguration() - .MinimumLevel.Information() - .Filter.ByExcluding(ev => - ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter - .ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) - .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information) - .WriteTo.UiLogSink() - .WriteTo.Console( - outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"); - - // ReSharper disable once RedundantAssignment - var isDebug = Environment.GetCommandLineArgs() - .Any(x => x.Equals("--debug", StringComparison.InvariantCultureIgnoreCase)); - -#if DEBUG - isDebug = true; -#endif - if (isDebug) - { - Console.WriteLine("Debug mode enabled"); - loggerConfiguration.MinimumLevel.Verbose(); - } - - Log.Logger = loggerConfiguration.CreateLogger(); - - builder.Services.AddSerilog(Log.Logger); - - builder.Services.AddMemoryCache(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(provider => - { - var config = provider.GetRequiredService(); - var listenAddress = config.Config.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; - return new OscQueryServer("ShockOsc", listenAddress, config); - }); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - + builder.Services.AddShockOscServices(); + +#if WINDOWS + builder.Services.AddWindowsServices(); + builder.ConfigureLifecycleEvents(lifecycleBuilder => { lifecycleBuilder.AddWindows(windowsLifecycleBuilder => { windowsLifecycleBuilder.OnWindowCreated(window => { - //use Microsoft.UI.Windowing functions for window var handle = WinRT.Interop.WindowNative.GetWindowHandle(window); - var id = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(handle); + var id = Win32Interop.GetWindowIdFromWindow(handle); var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(id); - // var style = WindowUtils.GetWindowLongPtrA(handle, (int)WindowLongFlags.GWL_STYLE); - // - // style &= ~WS_CAPTION; // Remove the title bar - // style |= WS_THICKFRAME; // Add thick frame for resizing - // - // WindowUtils.SetWindowLongPtrA(handle, (int)WindowLongFlags.GWL_STYLE, style); - // - // var reff = new Rect(); - // WindowUtils.AdjustWindowRectEx(ref reff, style, false, 0); - // reff.top = 6000; - // reff.left *= -1; - // - // var margins = new Margins - // { - // cxLeftWidth = 0, - // cxRightWidth = 0, - // cyTopHeight = 0, - // cyBottomHeight = 0 - // }; - // - // WindowUtils.DwmExtendFrameIntoClientArea(handle, ref margins); - // - // WindowUtils.SetWindowPos(handle, 0, 0, 0, 0, 0x0040 | 0x0002 | 0x0001 | 0x0020); - // - //When user execute the closing method, we can push a display alert. If user click Yes, close this application, if click the cancel, display alert will dismiss. appWindow.Closing += async (s, e) => { @@ -155,39 +53,20 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() }); }); }); - - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddMudServices(); - builder.Services.AddMauiBlazorWebView(); +#endif // <---- App ----> builder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); - - -#if DEBUG - builder.Services.AddBlazorWebViewDeveloperTools(); -#endif - + var app = builder.Build(); - + _config = app.Services.GetRequiredService().Config; - app.Services.GetService()?.Initialize(); - - // <---- Warmup ----> - app.Services.GetRequiredService(); - app.Services.GetRequiredService().Start(); - - var updater = app.Services.GetRequiredService(); - OsTask.Run(updater.CheckUpdate); - + app.Services.StartShockOscServices(false); + return app; } } diff --git a/ShockOsc/Platforms/Linux/LinuxApp.cs b/ShockOsc/Platforms/Linux/LinuxEntryPoint.cs similarity index 58% rename from ShockOsc/Platforms/Linux/LinuxApp.cs rename to ShockOsc/Platforms/Linux/LinuxEntryPoint.cs index a609cec..27db742 100644 --- a/ShockOsc/Platforms/Linux/LinuxApp.cs +++ b/ShockOsc/Platforms/Linux/LinuxEntryPoint.cs @@ -1,6 +1,6 @@ -namespace OpenShock.ShockOsc.Linux; +namespace OpenShock.ShockOsc.Platforms.Linux; #if !WINDOWS -public static class LinuxApp +public static class LinuxEntryPoint { public static void Main(string[] args) { diff --git a/ShockOsc/Platforms/Windows/App.xaml.cs b/ShockOsc/Platforms/Windows/App.xaml.cs index d144b91..11f6bcc 100644 --- a/ShockOsc/Platforms/Windows/App.xaml.cs +++ b/ShockOsc/Platforms/Windows/App.xaml.cs @@ -1,8 +1,9 @@ - +#if WINDOWS // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. -#if WINDOWS + +using Microsoft.Windows.AppLifecycle; namespace OpenShock.ShockOsc.Platforms.Windows; /// diff --git a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs new file mode 100644 index 0000000..eb7f8c8 --- /dev/null +++ b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs @@ -0,0 +1,56 @@ +#if WINDOWS +using System.Runtime.InteropServices; +using CommandLine; +using Microsoft.Extensions.Hosting; +using Microsoft.UI.Dispatching; +using OpenShock.ShockOsc.Cli; +using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Utils; +using WinRT; +using Application = Microsoft.UI.Xaml.Application; + +namespace OpenShock.ShockOsc.Platforms.Windows; + +public static class WindowsEntryPoint +{ + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + [DllImport("Microsoft.ui.xaml.dll")] + private static extern void XamlCheckProcessRequirements(); + + [STAThread] + private static void Main(string[] args) + { + var parsed = Parser.Default.ParseArguments(args); + parsed.WithParsed(Start); + parsed.WithNotParsed(errors => + { + errors.Output(); + Environment.Exit(1); + }); + } + + private static void Start(CliOptions config) + { + if (config.Headless) + { + Console.WriteLine("Running in headless mode."); + + var host = HeadlessProgram.SetupHeadlessHost(); + OsTask.Run(host.Services.GetRequiredService().Authenticate); + host.Run(); + + return; + } + + XamlCheckProcessRequirements(); + ComWrappersSupport.InitializeComWrappers(); + Application.Start(delegate + { + var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + // ReSharper disable once ObjectCreationAsStatement + new App(); + }); + } +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsServices.cs b/ShockOsc/Platforms/Windows/WindowsServices.cs new file mode 100644 index 0000000..e1abe29 --- /dev/null +++ b/ShockOsc/Platforms/Windows/WindowsServices.cs @@ -0,0 +1,13 @@ +#if WINDOWS +using OpenShock.ShockOsc.Services; + +namespace OpenShock.ShockOsc; + +public static class WindowsServices +{ + public static void AddWindowsServices(this IServiceCollection services) + { + services.AddSingleton(); + } +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index 861a924..142bd99 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -49,10 +49,14 @@ public void Initialize() menu.Items.Add(new ToolStripSeparator()); menu.Items.Add("Restart", null, Restart); menu.Items.Add("Quit ShockOSC", null, OnQuitClick); - + tray.ContextMenuStrip = menu; tray.Click += OnMainClick; + menu.Opened += async (sender, args) => + { + var aa = menu; + }; tray.Visible = true; } diff --git a/ShockOsc/Properties/launchSettings.json b/ShockOsc/Properties/launchSettings.json index de9182a..36a0c0e 100644 --- a/ShockOsc/Properties/launchSettings.json +++ b/ShockOsc/Properties/launchSettings.json @@ -1,8 +1,13 @@ { "profiles": { - "Windows Machine": { + "ShockOsc": { "commandName": "Project", "nativeDebugging": false + }, + "ShockOscHeadless": { + "commandName": "Project", + "nativeDebugging": false, + "commandLineArgs": "--headless" } } } diff --git a/ShockOsc/Services/AuthService.cs b/ShockOsc/Services/AuthService.cs new file mode 100644 index 0000000..c5a7ccf --- /dev/null +++ b/ShockOsc/Services/AuthService.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Hub; +using OpenShock.ShockOsc.Backend; + +namespace OpenShock.ShockOsc.Services; + +public sealed class AuthService +{ + private readonly ILogger _logger; + private readonly BackendHubManager _backendHubManager; + private readonly OpenShockHubClient _hubClient; + private readonly LiveControlManager _liveControlManager; + private readonly OpenShockApi _apiClient; + + public AuthService(ILogger logger, BackendHubManager backendHubManager, OpenShockHubClient hubClient, LiveControlManager liveControlManager, OpenShockApi apiClient) + { + _logger = logger; + _backendHubManager = backendHubManager; + _hubClient = hubClient; + _liveControlManager = liveControlManager; + _apiClient = apiClient; + } + + public async Task Authenticate() + { + _logger.LogInformation("Setting up live client"); + await _backendHubManager.SetupLiveClient(); + _logger.LogInformation("Starting live client"); + await _hubClient.StartAsync(); + + _logger.LogInformation("Refreshing shockers"); + await _apiClient.RefreshShockers(); + + await _liveControlManager.RefreshConnections(); + } +} \ No newline at end of file diff --git a/ShockOsc/Services/BackendControlService.cs b/ShockOsc/Services/BackendControlService.cs deleted file mode 100644 index b357be0..0000000 --- a/ShockOsc/Services/BackendControlService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using OpenShock.ShockOsc.Backend; -using OpenShock.ShockOsc.Ui.Components; - -namespace OpenShock.ShockOsc.Services; - -public sealed class BackendControlService -{ - private readonly BackendHubManager _backendHubManager; - - public BackendControlService(BackendHubManager backendHubManager) - { - _backendHubManager = backendHubManager; - } -} \ No newline at end of file diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 4d533c8..b8ffa9e 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -25,6 +25,8 @@ en en-US;en false + + DISABLE_XAML_GENERATED_MAIN @@ -32,10 +34,12 @@ 10.0.17763.0 10.0.17763.0 None + MAUI + OpenShock.ShockOsc.Platforms.Windows.WindowsEntryPoint - OpenShock.ShockOsc.Linux.LinuxApp + OpenShock.ShockOsc.Platforms.Linux.LinuxEntryPoint @@ -61,8 +65,10 @@ + + diff --git a/ShockOsc/ShockOscBootstrap.cs b/ShockOsc/ShockOscBootstrap.cs new file mode 100644 index 0000000..9fa1634 --- /dev/null +++ b/ShockOsc/ShockOscBootstrap.cs @@ -0,0 +1,114 @@ +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using MudBlazor.Services; +using OpenShock.SDK.CSharp.Hub; +using OpenShock.ShockOsc.Backend; +using OpenShock.ShockOsc.Config; +using OpenShock.ShockOsc.Logging; +using OpenShock.ShockOsc.OscQueryLibrary; +using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Utils; +using Serilog; + +namespace OpenShock.ShockOsc; + +public static class ShockOscBootstrap +{ + public static void AddShockOscServices(this IServiceCollection services) + { + var loggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Information() + .Filter.ByExcluding(ev => + ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter + .ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) + .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information) + .WriteTo.UiLogSink() + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"); + + // ReSharper disable once RedundantAssignment + var isDebug = Environment.GetCommandLineArgs() + .Any(x => x.Equals("--debug", StringComparison.InvariantCultureIgnoreCase)); + +#if DEBUG + isDebug = true; +#endif + if (isDebug) + { + Console.WriteLine("Debug mode enabled"); + loggerConfiguration.MinimumLevel.Verbose(); + } + + Log.Logger = loggerConfiguration.CreateLogger(); + + services.AddSerilog(Log.Logger); + + services.AddMemoryCache(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(provider => + { + var config = provider.GetRequiredService(); + var listenAddress = config.Config.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; + return new OscQueryServer("ShockOsc", listenAddress, config); + }); + + services.AddSingleton(); + services.AddSingleton(); + +#if DEBUG + services.AddBlazorWebViewDeveloperTools(); +#endif + + services.AddSingleton(); + + services.AddMudServices(); + services.AddMauiBlazorWebView(); + } + + public static void StartShockOscServices(this IServiceProvider services, bool headless) + { + #region SystemTray + +#if WINDOWS + if (headless) + { + var applicationThread = new Thread(() => + { + services.GetService()?.Initialize(); + System.Windows.Forms.Application.Run(); + }); + applicationThread.Start(); + } + else services.GetService()?.Initialize(); +#else + services.GetService()?.Initialize(); +#endif + + #endregion + + + // <---- Warmup ----> + services.GetRequiredService(); + services.GetRequiredService().Start(); + + var updater = services.GetRequiredService(); + OsTask.Run(updater.CheckUpdate); + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index ac24ab0..7af406e 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -1,16 +1,9 @@ -@using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config -@using Microsoft.Extensions.Logging -@using OpenShock.SDK.CSharp.Hub @using OpenShock.ShockOsc.Services @inject ConfigManager ConfigManager @inject NavigationManager NavigationManager -@inject BackendHubManager HubManager -@inject OpenShockHubClient HubClient -@inject OpenShockApi ApiClient -@inject ILogger Logger -@inject LiveControlManager LiveControlManager @inject ISnackbar Snackbar +@inject AuthService AuthService @layout NotAuthedLayout @inherits LayoutComponentBase @@ -83,15 +76,7 @@ try { - Logger.LogInformation("Setting up live client"); - await HubManager.SetupLiveClient(); - Logger.LogInformation("Starting live client"); - await HubClient.StartAsync(); - - Logger.LogInformation("Refreshing shockers"); - await ApiClient.RefreshShockers(); - - await LiveControlManager.RefreshConnections(); + await AuthService.Authenticate(); _currentState = State.Authenticated; await InvokeAsync(StateHasChanged); From 2b9752810fe02839a9495797ec2c6940e5844bda Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 May 2024 12:33:03 +0200 Subject: [PATCH 83/95] Installer update --- Installer/installer.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Installer/installer.nsi b/Installer/installer.nsi index 559b2e4..1150875 100644 --- a/Installer/installer.nsi +++ b/Installer/installer.nsi @@ -173,7 +173,7 @@ Section "Install" SecInstall WriteRegExpandStr HKCU "Software\Classes\ShockOSC\DefaultIcon" "" "$INSTDIR\Resources\openshock-icon.ico" WriteRegStr HKCU "Software\Classes\ShockOSC\shell" "" "open" WriteRegStr HKCU "Software\Classes\ShockOSC\shell\open" "FriendlyAppName" "ShockOSC" - WriteRegStr HKCU "Software\Classes\ShockOSC\shell\open\command" "" '"$INSTDIR\OpenShock.ShockOsc.exe" /uri="%1" /params="%2 %3 %4"' + WriteRegStr HKCU "Software\Classes\ShockOSC\shell\open\command" "" '"$INSTDIR\OpenShock.ShockOsc.exe" --uri="%1"' ${If} ${Silent} SetOutPath $INSTDIR From f8a6df388ac3e033aeaa5728d2df9b8d33a47c8a Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 7 May 2024 08:13:40 +0200 Subject: [PATCH 84/95] Named pipes for. custom uri handling --- ShockOsc/Cli/CliOptions.cs | 10 ++- ShockOsc/Cli/Uri/UriParameter.cs | 7 ++ ShockOsc/Cli/Uri/UriParameterType.cs | 6 ++ ShockOsc/Cli/Uri/UriParser.cs | 17 +++++ ShockOsc/Platforms/Windows/PipeHelper.cs | 32 ++++++++ .../Platforms/Windows/WindowsEntryPoint.cs | 58 +++++++++++++- .../Platforms/Windows/WindowsTrayService.cs | 8 +- ShockOsc/Services/Pipes/PipeMessage.cs | 7 ++ ShockOsc/Services/Pipes/PipeMessageType.cs | 6 ++ ShockOsc/Services/Pipes/PipeServerService.cs | 76 +++++++++++++++++++ ShockOsc/ShockOscBootstrap.cs | 4 + 11 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 ShockOsc/Cli/Uri/UriParameter.cs create mode 100644 ShockOsc/Cli/Uri/UriParameterType.cs create mode 100644 ShockOsc/Cli/Uri/UriParser.cs create mode 100644 ShockOsc/Platforms/Windows/PipeHelper.cs create mode 100644 ShockOsc/Services/Pipes/PipeMessage.cs create mode 100644 ShockOsc/Services/Pipes/PipeMessageType.cs create mode 100644 ShockOsc/Services/Pipes/PipeServerService.cs diff --git a/ShockOsc/Cli/CliOptions.cs b/ShockOsc/Cli/CliOptions.cs index 7829b28..fe22682 100644 --- a/ShockOsc/Cli/CliOptions.cs +++ b/ShockOsc/Cli/CliOptions.cs @@ -4,6 +4,12 @@ namespace OpenShock.ShockOsc.Cli; public sealed class CliOptions { - [Option('h', "headless", Required = false, Default = false, HelpText = "Run the application in headless mode.")] - public bool Headless { get; set; } + [Option("headless", Required = false, Default = false, HelpText = "Run the application in headless mode.")] + public required bool Headless { get; init; } + + [Option('c', "console", Required = false, Default = false, HelpText = "Create console window for stdout/stderr.")] + public required bool Console { get; init; } + + [Option("uri", Required = false, HelpText = "Custom URI for callbacks")] + public required string Uri { get; init; } } \ No newline at end of file diff --git a/ShockOsc/Cli/Uri/UriParameter.cs b/ShockOsc/Cli/Uri/UriParameter.cs new file mode 100644 index 0000000..f9491b4 --- /dev/null +++ b/ShockOsc/Cli/Uri/UriParameter.cs @@ -0,0 +1,7 @@ +namespace OpenShock.ShockOsc.Cli.Uri; + +public class UriParameter +{ + public required UriParameterType Type { get; set; } + public IReadOnlyCollection Arguments { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/ShockOsc/Cli/Uri/UriParameterType.cs b/ShockOsc/Cli/Uri/UriParameterType.cs new file mode 100644 index 0000000..d0c4d8c --- /dev/null +++ b/ShockOsc/Cli/Uri/UriParameterType.cs @@ -0,0 +1,6 @@ +namespace OpenShock.ShockOsc.Cli.Uri; + +public enum UriParameterType +{ + Token +} \ No newline at end of file diff --git a/ShockOsc/Cli/Uri/UriParser.cs b/ShockOsc/Cli/Uri/UriParser.cs new file mode 100644 index 0000000..64fe6b3 --- /dev/null +++ b/ShockOsc/Cli/Uri/UriParser.cs @@ -0,0 +1,17 @@ +namespace OpenShock.ShockOsc.Cli.Uri; + +public static class UriParser +{ + public static UriParameter Parse(string uri) + { + ReadOnlySpan uriSpan = uri; + var dePrefixed = uriSpan[9..]; + var type = dePrefixed[..dePrefixed.IndexOf('/')]; + + return new UriParameter + { + Type = Enum.Parse(type, true), + Arguments = dePrefixed[(type.Length + 1)..].ToString().Split('/') + }; + } +} \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/PipeHelper.cs b/ShockOsc/Platforms/Windows/PipeHelper.cs new file mode 100644 index 0000000..4cf5606 --- /dev/null +++ b/ShockOsc/Platforms/Windows/PipeHelper.cs @@ -0,0 +1,32 @@ +using System.Collections; + +namespace OpenShock.ShockOsc; + +public static class PipeHelper +{ + public static IEnumerable EnumeratePipes() { + bool MoveNextSafe(IEnumerator enumerator) { + + // Pipes might have illegal characters in path. Seen one from IAR containing < and >. + // The FileSystemEnumerable.MoveNext source code indicates that another call to MoveNext will return + // the next entry. + // Pose a limit in case the underlying implementation changes somehow. This also means that no more than 10 + // pipes with bad names may occur in sequence. + const int retries = 10; + for (int i = 0; i < retries; i++) { + try { + return enumerator.MoveNext(); + } catch (ArgumentException) { + } + } + Console.WriteLine("Pipe enumeration: Retry limit due to bad names reached."); + return false; + } + + using (var enumerator = Directory.EnumerateFiles(@"\\.\pipe\").GetEnumerator()) { + while (MoveNextSafe(enumerator)) { + yield return enumerator.Current; + } + } + } +} \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs index eb7f8c8..ba3b88e 100644 --- a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs +++ b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs @@ -1,22 +1,37 @@ #if WINDOWS +using System.Diagnostics; +using System.IO.Pipes; using System.Runtime.InteropServices; +using System.Text.Json; using CommandLine; using Microsoft.Extensions.Hosting; using Microsoft.UI.Dispatching; +using Microsoft.Windows.AppLifecycle; using OpenShock.ShockOsc.Cli; +using OpenShock.ShockOsc.Cli.Uri; using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Services.Pipes; using OpenShock.ShockOsc.Utils; using WinRT; using Application = Microsoft.UI.Xaml.Application; +using UriParser = OpenShock.ShockOsc.Cli.Uri.UriParser; namespace OpenShock.ShockOsc.Platforms.Windows; public static class WindowsEntryPoint { + private const int ATTACH_PARENT_PROCESS = -1; + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] [DllImport("Microsoft.ui.xaml.dll")] private static extern void XamlCheckProcessRequirements(); + [DllImport("kernel32.dll")] + private static extern bool AllocConsole(); + + [DllImport("kernel32.dll")] + private static extern bool AttachConsole(int pid); + [STAThread] private static void Main(string[] args) { @@ -31,6 +46,45 @@ private static void Main(string[] args) private static void Start(CliOptions config) { + if (config.Console) + { + // Command line given, display console + if (!AttachConsole(ATTACH_PARENT_PROCESS)) + AllocConsole(); + } + + const string pipeName = @"\\.\pipe\OpenShock.ShockOSC"; + + if (PipeHelper.EnumeratePipes().Any(x => x.Equals(pipeName, StringComparison.InvariantCultureIgnoreCase))) + { + if (!string.IsNullOrEmpty(config.Uri)) + { + using var pipeClientStream = new NamedPipeClientStream(".", "OpenShock.ShockOsc", PipeDirection.Out); + pipeClientStream.Connect(500); + + using var writer = new StreamWriter(pipeClientStream); + writer.AutoFlush = true; + + var parsedUri = UriParser.Parse(config.Uri); + + if (parsedUri.Type == UriParameterType.Token) + { + writer.WriteLine(JsonSerializer.Serialize(new PipeMessage + { + Type = PipeMessageType.Token, + Data = parsedUri.Arguments + })); + } + + return; + } + + Console.WriteLine("Another instance of ShockOSC is already running."); + Environment.Exit(1); + return; + } + + if (config.Headless) { Console.WriteLine("Running in headless mode."); @@ -38,10 +92,10 @@ private static void Start(CliOptions config) var host = HeadlessProgram.SetupHeadlessHost(); OsTask.Run(host.Services.GetRequiredService().Authenticate); host.Run(); - + return; } - + XamlCheckProcessRequirements(); ComWrappersSupport.InitializeComWrappers(); Application.Start(delegate diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index 142bd99..091c6e7 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -89,7 +89,13 @@ private static void OnMainClick(object? sender, EventArgs eventArgs) private static void OnQuitClick(object? sender, EventArgs eventArgs) { - Application.Current?.Quit(); + if (Application.Current != null) + { + Application.Current.Quit(); + return; + } + + Environment.Exit(0); } } diff --git a/ShockOsc/Services/Pipes/PipeMessage.cs b/ShockOsc/Services/Pipes/PipeMessage.cs new file mode 100644 index 0000000..4271760 --- /dev/null +++ b/ShockOsc/Services/Pipes/PipeMessage.cs @@ -0,0 +1,7 @@ +namespace OpenShock.ShockOsc.Services.Pipes; + +public sealed class PipeMessage +{ + public required PipeMessageType Type { get; set; } + public object? Data { get; set; } +} \ No newline at end of file diff --git a/ShockOsc/Services/Pipes/PipeMessageType.cs b/ShockOsc/Services/Pipes/PipeMessageType.cs new file mode 100644 index 0000000..e99a79b --- /dev/null +++ b/ShockOsc/Services/Pipes/PipeMessageType.cs @@ -0,0 +1,6 @@ +namespace OpenShock.ShockOsc.Services.Pipes; + +public enum PipeMessageType +{ + Token +} \ No newline at end of file diff --git a/ShockOsc/Services/Pipes/PipeServerService.cs b/ShockOsc/Services/Pipes/PipeServerService.cs new file mode 100644 index 0000000..1f89726 --- /dev/null +++ b/ShockOsc/Services/Pipes/PipeServerService.cs @@ -0,0 +1,76 @@ +using System.Collections.Concurrent; +using System.IO.Pipes; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Utils; +using OpenShock.ShockOsc.Utils; + +namespace OpenShock.ShockOsc.Services.Pipes; + +public sealed class PipeServerService +{ + private readonly ILogger _logger; + private uint _clientCount = 0; + + public PipeServerService(ILogger logger) + { + _logger = logger; + } + + public ConcurrentQueue MessageQueue { get; } = new(); + public event Func? OnMessageReceived; + + public void StartServer() + { + OsTask.Run(ServerLoop); + } + + private async Task ServerLoop() + { + var id = _clientCount++; + + await using var pipeServerStream = new NamedPipeServerStream("OpenShock.ShockOsc", PipeDirection.In, 20, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + + + _logger.LogInformation("[{Id}] Starting new server loop", id); + + await pipeServerStream.WaitForConnectionAsync(); +#pragma warning disable CS4014 + OsTask.Run(ServerLoop); +#pragma warning restore CS4014 + + _logger.LogInformation("[{Id}] Pipe connected!", id); + + using var reader = new StreamReader(pipeServerStream); + while (pipeServerStream.IsConnected && !reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (string.IsNullOrEmpty(line)) + { + _logger.LogWarning("[{Id}] Received empty pipe message. Skipping...", id); + continue; + } + + try + { + var jsonObj = JsonSerializer.Deserialize(line); + if (jsonObj is null) + { + _logger.LogWarning("[{Id}] Failed to deserialize pipe message. Skipping...", id); + continue; + } + + MessageQueue.Enqueue(jsonObj); + await OnMessageReceived.Raise(); + _logger.LogInformation("[{Id}], Received pipe message of type: {Type}", id, jsonObj.Type); + } + catch (JsonException ex) + { + _logger.LogError(ex, "[{Id}] Failed to deserialize pipe message. Skipping...", id); + } + } + + _logger.LogInformation("[{Id}] Pipe disconnected. Stopping server loop...", id); + } +} \ No newline at end of file diff --git a/ShockOsc/ShockOscBootstrap.cs b/ShockOsc/ShockOscBootstrap.cs index 9fa1634..3341838 100644 --- a/ShockOsc/ShockOscBootstrap.cs +++ b/ShockOsc/ShockOscBootstrap.cs @@ -7,6 +7,7 @@ using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Services.Pipes; using OpenShock.ShockOsc.Utils; using Serilog; @@ -45,6 +46,8 @@ public static void AddShockOscServices(this IServiceCollection services) services.AddMemoryCache(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); @@ -107,6 +110,7 @@ public static void StartShockOscServices(this IServiceProvider services, bool he // <---- Warmup ----> services.GetRequiredService(); services.GetRequiredService().Start(); + services.GetRequiredService().StartServer(); var updater = services.GetRequiredService(); OsTask.Run(updater.CheckUpdate); From 1dbc7a1949fce676dcdc5bcea16c2027b424d517 Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 9 May 2024 03:22:42 +0200 Subject: [PATCH 85/95] Token logic for auth + solve https://github.com/OpenShock/ShockOsc/issues/26 --- .../Platforms/Windows/WindowsEntryPoint.cs | 4 +- ShockOsc/Services/Pipes/PipeServerService.cs | 13 ++- ShockOsc/Services/UnderscoreConfig.cs | 96 +++++++++++++++++-- .../Pages/Authentication/Authenticate.razor | 19 +++- .../Ui/Pages/Authentication/LoginPart.razor | 7 ++ 5 files changed, 124 insertions(+), 15 deletions(-) diff --git a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs index ba3b88e..690cfeb 100644 --- a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs +++ b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs @@ -57,6 +57,7 @@ private static void Start(CliOptions config) if (PipeHelper.EnumeratePipes().Any(x => x.Equals(pipeName, StringComparison.InvariantCultureIgnoreCase))) { + // TODO: Refactor this if (!string.IsNullOrEmpty(config.Uri)) { using var pipeClientStream = new NamedPipeClientStream(".", "OpenShock.ShockOsc", PipeDirection.Out); @@ -72,7 +73,7 @@ private static void Start(CliOptions config) writer.WriteLine(JsonSerializer.Serialize(new PipeMessage { Type = PipeMessageType.Token, - Data = parsedUri.Arguments + Data = string.Join('/', parsedUri.Arguments) })); } @@ -84,7 +85,6 @@ private static void Start(CliOptions config) return; } - if (config.Headless) { Console.WriteLine("Running in headless mode."); diff --git a/ShockOsc/Services/Pipes/PipeServerService.cs b/ShockOsc/Services/Pipes/PipeServerService.cs index 1f89726..a5b5935 100644 --- a/ShockOsc/Services/Pipes/PipeServerService.cs +++ b/ShockOsc/Services/Pipes/PipeServerService.cs @@ -17,7 +17,7 @@ public PipeServerService(ILogger logger) _logger = logger; } - public ConcurrentQueue MessageQueue { get; } = new(); + public string? Token { get; set; } public event Func? OnMessageReceived; public void StartServer() @@ -61,7 +61,16 @@ private async Task ServerLoop() continue; } - MessageQueue.Enqueue(jsonObj); + switch (jsonObj.Type) + { + case PipeMessageType.Token: + Token = jsonObj.Data?.ToString(); + break; + } + { + + } + await OnMessageReceived.Raise(); _logger.LogInformation("[{Id}], Received pipe message of type: {Type}", id, jsonObj.Type); } diff --git a/ShockOsc/Services/UnderscoreConfig.cs b/ShockOsc/Services/UnderscoreConfig.cs index 8fda63a..80e2ef7 100644 --- a/ShockOsc/Services/UnderscoreConfig.cs +++ b/ShockOsc/Services/UnderscoreConfig.cs @@ -80,6 +80,41 @@ private void HandleGlobalConfigCommand(string action, object? value) { switch (action) { + case "ModeIntensity": + if (value is bool modeIntensity) + { + _configManager.Config.Behaviour.RandomIntensity = modeIntensity; + _configManager.SaveFnf(); + OnConfigUpdate?.Invoke(); // update Ui + } + break; + + case "ModeDuration": + if (value is bool modeDuration) + { + _configManager.Config.Behaviour.RandomDuration = modeDuration; + _configManager.SaveFnf(); + OnConfigUpdate?.Invoke(); // update Ui + } + break; + + case "Intensity": + // 0..10sec + if (value is float intensityFloat) + { + var currentIntensity = + MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedIntensity / 100f); + if (Math.Abs(intensityFloat - currentIntensity) < 0.001) return; + + _configManager.Config.Behaviour.FixedIntensity = + Math.Clamp((byte)Math.Round(intensityFloat * 100), (byte)0, (byte)100); + ValidateSettings(); + _configManager.SaveFnf(); + OnConfigUpdate?.Invoke(); // update Ui + } + + break; + case "MinIntensity": // 0..100% if (value is float minIntensityFloat) @@ -91,7 +126,7 @@ private void HandleGlobalConfigCommand(string action, object? value) _configManager.Config.Behaviour.IntensityRange.Min = MathUtils.ClampUint((uint)Math.Round(minIntensityFloat * 100), 0, 100); ValidateSettings(); - _configManager.Save(); + _configManager.SaveFnf(); OnConfigUpdate?.Invoke(); // update Ui } @@ -108,7 +143,39 @@ private void HandleGlobalConfigCommand(string action, object? value) _configManager.Config.Behaviour.IntensityRange.Max = MathUtils.ClampUint((uint)Math.Round(maxIntensityFloat * 100), 0, 100); ValidateSettings(); - _configManager.Save(); + _configManager.SaveFnf(); + OnConfigUpdate?.Invoke(); // update Ui + } + + break; + + case "MinDuration": + // 0..10sec + if (value is float minDurationFloat) + { + var currentMinDuration = _configManager.Config.Behaviour.DurationRange.Min / 10_000f; + if (Math.Abs(minDurationFloat - currentMinDuration) < 0.001) return; + + _configManager.Config.Behaviour.DurationRange.Min = + MathUtils.ClampUint((uint)Math.Round(minDurationFloat * 100), 300, 30_000); + ValidateSettings(); + _configManager.SaveFnf(); + OnConfigUpdate?.Invoke(); // update Ui + } + + break; + + case "MaxDuration": + // 0..10sec + if (value is float maxDurationFloat) + { + var currentMaxDuration = _configManager.Config.Behaviour.DurationRange.Max / 10_000f; + if (Math.Abs(maxDurationFloat - currentMaxDuration) < 0.001) return; + + _configManager.Config.Behaviour.DurationRange.Max = + MathUtils.ClampUint((uint)Math.Round(maxDurationFloat * 10_000), 300, 30_000); + ValidateSettings(); + _configManager.SaveFnf(); OnConfigUpdate?.Invoke(); // update Ui } @@ -118,14 +185,13 @@ private void HandleGlobalConfigCommand(string action, object? value) // 0..10sec if (value is float durationFloat) { - var currentDuration = - MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f); + var currentDuration = _configManager.Config.Behaviour.FixedDuration / 10000f; if (Math.Abs(durationFloat - currentDuration) < 0.001) return; _configManager.Config.Behaviour.FixedDuration = - MathUtils.ClampUint((uint)Math.Round(durationFloat * 10000), 0, 10000); + MathUtils.ClampUint((uint)Math.Round(durationFloat * 10_000), 300, 10_000); ValidateSettings(); - _configManager.Save(); + _configManager.SaveFnf(); OnConfigUpdate?.Invoke(); // update Ui } @@ -142,7 +208,7 @@ private void HandleGlobalConfigCommand(string action, object? value) _configManager.Config.Behaviour.CooldownTime = MathUtils.ClampUint((uint)Math.Round(cooldownTimeFloat * 100000), 0, 100000); ValidateSettings(); - _configManager.Save(); + _configManager.SaveFnf(); OnConfigUpdate?.Invoke(); // update Ui } @@ -158,7 +224,7 @@ private void HandleGlobalConfigCommand(string action, object? value) _configManager.Config.Behaviour.HoldTime = MathUtils.ClampUint((uint)Math.Round(holdTimeFloat * 1000), 0, 1000); ValidateSettings(); - _configManager.Save(); + _configManager.SaveFnf(); OnConfigUpdate?.Invoke(); // update Ui } @@ -188,6 +254,10 @@ private void ValidateSettings() var intensityRange = _configManager.Config.Behaviour.IntensityRange; if (intensityRange.Min > intensityRange.Max) intensityRange.Max = intensityRange.Min; if (intensityRange.Max < intensityRange.Min) intensityRange.Min = intensityRange.Max; + + var durationRange = _configManager.Config.Behaviour.DurationRange; + if (durationRange.Min > durationRange.Max) durationRange.Max = durationRange.Min; + if (durationRange.Max < durationRange.Min) durationRange.Min = durationRange.Max; } public async Task SendUpdateForAll() @@ -204,5 +274,15 @@ await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Coold MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/ModeIntensity", + _configManager.Config.Behaviour.RandomIntensity); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/ModeDuration", + _configManager.Config.Behaviour.RandomDuration); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Intensity", + MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedIntensity / 100f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinDuration", + MathUtils.ClampFloat(_configManager.Config.Behaviour.DurationRange.Min / 10_000f)); + await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxDuration", + MathUtils.ClampFloat(_configManager.Config.Behaviour.DurationRange.Max / 10_000f)); } } \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index 7af406e..29eedb1 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -1,12 +1,15 @@ @using OpenShock.ShockOsc.Config @using OpenShock.ShockOsc.Services +@using OpenShock.ShockOsc.Services.Pipes @inject ConfigManager ConfigManager @inject NavigationManager NavigationManager @inject ISnackbar Snackbar @inject AuthService AuthService +@inject PipeServerService PipeService @layout NotAuthedLayout @inherits LayoutComponentBase + @page "/" @@ -54,13 +57,24 @@ private State _currentState = State.Login; - private bool Loading { get; set; } - protected override async Task OnInitializedAsync() { + if (await CheckTokenReceived()) return; + + PipeService.OnMessageReceived += CheckTokenReceived; + if (string.IsNullOrEmpty(ConfigManager.Config.OpenShock.Token)) return; + await ProceedAuthenticated(); + } + private async Task CheckTokenReceived() + { + if (string.IsNullOrEmpty(PipeService.Token) || _currentState != State.Login) return false; + ConfigManager.Config.OpenShock.Token = PipeService.Token; + await ConfigManager.SaveAsync(); + PipeService.Token = null; await ProceedAuthenticated(); + return true; } public void ReLogin() @@ -70,7 +84,6 @@ private async Task ProceedAuthenticated() { - Loading = true; _currentState = State.Loading; await InvokeAsync(StateHasChanged); diff --git a/ShockOsc/Ui/Pages/Authentication/LoginPart.razor b/ShockOsc/Ui/Pages/Authentication/LoginPart.razor index 0e43c79..e85f007 100644 --- a/ShockOsc/Ui/Pages/Authentication/LoginPart.razor +++ b/ShockOsc/Ui/Pages/Authentication/LoginPart.razor @@ -1,10 +1,12 @@ @using OneOf.Types @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config +@using OpenShock.ShockOsc.Utils @inject ConfigManager ConfigManager @inject OpenShockApi ApiClient +Get Token
Continue @@ -146,4 +148,9 @@ if (Server == BackendServer.Custom) _customServerUri = ConfigManager.Config.OpenShock.Backend; } + private async Task GetToken() + { + UiUtils.OpenUrl("https://shockl.ink/t/?redirect_uri=shockosc:token/%&permissions=shockers.use"); + } + } \ No newline at end of file From f62708d8626b0a2a5540ff7ad69ade9bdcdd6988 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 May 2024 08:45:21 +0200 Subject: [PATCH 86/95] Fix underscore config loop --- ShockOsc/Services/UnderscoreConfig.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ShockOsc/Services/UnderscoreConfig.cs b/ShockOsc/Services/UnderscoreConfig.cs index 80e2ef7..31bc0d1 100644 --- a/ShockOsc/Services/UnderscoreConfig.cs +++ b/ShockOsc/Services/UnderscoreConfig.cs @@ -83,6 +83,7 @@ private void HandleGlobalConfigCommand(string action, object? value) case "ModeIntensity": if (value is bool modeIntensity) { + if (_configManager.Config.Behaviour.RandomIntensity == modeIntensity) return; _configManager.Config.Behaviour.RandomIntensity = modeIntensity; _configManager.SaveFnf(); OnConfigUpdate?.Invoke(); // update Ui @@ -92,6 +93,7 @@ private void HandleGlobalConfigCommand(string action, object? value) case "ModeDuration": if (value is bool modeDuration) { + if(_configManager.Config.Behaviour.RandomDuration == modeDuration) return; _configManager.Config.Behaviour.RandomDuration = modeDuration; _configManager.SaveFnf(); OnConfigUpdate?.Invoke(); // update Ui From a3cc61f5c9d5a71d02fa1e56122eed4302de72d3 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 May 2024 10:01:04 +0200 Subject: [PATCH 87/95] Login Get Token Button --- .../Ui/Pages/Authentication/LoginPart.razor | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/ShockOsc/Ui/Pages/Authentication/LoginPart.razor b/ShockOsc/Ui/Pages/Authentication/LoginPart.razor index e85f007..b524a31 100644 --- a/ShockOsc/Ui/Pages/Authentication/LoginPart.razor +++ b/ShockOsc/Ui/Pages/Authentication/LoginPart.razor @@ -1,13 +1,20 @@ -@using OneOf.Types +@using System.Net.Http.Json +@using Microsoft.Extensions.Logging +@using OneOf.Types +@using OpenShock.SDK.CSharp.Models @using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config @using OpenShock.ShockOsc.Utils @inject ConfigManager ConfigManager @inject OpenShockApi ApiClient +@inject ISnackbar Snackbar +@inject ILogger Logger -Get Token +
+Get Token +

Continue
@@ -57,6 +64,12 @@ background-color: rgba(66, 66, 66, 1); border-radius: 5px; } + + .get-token-button { + margin-top: 8px; + margin-bottom: 2px; + margin-right: 10px; + } @code { @@ -150,7 +163,37 @@ private async Task GetToken() { - UiUtils.OpenUrl("https://shockl.ink/t/?redirect_uri=shockosc:token/%&permissions=shockers.use"); + Logger.LogTrace("Get API info"); + var httpClient = new HttpClient(); + httpClient.BaseAddress = ConfigManager.Config.OpenShock.Backend; + var response = await httpClient.GetAsync("1"); + if (!response.IsSuccessStatusCode) + { + Snackbar.Add("Failed to reach API", Severity.Error); + return; + } + + var root = await response.Content.ReadFromJsonAsync>(); + + if (root == null) + { + Snackbar.Add("Failed to reach API", Severity.Error); + return; + } + + Logger.LogTrace("Open browser for token"); + + var requestUri = new Uri(root.Data!.ShortLinkUrl, "/t/?name=ShockOSC&redirect_uri=shockosc:token/%&permissions=shockers.use"); + UiUtils.OpenUrl(requestUri.ToString()); } + public sealed class RootResponse + { + public required string Version { get; set; } + public required string Commit { get; set; } + public required DateTimeOffset CurrentTime { get; set; } + public required Uri FrontendUrl { get; set; } + public required Uri ShortLinkUrl { get; set; } + } + } \ No newline at end of file From 9d8402fb9c300696c1b02876ba52e89670cb3476 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 May 2024 10:01:10 +0200 Subject: [PATCH 88/95] RC 4 --- ShockOsc/ShockOsc.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index b8ffa9e..7ca47e4 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -15,7 +15,7 @@ OpenShock.ShockOsc OpenShock 2.0.0 - 2.0.0-rc.3 + 2.0.0-rc.4 Resources\openshock-icon.ico true ShockOsc From fa647e5fec9cd11c14ff2c0d7156306c2db59606 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 May 2024 21:04:55 +0200 Subject: [PATCH 89/95] Show window on single instance kill --- ShockOsc/Cli/Uri/UriParameterType.cs | 1 + ShockOsc/HeadlessProgram.cs | 3 ++ ShockOsc/MauiProgram.cs | 41 +++++++++++++------ ShockOsc/Platforms/Windows/PipeHelper.cs | 9 ++-- ShockOsc/Platforms/Windows/WindowUtils.cs | 26 ++++++++++++ .../Platforms/Windows/WindowsEntryPoint.cs | 24 ++++++----- ShockOsc/Platforms/Windows/WindowsServices.cs | 3 +- .../Platforms/Windows/WindowsTrayService.cs | 26 +++++------- ShockOsc/Services/Pipes/PipeMessageType.cs | 1 + ShockOsc/Services/Pipes/PipeServerService.cs | 8 ++-- ShockOsc/Services/ShockOsc.cs | 19 +++++---- .../Ui/Pages/Authentication/LoginPart.razor | 5 ++- ShockOsc/Ui/Utils/DebouncedSlider.razor.cs | 2 + 13 files changed, 110 insertions(+), 58 deletions(-) create mode 100644 ShockOsc/Platforms/Windows/WindowUtils.cs diff --git a/ShockOsc/Cli/Uri/UriParameterType.cs b/ShockOsc/Cli/Uri/UriParameterType.cs index d0c4d8c..985f184 100644 --- a/ShockOsc/Cli/Uri/UriParameterType.cs +++ b/ShockOsc/Cli/Uri/UriParameterType.cs @@ -2,5 +2,6 @@ public enum UriParameterType { + Show, Token } \ No newline at end of file diff --git a/ShockOsc/HeadlessProgram.cs b/ShockOsc/HeadlessProgram.cs index be5f6e5..f9acb94 100644 --- a/ShockOsc/HeadlessProgram.cs +++ b/ShockOsc/HeadlessProgram.cs @@ -1,4 +1,7 @@ using Microsoft.Extensions.Hosting; +#if WINDOWS +using OpenShock.ShockOsc.Platforms.Windows; +#endif namespace OpenShock.ShockOsc; diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 7ab4e45..fb1dbae 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -2,13 +2,17 @@ using Microsoft.Maui.LifecycleEvents; using OpenShock.ShockOsc.Config; using MauiApp = OpenShock.ShockOsc.Ui.MauiApp; -using Microsoft.UI; +using OpenShock.ShockOsc.Services.Pipes; +#if WINDOWS +using OpenShock.ShockOsc.Platforms.Windows; +#endif namespace OpenShock.ShockOsc; public static class MauiProgram { private static ShockOscConfig? _config; + private static PipeServerService? _pipeServerService; public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() { @@ -17,20 +21,28 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() // <---- Services ----> builder.Services.AddShockOscServices(); - + #if WINDOWS builder.Services.AddWindowsServices(); - + builder.ConfigureLifecycleEvents(lifecycleBuilder => { lifecycleBuilder.AddWindows(windowsLifecycleBuilder => { windowsLifecycleBuilder.OnWindowCreated(window => { - var handle = WinRT.Interop.WindowNative.GetWindowHandle(window); - var id = Win32Interop.GetWindowIdFromWindow(handle); - var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(id); - + var appWindow = WindowUtils.GetAppWindow(window); + + if (_pipeServerService != null) + { + _pipeServerService.OnMessageReceived += () => + { + appWindow.ShowOnTop(); + + return Task.CompletedTask; + }; + } + //When user execute the closing method, we can push a display alert. If user click Yes, close this application, if click the cancel, display alert will dismiss. appWindow.Closing += async (s, e) => { @@ -41,32 +53,35 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() appWindow.Hide(); return; } + + if(Application.Current == null) return; - var result = await Application.Current.MainPage.DisplayAlert( + var result = await Application.Current.MainPage!.DisplayAlert( "Close?", "Do you want to close ShockOSC?", "Yes", "Cancel"); - if (result) Application.Current.Quit(); + if (result) Application.Current?.Quit(); }; }); }); }); #endif - + // <---- App ----> builder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); - + var app = builder.Build(); - + _config = app.Services.GetRequiredService().Config; + _pipeServerService = app.Services.GetRequiredService(); app.Services.StartShockOscServices(false); - + return app; } } diff --git a/ShockOsc/Platforms/Windows/PipeHelper.cs b/ShockOsc/Platforms/Windows/PipeHelper.cs index 4cf5606..0da0ede 100644 --- a/ShockOsc/Platforms/Windows/PipeHelper.cs +++ b/ShockOsc/Platforms/Windows/PipeHelper.cs @@ -1,6 +1,8 @@ -using System.Collections; +#if WINDOWS +using System.Collections; -namespace OpenShock.ShockOsc; +// ReSharper disable once CheckNamespace +namespace OpenShock.ShockOsc.Platforms.Windows; public static class PipeHelper { @@ -29,4 +31,5 @@ bool MoveNextSafe(IEnumerator enumerator) { } } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowUtils.cs b/ShockOsc/Platforms/Windows/WindowUtils.cs new file mode 100644 index 0000000..42173c7 --- /dev/null +++ b/ShockOsc/Platforms/Windows/WindowUtils.cs @@ -0,0 +1,26 @@ +#if WINDOWS +using Microsoft.UI; +using Microsoft.UI.Windowing; + +// ReSharper disable once CheckNamespace +namespace OpenShock.ShockOsc.Platforms.Windows; + +public static class WindowUtils +{ + public static void ShowOnTop(this AppWindow appWindow) + { + appWindow.Show(); + + if (appWindow.Presenter is not OverlappedPresenter presenter) return; + presenter.IsAlwaysOnTop = true; + presenter.IsAlwaysOnTop = false; + } + + public static AppWindow GetAppWindow(object window) + { + var handle = WinRT.Interop.WindowNative.GetWindowHandle(window); + var id = Win32Interop.GetWindowIdFromWindow(handle); + return AppWindow.GetFromWindowId(id); + } +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs index 690cfeb..f0f7cad 100644 --- a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs +++ b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs @@ -1,12 +1,10 @@ #if WINDOWS -using System.Diagnostics; using System.IO.Pipes; using System.Runtime.InteropServices; using System.Text.Json; using CommandLine; using Microsoft.Extensions.Hosting; using Microsoft.UI.Dispatching; -using Microsoft.Windows.AppLifecycle; using OpenShock.ShockOsc.Cli; using OpenShock.ShockOsc.Cli.Uri; using OpenShock.ShockOsc.Services; @@ -16,6 +14,7 @@ using Application = Microsoft.UI.Xaml.Application; using UriParser = OpenShock.ShockOsc.Cli.Uri.UriParser; +// ReSharper disable once CheckNamespace namespace OpenShock.ShockOsc.Platforms.Windows; public static class WindowsEntryPoint @@ -54,7 +53,7 @@ private static void Start(CliOptions config) } const string pipeName = @"\\.\pipe\OpenShock.ShockOSC"; - + if (PipeHelper.EnumeratePipes().Any(x => x.Equals(pipeName, StringComparison.InvariantCultureIgnoreCase))) { // TODO: Refactor this @@ -63,19 +62,22 @@ private static void Start(CliOptions config) using var pipeClientStream = new NamedPipeClientStream(".", "OpenShock.ShockOsc", PipeDirection.Out); pipeClientStream.Connect(500); + var parsedUri = UriParser.Parse(config.Uri); + using var writer = new StreamWriter(pipeClientStream); writer.AutoFlush = true; - var parsedUri = UriParser.Parse(config.Uri); - - if (parsedUri.Type == UriParameterType.Token) + var pipeMessage = parsedUri.Type switch { - writer.WriteLine(JsonSerializer.Serialize(new PipeMessage + UriParameterType.Show => new PipeMessage { Type = PipeMessageType.Show }, + UriParameterType.Token => new PipeMessage { - Type = PipeMessageType.Token, - Data = string.Join('/', parsedUri.Arguments) - })); - } + Type = PipeMessageType.Token, Data = string.Join('/', parsedUri.Arguments) + }, + _ => null + }; + + if (pipeMessage != null) writer.WriteLine(JsonSerializer.Serialize(pipeMessage)); return; } diff --git a/ShockOsc/Platforms/Windows/WindowsServices.cs b/ShockOsc/Platforms/Windows/WindowsServices.cs index e1abe29..5380771 100644 --- a/ShockOsc/Platforms/Windows/WindowsServices.cs +++ b/ShockOsc/Platforms/Windows/WindowsServices.cs @@ -1,7 +1,8 @@ #if WINDOWS using OpenShock.ShockOsc.Services; -namespace OpenShock.ShockOsc; +// ReSharper disable once CheckNamespace +namespace OpenShock.ShockOsc.Platforms.Windows; public static class WindowsServices { diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index 091c6e7..0a59cb1 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -2,20 +2,22 @@ using System.Drawing; using System.Windows.Forms; -using Microsoft.UI; -using Microsoft.UI.Windowing; using OpenShock.SDK.CSharp.Hub; using OpenShock.ShockOsc.Services; using Application = Microsoft.Maui.Controls.Application; -using Color = System.Drawing.Color; using Image = System.Drawing.Image; -namespace OpenShock.ShockOsc; +// ReSharper disable once CheckNamespace +namespace OpenShock.ShockOsc.Platforms.Windows; public class WindowsTrayService : ITrayService { private readonly OpenShockHubClient _apiHubClient; + /// + /// Windows Tray Service + /// + /// public WindowsTrayService(OpenShockHubClient apiHubClient) { _apiHubClient = apiHubClient; @@ -61,7 +63,7 @@ public void Initialize() tray.Visible = true; } - private void Restart(object? sender, EventArgs e) + private static void Restart(object? sender, EventArgs e) { Application.Current?.Quit(); } @@ -73,18 +75,10 @@ private static void OnMainClick(object? sender, EventArgs eventArgs) var window = Application.Current?.Windows[0]; var nativeWindow = window?.Handler?.PlatformView; if (nativeWindow == null) return; - - var windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow); - var windowId = Win32Interop.GetWindowIdFromWindow(windowHandle); - var appWindow = AppWindow.GetFromWindowId(windowId); - appWindow.Show(); - - if (appWindow.Presenter is OverlappedPresenter presenter) - { - presenter.IsAlwaysOnTop = true; - presenter.IsAlwaysOnTop = false; - } + var appWindow = WindowUtils.GetAppWindow(nativeWindow); + + appWindow.ShowOnTop(); } private static void OnQuitClick(object? sender, EventArgs eventArgs) diff --git a/ShockOsc/Services/Pipes/PipeMessageType.cs b/ShockOsc/Services/Pipes/PipeMessageType.cs index e99a79b..5e2ba13 100644 --- a/ShockOsc/Services/Pipes/PipeMessageType.cs +++ b/ShockOsc/Services/Pipes/PipeMessageType.cs @@ -2,5 +2,6 @@ public enum PipeMessageType { + Show, Token } \ No newline at end of file diff --git a/ShockOsc/Services/Pipes/PipeServerService.cs b/ShockOsc/Services/Pipes/PipeServerService.cs index a5b5935..eae0192 100644 --- a/ShockOsc/Services/Pipes/PipeServerService.cs +++ b/ShockOsc/Services/Pipes/PipeServerService.cs @@ -66,11 +66,11 @@ private async Task ServerLoop() case PipeMessageType.Token: Token = jsonObj.Data?.ToString(); break; + case PipeMessageType.Show: + default: + break; } - { - - } - + await OnMessageReceived.Raise(); _logger.LogInformation("[{Id}], Received pipe message of type: {Type}", id, jsonObj.Type); } diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index edfe912..6c00e69 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -70,14 +70,17 @@ public ShockOsc(ILogger logger, _dataLayer = dataLayer; _oscHandler = oscHandler; _liveControlManager = liveControlManager; - - - OnGroupsChanged += SetupGroups; + + OnGroupsChanged += () => + { + SetupGroups(); + return Task.CompletedTask; + }; oscQueryServer.FoundVrcClient += FoundVrcClient; oscQueryServer.ParameterUpdate += OnAvatarChange; - - SetupGroups().Wait(); + + SetupGroups(); if (!_configManager.Config.Osc.OscQuery) { @@ -87,7 +90,7 @@ public ShockOsc(ILogger logger, _logger.LogInformation("Started ShockOsc.cs"); } - private async Task SetupGroups() + private void SetupGroups() { _dataLayer.ProgramGroups.Clear(); _dataLayer.ProgramGroups[Guid.Empty] = new ProgramGroup(Guid.Empty, "_All", _oscClient, null); @@ -101,7 +104,7 @@ private void OnParamChange(bool shockOscParam) OnParamsChange?.Invoke(shockOscParam); } - public async Task FoundVrcClient(IPEndPoint? oscClient) + private async Task FoundVrcClient(IPEndPoint? oscClient) { _logger.LogInformation("Found VRC client"); // stop tasks @@ -129,7 +132,7 @@ public async Task FoundVrcClient(IPEndPoint? oscClient) _logger.LogInformation("Ready"); OsTask.Run(_underscoreConfig.SendUpdateForAll); - _oscClient.SendChatboxMessage($"{_configManager.Config.Chatbox.Prefix} Game Connected"); + await _oscClient.SendChatboxMessage($"{_configManager.Config.Chatbox.Prefix} Game Connected"); } public async Task OnAvatarChange(Dictionary parameters, string avatarId) diff --git a/ShockOsc/Ui/Pages/Authentication/LoginPart.razor b/ShockOsc/Ui/Pages/Authentication/LoginPart.razor index b524a31..821ad64 100644 --- a/ShockOsc/Ui/Pages/Authentication/LoginPart.razor +++ b/ShockOsc/Ui/Pages/Authentication/LoginPart.razor @@ -79,7 +79,7 @@ private string? _server = null; - [Parameter] public Func ProceedAuthenticated { get; set; } + [Parameter] public required Func ProceedAuthenticated { get; set; } public async Task Login() { @@ -141,7 +141,8 @@ BackendServer.Production => _productionServer, BackendServer.Staging => _stagingServer, BackendServer.Custom => _customServerUri, - }; + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) + } ?? throw new InvalidOperationException(); } private struct WrongSchema; diff --git a/ShockOsc/Ui/Utils/DebouncedSlider.razor.cs b/ShockOsc/Ui/Utils/DebouncedSlider.razor.cs index 678454f..0a900a0 100644 --- a/ShockOsc/Ui/Utils/DebouncedSlider.razor.cs +++ b/ShockOsc/Ui/Utils/DebouncedSlider.razor.cs @@ -37,7 +37,9 @@ protected override void OnInitialized() private T _sliderValue = default!; [Parameter] +#pragma warning disable BL0007 public T SliderValue +#pragma warning restore BL0007 { get => _sliderValue; set From a22af46767d41c78528c37708489934177b94625 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 May 2024 23:38:41 +0200 Subject: [PATCH 90/95] Fix URI parser --- ShockOsc/Cli/Uri/UriParser.cs | 10 ++++++++-- ShockOsc/Properties/launchSettings.json | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ShockOsc/Cli/Uri/UriParser.cs b/ShockOsc/Cli/Uri/UriParser.cs index 64fe6b3..a958314 100644 --- a/ShockOsc/Cli/Uri/UriParser.cs +++ b/ShockOsc/Cli/Uri/UriParser.cs @@ -6,12 +6,18 @@ public static UriParameter Parse(string uri) { ReadOnlySpan uriSpan = uri; var dePrefixed = uriSpan[9..]; - var type = dePrefixed[..dePrefixed.IndexOf('/')]; + + var getEnd = dePrefixed.IndexOf('/'); + if(getEnd == -1) getEnd = dePrefixed.Length; + + var type = dePrefixed[..getEnd]; + var hasArgumentLength = dePrefixed.Length > type.Length + 1; + return new UriParameter { Type = Enum.Parse(type, true), - Arguments = dePrefixed[(type.Length + 1)..].ToString().Split('/') + Arguments = hasArgumentLength ? dePrefixed[(type.Length + 1)..].ToString().Split('/') : [] }; } } \ No newline at end of file diff --git a/ShockOsc/Properties/launchSettings.json b/ShockOsc/Properties/launchSettings.json index 36a0c0e..dc75cdc 100644 --- a/ShockOsc/Properties/launchSettings.json +++ b/ShockOsc/Properties/launchSettings.json @@ -8,6 +8,11 @@ "commandName": "Project", "nativeDebugging": false, "commandLineArgs": "--headless" + }, + "ShockOscUriShow": { + "commandName": "Project", + "nativeDebugging": false, + "commandLineArgs": "--uri=shockosc:show" } } } From c0c2a1a372b1abfc20d83194c9b4c10298bd03b9 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 May 2024 23:38:59 +0200 Subject: [PATCH 91/95] Fix physbone pull fixed intensity --- ShockOsc/Services/OscHandler.cs | 4 +- ShockOsc/Services/ShockOsc.cs | 75 ++++++++++++++++----------- ShockOsc/Services/UnderscoreConfig.cs | 26 +++++----- ShockOsc/Utils/MathUtils.cs | 2 +- 4 files changed, 61 insertions(+), 46 deletions(-) diff --git a/ShockOsc/Services/OscHandler.cs b/ShockOsc/Services/OscHandler.cs index be004ef..0a8392b 100644 --- a/ShockOsc/Services/OscHandler.cs +++ b/ShockOsc/Services/OscHandler.cs @@ -84,11 +84,11 @@ public async Task SendParams() if (!isActiveOrOnCooldown && shocker.LastIntensity > 0) shocker.LastIntensity = 0; - var intensity = MathUtils.ClampFloat(shocker.LastIntensity / 100f); + var intensity = MathUtils.Saturate(shocker.LastIntensity / 100f); var onCoolDown = !isActive && isActiveOrOnCooldown; var cooldownPercentage = 0f; if (onCoolDown) - cooldownPercentage = MathUtils.ClampFloat(1 - + cooldownPercentage = MathUtils.Saturate(1 - (float)(DateTime.UtcNow - shocker.LastExecuted.AddMilliseconds(shocker.LastDuration)) .TotalMilliseconds / diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index 6c00e69..72819b4 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -402,7 +402,7 @@ private async Task InstantShock(ProgramGroup programGroup, uint duration, byte i { programGroup.LastExecuted = DateTime.UtcNow; programGroup.LastDuration = duration; - var intensityPercentage = MathF.Round(MathUtils.ClampFloat(intensity) * 100f); + var intensityPercentage = MathF.Round(MathUtils.Saturate(intensity) * 100f); programGroup.LastIntensity = intensity; _oscHandler.ForceUnmute(); @@ -447,29 +447,7 @@ private async Task CheckLoop() await Task.Delay(20); } } - - private byte GetIntensity(ProgramGroup programGroup) - { - if (programGroup.ConfigGroup is not { OverrideDuration: true }) - { - // Use global config - var config = _configManager.Config.Behaviour; - - if (!config.RandomIntensity) return config.FixedIntensity; - var rir = config.IntensityRange; - var intensityValue = Random.Next((int)rir.Min, (int)rir.Max); - return (byte)intensityValue; - } - - // Use groupConfig - var groupConfig = programGroup.ConfigGroup; - - if (!groupConfig.RandomIntensity) return groupConfig.FixedIntensity; - var groupRir = groupConfig.IntensityRange; - var groupIntensityValue = Random.Next((int)groupRir.Min, (int)groupRir.Max); - return (byte)groupIntensityValue; - } - + private async Task CheckLogic() { var config = _configManager.Config.Behaviour; @@ -533,12 +511,7 @@ private async Task CheckLogic() if (programGroup.TriggerMethod == TriggerMethod.PhysBoneRelease) { - intensity = programGroup.ConfigGroup is { OverrideIntensity: true } - ? (byte)MathUtils.LerpFloat(programGroup.ConfigGroup.IntensityRange.Min, - programGroup.ConfigGroup.IntensityRange.Max, - programGroup.LastStretchValue) - : (byte)MathUtils.LerpFloat(config.IntensityRange.Min, config.IntensityRange.Max, - programGroup.LastStretchValue); + intensity = GetPhysbonePullIntensity(programGroup, programGroup.LastStretchValue); programGroup.LastStretchValue = 0; exclusive = true; @@ -549,6 +522,25 @@ private async Task CheckLogic() } } + private byte GetPhysbonePullIntensity(ProgramGroup programGroup, float stretch) + { + stretch = MathUtils.Saturate(stretch); + if (programGroup.ConfigGroup is not { OverrideIntensity: true }) + { + // Use global config + var config = _configManager.Config.Behaviour; + + if (!config.RandomIntensity) return config.FixedIntensity; + return (byte)MathUtils.LerpFloat(config.IntensityRange.Min, config.IntensityRange.Max, stretch); + } + + // Use group config + var groupConfig = programGroup.ConfigGroup; + + if (!groupConfig.RandomIntensity) return groupConfig.FixedIntensity; + return (byte)MathUtils.LerpFloat(groupConfig.IntensityRange.Min, groupConfig.IntensityRange.Max, stretch); + } + private uint GetDuration(ProgramGroup programGroup) { if (programGroup.ConfigGroup is not { OverrideDuration: true }) @@ -570,5 +562,28 @@ private uint GetDuration(ProgramGroup programGroup) return (uint)(Random.Next((int)(groupRdr.Min / groupConfig.RandomDurationStep), (int)(groupRdr.Max / groupConfig.RandomDurationStep)) * groupConfig.RandomDurationStep); } + + private byte GetIntensity(ProgramGroup programGroup) + { + if (programGroup.ConfigGroup is not { OverrideDuration: true }) + { + // Use global config + var config = _configManager.Config.Behaviour; + + if (!config.RandomIntensity) return config.FixedIntensity; + var rir = config.IntensityRange; + var intensityValue = Random.Next((int)rir.Min, (int)rir.Max); + return (byte)intensityValue; + } + + // Use groupConfig + var groupConfig = programGroup.ConfigGroup; + + if (!groupConfig.RandomIntensity) return groupConfig.FixedIntensity; + var groupRir = groupConfig.IntensityRange; + var groupIntensityValue = Random.Next((int)groupRir.Min, (int)groupRir.Max); + return (byte)groupIntensityValue; + } + } \ No newline at end of file diff --git a/ShockOsc/Services/UnderscoreConfig.cs b/ShockOsc/Services/UnderscoreConfig.cs index 31bc0d1..491b8fc 100644 --- a/ShockOsc/Services/UnderscoreConfig.cs +++ b/ShockOsc/Services/UnderscoreConfig.cs @@ -105,7 +105,7 @@ private void HandleGlobalConfigCommand(string action, object? value) if (value is float intensityFloat) { var currentIntensity = - MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedIntensity / 100f); + MathUtils.Saturate(_configManager.Config.Behaviour.FixedIntensity / 100f); if (Math.Abs(intensityFloat - currentIntensity) < 0.001) return; _configManager.Config.Behaviour.FixedIntensity = @@ -122,7 +122,7 @@ private void HandleGlobalConfigCommand(string action, object? value) if (value is float minIntensityFloat) { var currentMinIntensity = - MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f); + MathUtils.Saturate(_configManager.Config.Behaviour.IntensityRange.Min / 100f); if (Math.Abs(minIntensityFloat - currentMinIntensity) < 0.001) return; _configManager.Config.Behaviour.IntensityRange.Min = @@ -139,7 +139,7 @@ private void HandleGlobalConfigCommand(string action, object? value) if (value is float maxIntensityFloat) { var currentMaxIntensity = - MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f); + MathUtils.Saturate(_configManager.Config.Behaviour.IntensityRange.Max / 100f); if (Math.Abs(maxIntensityFloat - currentMaxIntensity) < 0.001) return; _configManager.Config.Behaviour.IntensityRange.Max = @@ -204,7 +204,7 @@ private void HandleGlobalConfigCommand(string action, object? value) if (value is float cooldownTimeFloat) { var currentCooldownTime = - MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f); + MathUtils.Saturate(_configManager.Config.Behaviour.CooldownTime / 100000f); if (Math.Abs(cooldownTimeFloat - currentCooldownTime) < 0.001) return; _configManager.Config.Behaviour.CooldownTime = @@ -220,7 +220,7 @@ private void HandleGlobalConfigCommand(string action, object? value) // 0..1sec if (value is float holdTimeFloat) { - var currentHoldTime = MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f); + var currentHoldTime = MathUtils.Saturate(_configManager.Config.Behaviour.HoldTime / 1000f); if (Math.Abs(holdTimeFloat - currentHoldTime) < 0.001) return; _configManager.Config.Behaviour.HoldTime = @@ -267,24 +267,24 @@ public async Task SendUpdateForAll() await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/Paused", KillSwitch); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Paused", KillSwitch); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinIntensity", - MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Min / 100f)); + MathUtils.Saturate(_configManager.Config.Behaviour.IntensityRange.Min / 100f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxIntensity", - MathUtils.ClampFloat(_configManager.Config.Behaviour.IntensityRange.Max / 100f)); + MathUtils.Saturate(_configManager.Config.Behaviour.IntensityRange.Max / 100f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Duration", - MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedDuration / 10000f)); + MathUtils.Saturate(_configManager.Config.Behaviour.FixedDuration / 10000f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/CooldownTime", - MathUtils.ClampFloat(_configManager.Config.Behaviour.CooldownTime / 100000f)); + MathUtils.Saturate(_configManager.Config.Behaviour.CooldownTime / 100000f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/HoldTime", - MathUtils.ClampFloat(_configManager.Config.Behaviour.HoldTime / 1000f)); + MathUtils.Saturate(_configManager.Config.Behaviour.HoldTime / 1000f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/ModeIntensity", _configManager.Config.Behaviour.RandomIntensity); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/ModeDuration", _configManager.Config.Behaviour.RandomDuration); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/Intensity", - MathUtils.ClampFloat(_configManager.Config.Behaviour.FixedIntensity / 100f)); + MathUtils.Saturate(_configManager.Config.Behaviour.FixedIntensity / 100f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MinDuration", - MathUtils.ClampFloat(_configManager.Config.Behaviour.DurationRange.Min / 10_000f)); + MathUtils.Saturate(_configManager.Config.Behaviour.DurationRange.Min / 10_000f)); await _oscClient.SendGameMessage("/avatar/parameters/ShockOsc/_Config/_All/MaxDuration", - MathUtils.ClampFloat(_configManager.Config.Behaviour.DurationRange.Max / 10_000f)); + MathUtils.Saturate(_configManager.Config.Behaviour.DurationRange.Max / 10_000f)); } } \ No newline at end of file diff --git a/ShockOsc/Utils/MathUtils.cs b/ShockOsc/Utils/MathUtils.cs index aa5f6fa..69f2edc 100644 --- a/ShockOsc/Utils/MathUtils.cs +++ b/ShockOsc/Utils/MathUtils.cs @@ -3,7 +3,7 @@ public static class MathUtils { public static float LerpFloat(float min, float max, float t) => min + (max - min) * t; - public static float ClampFloat(float value) => value < 0 ? 0 : value > 1 ? 1 : value; + public static float Saturate(float value) => value < 0 ? 0 : value > 1 ? 1 : value; public static uint LerpUint(uint min, uint max, float t) => (uint)(min + (max - min) * t); public static uint ClampUint(uint value, uint min, uint max) => value < min ? min : value > max ? max : value; } \ No newline at end of file From e9070a0081d2bcbb4c3b643102266a8061a1eb80 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 May 2024 23:41:55 +0200 Subject: [PATCH 92/95] RC 5 --- ShockOsc/ShockOsc.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 7ca47e4..25aab2f 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -15,7 +15,7 @@ OpenShock.ShockOsc OpenShock 2.0.0 - 2.0.0-rc.4 + 2.0.0-rc.5 Resources\openshock-icon.ico true ShockOsc From 9c3a156b87bd6371db9d779f0eed28cb2466a444 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 11 May 2024 00:32:12 +0200 Subject: [PATCH 93/95] Updater handle latest release being able to be newer than pre release --- ShockOsc/Services/Updater.cs | 53 ++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/ShockOsc/Services/Updater.cs b/ShockOsc/Services/Updater.cs index b1064db..5d82ea6 100644 --- a/ShockOsc/Services/Updater.cs +++ b/ShockOsc/Services/Updater.cs @@ -77,22 +77,16 @@ private static bool TryDeleteFile(string fileName) return null; } - if (!SemVersion.TryParse(release.TagName, SemVersionStyles.AllowV, out var version)) - { - _logger.LogWarning("Failed to parse version. Value: {Version}", release.TagName); - return null; - } - - var asset = release.Assets.FirstOrDefault(x => + var asset = release.Value.Item1.Assets.FirstOrDefault(x => x.Name.Equals(SetupFileName, StringComparison.InvariantCultureIgnoreCase)); if (asset == null) { _logger.LogWarning("Could not find asset with {@SetupName}. Assets found: {@Assets}", SetupFileName, - release.Assets); + release.Value.Item1.Assets); return null; } - return (version, asset); + return (release.Value.Item2, asset); } catch (Exception e) { @@ -101,7 +95,7 @@ private static bool TryDeleteFile(string fileName) } } - private async Task GetPreRelease() + private async Task<(GithubReleaseResponse, SemVersion)?> GetPreRelease() { using var res = await HttpClient.GetAsync(GithubReleasesUrl); if (!res.IsSuccessStatusCode) @@ -133,12 +127,37 @@ await JsonSerializer.DeserializeAsync>( listOfValid.Add((release, version)); } - var newestPreRelease = listOfValid.OrderByDescending(x => x.Item2); + (GithubReleaseResponse, SemVersion)? newestPreRelease = listOfValid.OrderByDescending(x => x.Item2).FirstOrDefault(); + var latestRelease = await GetLatestRelease(); + if (newestPreRelease == null && latestRelease == null) + { + _logger.LogWarning("Could not find any valid pre-releases or releases"); + return null; + } - return newestPreRelease.FirstOrDefault().Item1; + if (newestPreRelease == null) + { + _logger.LogWarning("Could not find any valid pre-releases, using latest release"); + return latestRelease; + } + + if (latestRelease == null) + { + _logger.LogWarning("Could not find any valid releases, using latest pre-release without comparing to latest release"); + return newestPreRelease; + } + + + if (latestRelease.Value.Item2.ComparePrecedenceTo(newestPreRelease.Value.Item2) >= 0) + { + _logger.LogInformation("Latest release is newer than or the same as latest pre-release. Using latest release"); + return latestRelease; + } + + return newestPreRelease; } - private async Task GetLatestRelease() + private async Task<(GithubReleaseResponse, SemVersion)?> GetLatestRelease() { using var res = await HttpClient.GetAsync(GithubLatest); if (!res.IsSuccessStatusCode) @@ -155,8 +174,14 @@ await JsonSerializer.DeserializeAsync>( _logger.LogWarning("Could not deserialize json"); return null; } + + if (!SemVersion.TryParse(json.TagName, SemVersionStyles.AllowV, out var version)) + { + _logger.LogWarning("Failed to parse version. Value: {Version}", json.TagName); + return null; + } - return json; + return (json, version); } private readonly SemaphoreSlim _updateLock = new(1, 1); From e1d377e05de8f9b85656f18ddef071b92c95fe45 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 11 May 2024 00:40:20 +0200 Subject: [PATCH 94/95] Disable discord preview by default --- ShockOsc/Config/AppConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShockOsc/Config/AppConfig.cs b/ShockOsc/Config/AppConfig.cs index 152f37b..d89464f 100644 --- a/ShockOsc/Config/AppConfig.cs +++ b/ShockOsc/Config/AppConfig.cs @@ -5,7 +5,7 @@ namespace OpenShock.ShockOsc.Config; public sealed class AppConfig { public bool CloseToTray { get; set; } = true; - public bool DiscordPreview { get; set; } = true; + public bool DiscordPreview { get; set; } = false; public UpdateChannel UpdateChannel { get; set; } = UpdateChannel.Release; public SemVersion? LastIgnoredVersion { get; set; } = null; From 4095507753af73d395522c603f8e2f587a28297b Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 11 May 2024 00:41:10 +0200 Subject: [PATCH 95/95] Version 2.0.0 Release --- ShockOsc/ShockOsc.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 25aab2f..9891d5e 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -15,7 +15,7 @@ OpenShock.ShockOsc OpenShock 2.0.0 - 2.0.0-rc.5 + 2.0.0 Resources\openshock-icon.ico true ShockOsc