diff --git a/MigrationInstaller/MigrationInstaller.csproj b/MigrationInstaller/MigrationInstaller.csproj index 0e058c0..17c628a 100644 --- a/MigrationInstaller/MigrationInstaller.csproj +++ b/MigrationInstaller/MigrationInstaller.csproj @@ -2,7 +2,7 @@ Exe - net9.0-windows + net10.0-windows enable enable true diff --git a/ShockOsc/Config/AvatarParameterAction.cs b/ShockOsc/Config/AvatarParameterAction.cs new file mode 100644 index 0000000..2c364dc --- /dev/null +++ b/ShockOsc/Config/AvatarParameterAction.cs @@ -0,0 +1,40 @@ +using OpenShock.Desktop.ModuleBase.Models; + +namespace OpenShock.ShockOSC.Config; + +public sealed class AvatarParameterAction +{ + public required string ParameterName { get; set; } + public ControlType Action { get; set; } = ControlType.Shock; + public Guid GroupId { get; set; } + public ParameterTriggerKind TriggerKind { get; set; } = ParameterTriggerKind.OnTrue; + public float Threshold { get; set; } = 0.5f; + public byte? OverrideIntensity { get; set; } + public ushort? OverrideDuration { get; set; } + public bool IsLiveControl { get; set; } + public float LiveControlMin { get; set; } + public float LiveControlMax { get; set; } = 1f; +} + +public enum ParameterTriggerKind +{ + /// + /// Triggers when a bool parameter becomes true + /// + OnTrue, + + /// + /// Triggers when a bool parameter becomes false + /// + OnFalse, + + /// + /// Triggers when a float parameter exceeds the threshold + /// + Threshold, + + /// + /// Triggers on any value change (non-default) + /// + OnChange +} diff --git a/ShockOsc/Config/BehaviourConf.cs b/ShockOsc/Config/BehaviourConf.cs index a3f95d7..8a617cf 100644 --- a/ShockOsc/Config/BehaviourConf.cs +++ b/ShockOsc/Config/BehaviourConf.cs @@ -5,4 +5,5 @@ public sealed class BehaviourConf : SharedBehaviourConfig public uint HoldTime { get; set; } = 250; public bool DisableWhileAfk { get; set; } = true; public bool ForceUnmute { get; set; } + public uint CheckLoopIntervalMs { get; set; } = 50; } \ No newline at end of file diff --git a/ShockOsc/Config/ShockOscConfig.cs b/ShockOsc/Config/ShockOscConfig.cs index 82729aa..7d97753 100644 --- a/ShockOsc/Config/ShockOscConfig.cs +++ b/ShockOsc/Config/ShockOscConfig.cs @@ -9,6 +9,12 @@ public sealed class ShockOscConfig public ChatboxConf Chatbox { get; set; } = new(); public IDictionary Groups { get; set; } = new Dictionary(); + /// + /// Custom parameter-to-action mappings, keyed by avatar ID + /// + public IDictionary> AvatarParameterActions { get; set; } = + new Dictionary>(); + public T GetGroupOrGlobal(ProgramGroup group, Func selector, Func groupOverrideSelector) { diff --git a/ShockOsc/Services/OscHandler.cs b/ShockOsc/Services/OscHandler.cs index f78b0bf..6fa252d 100644 --- a/ShockOsc/Services/OscHandler.cs +++ b/ShockOsc/Services/OscHandler.cs @@ -84,9 +84,13 @@ public async Task SendParams() foreach (var shocker in _shockOscData.ProgramGroups.Values) { + var cooldownTime = _moduleConfig.Config.Behaviour.CooldownTime; + if (shocker.ConfigGroup is { OverrideCooldownTime: true }) + cooldownTime = shocker.ConfigGroup.CooldownTime; + var isActive = shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; var isActiveOrOnCooldown = - shocker.LastExecuted.AddMilliseconds(_moduleConfig.Config.Behaviour.CooldownTime) + shocker.LastExecuted.AddMilliseconds(cooldownTime) .AddMilliseconds(shocker.LastDuration) > DateTime.UtcNow; if (!isActiveOrOnCooldown && shocker.LastIntensity > 0) shocker.LastIntensity = 0; @@ -100,7 +104,7 @@ public async Task SendParams() shocker.LastExecuted.AddMilliseconds( shocker.LastDuration)) .TotalMilliseconds / - _moduleConfig.Config.Behaviour.CooldownTime); + cooldownTime); await shocker.ParamActive.SetValue(isActive); await shocker.ParamCooldown.SetValue(onCoolDown); diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index bd9ce4e..566f204 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -32,8 +32,13 @@ public sealed class ShockOsc private readonly OscHandler _oscHandler; private readonly ChatboxService _chatboxService; - private bool _oscServerActive; + private CancellationTokenSource _loopCts = new(); + private Task[] _loopTasks = []; private bool _isAfk; + public bool IsGameConnected { get; private set; } + public bool IsConnectedViaOscQuery { get; private set; } + public event Action? OnGameConnectionChanged; + public event Action? OnAvatarActionTriggered; public string AvatarId = string.Empty; private readonly Random Random = new(); @@ -61,6 +66,11 @@ public sealed class ShockOsc public readonly Dictionary ShockOscParams = new(); public readonly Dictionary AllAvatarParams = new(); + /// + /// Tracks previous values of avatar parameters for custom action trigger detection + /// + private readonly Dictionary _previousParamValues = new(); + public IObservable OnParamsChangeObservable => _onParamsChange; private readonly Subject _onParamsChange = new(); @@ -113,9 +123,16 @@ private void SetupGroups() private async Task SetupVrcClient((OscQueryServer, IPEndPoint)? client) { - // stop tasks - _oscServerActive = false; - await Task.Delay(1000); // wait for tasks to stop TODO: REWORK THIS + // Stop existing loops + await _loopCts.CancelAsync(); + try { await Task.WhenAll(_loopTasks); } + catch (OperationCanceledException) { } + _loopCts.Dispose(); + _loopCts = new CancellationTokenSource(); + + IsGameConnected = false; + IsConnectedViaOscQuery = false; + OnGameConnectionChanged?.Invoke(); if (client != null) { @@ -133,10 +150,17 @@ private async Task SetupVrcClient((OscQueryServer, IPEndPoint)? client) _logger.LogInformation("Connecting UDP Clients..."); // Start tasks - _oscServerActive = true; - OsTask.Run(ReceiverLoopAsync); - OsTask.Run(SenderLoopAsync); - OsTask.Run(CheckLoop); + var ct = _loopCts.Token; + _loopTasks = + [ + Task.Run(() => ReceiverLoopAsync(ct), ct), + Task.Run(() => SenderLoopAsync(ct), ct), + Task.Run(() => CheckLoop(ct), ct) + ]; + + IsGameConnected = true; + IsConnectedViaOscQuery = client != null; + OnGameConnectionChanged?.Invoke(); _logger.LogInformation("Ready"); OsTask.Run(_underscoreConfig.SendUpdateForAll); @@ -159,6 +183,7 @@ private Task OnAvatarChange(OscQueryServer.ParameterUpdateArgs parameterUpdateAr ShockOscParams.Clear(); AllAvatarParams.Clear(); + _previousParamValues.Clear(); foreach (var param in parameters.Keys) { @@ -201,46 +226,120 @@ private Task OnAvatarChange(OscQueryServer.ParameterUpdateArgs parameterUpdateAr return Task.CompletedTask; } - private async Task ReceiverLoopAsync() + private void CheckCustomParameterAction(string paramName, object? oldValue, object? newValue) { - while (_oscServerActive) + if (string.IsNullOrEmpty(AvatarId)) return; + if (!_moduleConfig.Config.AvatarParameterActions.TryGetValue(AvatarId, out var actions)) return; + + foreach (var action in actions) { - try + if (action.ParameterName != paramName) continue; + + if (!_dataLayer.ProgramGroups.TryGetValue(action.GroupId, out var programGroup)) { - await ReceiveLogic(); + _logger.LogWarning("Custom parameter action references unknown group {GroupId}", action.GroupId); + continue; } - catch (Exception e) + + if (action.IsLiveControl) { - _logger.LogError(e, "Error in receiver loop"); + var rawValue = newValue switch + { + float f => f, + int i => i, + true => action.LiveControlMax, + _ => action.LiveControlMin + }; + + var range = action.LiveControlMax - action.LiveControlMin; + var liveIntensity = range == 0f + ? 0f + : MathUtils.Saturate((rawValue - action.LiveControlMin) / range); + + var scaledIntensity = Convert.ToByte(liveIntensity * 100f); + if (action.OverrideIntensity.HasValue && scaledIntensity > 0) + scaledIntensity = GetScaledIntensity(programGroup, scaledIntensity); + + programGroup.ConcurrentIntensity = scaledIntensity; + programGroup.ConcurrentType = scaledIntensity > 0 ? action.Action : ControlType.Stop; + + if (scaledIntensity > 0) + OnAvatarActionTriggered?.Invoke(action); + + continue; + } + + var shouldTrigger = action.TriggerKind switch + { + ParameterTriggerKind.OnTrue => newValue is true && oldValue is not true, + ParameterTriggerKind.OnFalse => newValue is false && oldValue is not false, + ParameterTriggerKind.Threshold => newValue is float f && f >= action.Threshold && + (oldValue is not float oldF || oldF < action.Threshold), + ParameterTriggerKind.OnChange => !Equals(newValue, oldValue) && newValue is not null && + newValue is not false && newValue is not 0 && newValue is not 0f, + _ => false + }; + + if (!shouldTrigger) continue; + + if (!CheckAndSetAllPreconditions(programGroup).IsT0) + { + _logger.LogDebug("Custom parameter action skipped due to preconditions for group {Group}", + programGroup.Name); + continue; } + + var intensity = action.OverrideIntensity ?? GetIntensity(programGroup); + var duration = action.OverrideDuration ?? GetDuration(programGroup); + + _logger.LogInformation( + "Custom parameter action triggered: {Param} -> {Action} on group {Group} (intensity: {Intensity}, duration: {Duration}ms)", + paramName, action.Action, programGroup.Name, intensity, duration); + + OnAvatarActionTriggered?.Invoke(action); + OsTask.Run(() => SendCommand(programGroup, duration, intensity, action.Action)); } - // ReSharper disable once FunctionNeverReturns } - private async Task ReceiveLogic() + private async Task ReceiverLoopAsync(CancellationToken ct) { - OscMessage received; - try - { - received = await _oscClient.ReceiveGameMessage()!; - } - catch (Exception e) + while (!ct.IsCancellationRequested) { - _logger.LogTrace(e, "Error receiving message"); - return; + try + { + var receiveTask = _oscClient.ReceiveGameMessage(); + if (receiveTask == null) break; + await receiveTask.WaitAsync(ct); + await ReceiveLogic(receiveTask.Result); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception e) + { + _logger.LogError(e, "Error in receiver loop"); + } } + } + private async Task ReceiveLogic(OscMessage received) + { var addr = received.Address; if (addr.StartsWith("/avatar/parameters/")) { // FIXME: less alloc pls var fullName = addr[19..]; + var oldValue = AllAvatarParams.GetValueOrDefault(fullName); if (AllAvatarParams.ContainsKey(fullName)) AllAvatarParams[fullName] = received.Arguments[0]; else AllAvatarParams.TryAdd(fullName, received.Arguments[0]); _onParamsChange.OnNext(false); + + // Check custom avatar parameter actions + CheckCustomParameterAction(fullName, oldValue, received.Arguments[0]); } switch (addr) @@ -512,12 +611,12 @@ private ValueTask LogIgnoredAfk() return _chatboxService.SendGenericMessage(_moduleConfig.Config.Chatbox.IgnoredAfk); } - private async Task SenderLoopAsync() + private async Task SenderLoopAsync(CancellationToken ct) { - while (_oscServerActive) + using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(300)); + while (await timer.WaitForNextTickAsync(ct)) { await _oscHandler.SendParams(); - await Task.Delay(300); } } @@ -584,9 +683,11 @@ private async Task SendCommand(ProgramGroup programGroup, ushort duration, byte await _chatboxService.SendLocalControlMessage(programGroup.Name, actualIntensity, actualDuration, type); } - private async Task CheckLoop() + private async Task CheckLoop(CancellationToken ct) { - while (_oscServerActive) + var intervalMs = Math.Max(10, _moduleConfig.Config.Behaviour.CheckLoopIntervalMs); + using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(intervalMs)); + while (await timer.WaitForNextTickAsync(ct)) { try { @@ -596,8 +697,6 @@ private async Task CheckLoop() { _logger.LogError(e, "Error in check loop"); } - - await Task.Delay(20); } } diff --git a/ShockOsc/ShockOSCModule.cs b/ShockOsc/ShockOSCModule.cs index cbf4851..8a97229 100644 --- a/ShockOsc/ShockOSCModule.cs +++ b/ShockOsc/ShockOSCModule.cs @@ -37,6 +37,12 @@ public sealed class ShockOSCModule : DesktopModuleBase, IAsyncDisposable Icon = IconOneOf.FromSvg(Icons.Material.Filled.Group) }, new() + { + Name = "Avatar Actions", + ComponentType = typeof(AvatarActionsTab), + Icon = IconOneOf.FromSvg(Icons.Material.Filled.Tune) + }, + new() { Name = "Chatbox", ComponentType = typeof(ChatboxTab), diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 0a7aafa..83eff9b 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -10,9 +10,10 @@ OpenShock.ShockOSC OpenShock - 3.1.0 - 3.1.0 + 3.2.0 + 3.2.0-preview.1 + openshock.shockosc ShockOsc en @@ -22,18 +23,18 @@ Debug;Release AnyCPU - net9.0 + net10.0 10.0.17763.0 10.0.17763.0 - - + + - + @@ -51,5 +52,12 @@ + + + $(APPDATA)\OpenShock\Desktop\modules\$(OpenShockModuleId)\ + + + + diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/AvatarActionsTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/AvatarActionsTab.razor new file mode 100644 index 0000000..6617936 --- /dev/null +++ b/ShockOsc/Ui/Pages/Dash/Tabs/AvatarActionsTab.razor @@ -0,0 +1,389 @@ +@using OpenShock.Desktop.ModuleBase +@using OpenShock.Desktop.ModuleBase.Api +@using OpenShock.Desktop.ModuleBase.Config +@using OpenShock.Desktop.ModuleBase.Models +@using OpenShock.ShockOSC.Config +@using OpenShock.ShockOSC.Services +@using OpenShock.ShockOSC.Ui.Utils +@using OpenShock.ShockOSC.Utils + +@page "/dash/avatar-actions" +@implements IAsyncDisposable + +@code { + + [ModuleInject] private IModuleConfig ModuleConfig { get; set; } = null!; + [ModuleInject] private ShockOsc ShockOsc { get; set; } = null!; + [ModuleInject] private IOpenShockService OpenShock { get; set; } = null!; + + private string CurrentAvatarId => ShockOsc.AvatarId; + + private IList CurrentActions + { + get + { + if (string.IsNullOrEmpty(CurrentAvatarId)) return []; + if (!ModuleConfig.Config.AvatarParameterActions.TryGetValue(CurrentAvatarId, out var actions)) + return []; + return actions; + } + } + + private AvatarParameterAction? _selectedAction; + private string _parameterSearch = ""; + + private readonly Dictionary _lastTriggered = new(); + private IDisposable? _paramsSubscription; + private readonly CancellationTokenSource _cts = new(); + private bool _updateQueued; + + protected override void OnInitialized() + { + _paramsSubscription = ShockOsc.OnParamsChangeObservable.Subscribe(_ => _updateQueued = true); + ShockOsc.OnAvatarActionTriggered += OnActionTriggered; + OsTask.Run(UpdateLoop); + } + + private async Task UpdateLoop() + { + using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200)); + while (await timer.WaitForNextTickAsync(_cts.Token)) + { + if (!_updateQueued) continue; + _updateQueued = false; + await InvokeAsync(StateHasChanged); + } + } + + private void OnActionTriggered(AvatarParameterAction action) + { + _lastTriggered[action] = DateTime.UtcNow; + _updateQueued = true; + } + + private bool IsRecentlyTriggered(AvatarParameterAction action) + { + return _lastTriggered.TryGetValue(action, out var time) && + (DateTime.UtcNow - time).TotalSeconds < 2; + } + + private string GetCurrentValue(string paramName) + { + if (string.IsNullOrEmpty(paramName)) return ""; + return ShockOsc.AllAvatarParams.TryGetValue(paramName, out var val) ? $"{val}" : ""; + } + + public async ValueTask DisposeAsync() + { + ShockOsc.OnAvatarActionTriggered -= OnActionTriggered; + _paramsSubscription?.Dispose(); + await _cts.CancelAsync(); + } + + private void AddAction() + { + if (string.IsNullOrEmpty(CurrentAvatarId)) return; + + if (!ModuleConfig.Config.AvatarParameterActions.ContainsKey(CurrentAvatarId)) + ModuleConfig.Config.AvatarParameterActions[CurrentAvatarId] = new List(); + + var action = new AvatarParameterAction + { + ParameterName = "", + Action = ControlType.Shock, + TriggerKind = ParameterTriggerKind.OnTrue + }; + + ModuleConfig.Config.AvatarParameterActions[CurrentAvatarId].Add(action); + _selectedAction = action; + ModuleConfig.SaveDeferred(); + InvokeAsync(StateHasChanged); + } + + private void RemoveAction(AvatarParameterAction action) + { + if (string.IsNullOrEmpty(CurrentAvatarId)) return; + if (!ModuleConfig.Config.AvatarParameterActions.TryGetValue(CurrentAvatarId, out var actions)) return; + + actions.Remove(action); + if (actions.Count == 0) + ModuleConfig.Config.AvatarParameterActions.Remove(CurrentAvatarId); + + if (_selectedAction == action) + _selectedAction = null; + + _lastTriggered.Remove(action); + ModuleConfig.SaveDeferred(); + InvokeAsync(StateHasChanged); + } + + private void OnActionChanged() + { + ModuleConfig.SaveDeferred(); + } + + private void SelectParameter(string paramName) + { + if (_selectedAction == null) return; + _selectedAction.ParameterName = paramName; + ModuleConfig.SaveDeferred(); + InvokeAsync(StateHasChanged); + } + + private IEnumerable> FilteredAvatarParams => + ShockOsc.AllAvatarParams + .Where(x => !x.Key.StartsWith("ShockOsc/")) + .Where(x => string.IsNullOrEmpty(_parameterSearch) || + x.Key.Contains(_parameterSearch, StringComparison.InvariantCultureIgnoreCase)) + .OrderBy(x => x.Key); + + private string GetGroupName(Guid groupId) + { + if (groupId == Guid.Empty) return "_All (All Shockers)"; + return ModuleConfig.Config.Groups.TryGetValue(groupId, out var group) ? group.Name : "Unknown"; + } + + private string GetActionSummary(AvatarParameterAction action) + { + if (action.IsLiveControl) + return $"{action.Action} | Live Control | {GetGroupName(action.GroupId)}"; + + var trigger = action.TriggerKind switch + { + ParameterTriggerKind.OnTrue => "On True", + ParameterTriggerKind.OnFalse => "On False", + ParameterTriggerKind.Threshold => $"Threshold >= {action.Threshold:F2}", + ParameterTriggerKind.OnChange => "On Change", + _ => "Unknown" + }; + return $"{action.Action} | {trigger} | {GetGroupName(action.GroupId)}"; + } + + private static readonly ControlType[] ActionTypes = [ControlType.Shock, ControlType.Vibrate, ControlType.Sound]; + private static readonly ParameterTriggerKind[] TriggerKinds = Enum.GetValues(); + + private bool _overrideIntensity; + private bool _overrideDuration; + + private void OnSelectAction(AvatarParameterAction? action) + { + _selectedAction = action; + if (action != null) + { + _overrideIntensity = action.OverrideIntensity.HasValue; + _overrideDuration = action.OverrideDuration.HasValue; + } + } + + private void OnOverrideIntensityChanged(bool value) + { + _overrideIntensity = value; + if (_selectedAction == null) return; + _selectedAction.OverrideIntensity = value ? (byte)50 : null; + ModuleConfig.SaveDeferred(); + } + + private void OnOverrideDurationChanged(bool value) + { + _overrideDuration = value; + if (_selectedAction == null) return; + _selectedAction.OverrideDuration = value ? (ushort)2000 : null; + ModuleConfig.SaveDeferred(); + } + + private byte IntensityValue + { + get => _selectedAction?.OverrideIntensity ?? 50; + set + { + if (_selectedAction == null) return; + _selectedAction.OverrideIntensity = value; + ModuleConfig.SaveDeferred(); + } + } + + private ushort DurationValue + { + get => _selectedAction?.OverrideDuration ?? 2000; + set + { + if (_selectedAction == null) return; + _selectedAction.OverrideDuration = value; + ModuleConfig.SaveDeferred(); + } + } +} + +@if (string.IsNullOrEmpty(CurrentAvatarId)) +{ + + No avatar detected. Connect to VRChat to configure avatar parameter actions. + +} +else +{ + + Avatar ID: @CurrentAvatarId + + + + Add Parameter Action + + + @if (CurrentActions.Count == 0) + { + + No parameter actions configured for this avatar. Click "Add Parameter Action" to + create one. + + + } + else + { + + Configured Actions + +
+ + + + + Parameter + Value + Configuration + + + + + @if (IsRecentlyTriggered(context)) + { + + } + + + @(string.IsNullOrEmpty(context.ParameterName) ? "(not set)" : context.ParameterName) + + + @GetCurrentValue(context.ParameterName) + + @GetActionSummary(context) + + + + + +
+ } + + @if (_selectedAction != null) + { + + Action Settings + +
+ + Select Avatar Parameter + + +
+ @foreach (var param in FilteredAvatarParams) + { + var isSelected = _selectedAction.ParameterName == param.Key; + + @param.Key + @param.Value + + } +
+ + + + + @foreach (var actionType in ActionTypes) + { + @actionType + } + + + + + @if (_selectedAction.IsLiveControl) + { +
+ + +
+ } + + @if (!_selectedAction.IsLiveControl) + { + + @foreach (var trigger in TriggerKinds) + { + @trigger + } + + + @if (_selectedAction.TriggerKind == ParameterTriggerKind.Threshold) + { + + Threshold: @_selectedAction.Threshold.ToString("F2") + + } + } + + + _All (All Shockers) + @foreach (var group in ModuleConfig.Config.Groups) + { + @group.Value.Name + } + + + @if (!_selectedAction.IsLiveControl) + { + + @if (_overrideIntensity) + { + + Intensity: @IntensityValue% + + } + + + } + @if (_overrideDuration) + { + + Duration: @MathF.Round(DurationValue / 1000f, 1).ToString(System.Globalization.CultureInfo.InvariantCulture)s + + } +
+ } +} diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor index 4061a92..30ee738 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/ChatboxTab.razor @@ -59,7 +59,7 @@
- + @foreach (ControlType controlType in Enum.GetValues(typeof(ControlType))) { diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor index a9f5004..731d04b 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor @@ -34,12 +34,30 @@
-
- - +
+ + Cooldown: @MathF.Round(ModuleConfig.Config.Behaviour.CooldownTime / 1000f, 1).ToString(CultureInfo.InvariantCulture)s + + +
+ +
+ + Hold Time: @ModuleConfig.Config.Behaviour.HoldTime ms + +
@@ -181,6 +199,26 @@ } + + + +
+ + Check Loop Interval: @ModuleConfig.Config.Behaviour.CheckLoopIntervalMs ms + + +
+
+
+
+ @code { [ModuleInject] private UnderscoreConfig UnderscoreConfig { get; set; } = null!; diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor index d0aa970..ef458d4 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/DebugTab.razor @@ -1,4 +1,4 @@ -@using OpenShock.Desktop.ModuleBase +@using OpenShock.Desktop.ModuleBase @using OpenShock.Desktop.ModuleBase.Api @using OpenShock.ShockOSC.Services @using OpenShock.ShockOSC.Utils @@ -7,7 +7,22 @@ @page "/dash/debug" - Avatar ID: @ShockOsc.AvatarId +
+ + @if (ShockOsc.IsGameConnected) + { + @($"Connected{(ShockOsc.IsConnectedViaOscQuery ? " (OSCQuery)" : " (Manual)")}") + } + else + { + @("Not Connected") + } + + Avatar ID: @ShockOsc.AvatarId +
OSC Parameters @@ -72,21 +87,21 @@ protected override void OnInitialized() { _onParamsVhangeSubscription = ShockOsc.OnParamsChangeObservable.Subscribe(OnParamsChange); + ShockOsc.OnGameConnectionChanged += OnConnectionChanged; OsTask.Run(UpdateParams); } + private void OnConnectionChanged() => _updateQueued = true; + private async Task UpdateParams() { - while (!_cts.IsCancellationRequested) + using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(100)); + while (await timer.WaitForNextTickAsync(_cts.Token)) { - if (!_updateQueued) - continue; + if (!_updateQueued) continue; _updateQueued = false; - await InvokeAsync(StateHasChanged); - - await Task.Delay(100); } } @@ -96,6 +111,7 @@ public async ValueTask DisposeAsync() { + ShockOsc.OnGameConnectionChanged -= OnConnectionChanged; _onParamsVhangeSubscription?.Dispose(); await _cts.CancelAsync(); } diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/GroupsTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/GroupsTab.razor index a4fa79b..ef251bc 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/GroupsTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/GroupsTab.razor @@ -10,6 +10,7 @@ @using ProgramGroup = OpenShock.ShockOSC.Models.ProgramGroup @page "/dash/groups" +@implements IDisposable @code { @@ -161,7 +162,7 @@ InvokeAsync(StateHasChanged); } - private void Dispose() + public void Dispose() { UnderscoreConfig.OnGroupConfigUpdate -= OnGroupConfigUpdate; } @@ -323,8 +324,18 @@
- +
+ + Cooldown: @MathF.Round(CurrentGroup.CooldownTime / 1000f, 1).ToString(CultureInfo.InvariantCulture)s + + +
diff --git a/copy-module-dll.cmd b/copy-module-dll.cmd deleted file mode 100644 index e700ca7..0000000 --- a/copy-module-dll.cmd +++ /dev/null @@ -1 +0,0 @@ -copy ShockOsc\bin\Debug\net9.0\OpenShock.ShockOSC.dll %appdata%\OpenShock\Desktop\modules\openshock.shockosc\OpenShock.ShockOSC.dll \ No newline at end of file