From fdd4562717a7a4467d951d297192721c05565d17 Mon Sep 17 00:00:00 2001 From: kekkels Date: Fri, 16 Oct 2026 02:52:40 -0400 Subject: [PATCH 1/2] add DurationRemaining as a float from 1 to 0 to count down remaining shock time. --- README.md | 2 ++ ShockOsc/Models/ProgramGroup.cs | 2 ++ ShockOsc/Services/OscHandler.cs | 27 ++++++++++++++++++++++++--- ShockOsc/Services/ShockOsc.cs | 1 + 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f5fdcc5..082c7ac 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Add these parameters to your avatars animator & params file. - **bool** `ShockOsc/{GroupName}_Active` enabled only while the shocker is active - **bool** `ShockOsc/{GroupName}_Cooldown` enabled only while the shocker isn't active and on cooldown - **float** `ShockOsc/{GroupName}_CooldownPercentage` 0f = shocker isn't on cooldown, 1f = shocker on cooldown (0f while shocker is active) +- **float** `ShockOsc/{GroupName}_DurationRemaining` 1f = shock just started, counts down linearly to 0f when the shock ends (0f while the shocker isn't active) - **float** `ShockOsc/{GroupName}_Intensity` 0..1f percentage value that represents how close the shock was to maximum intensity from `IntensityRange` (except for FixedIntensity) #### Virtual Groups (visual) @@ -37,6 +38,7 @@ You can use the virtual, or pseudo, shockers with the name `_Any` and `_All` for ##### `_Any` - `ShockOsc/_Any_Active` is true whenever there is any shocker currently **shocking** - `ShockOsc/_Any_Cooldown` is true whenever there is any shocker currently **on cooldown** +- `ShockOsc/_Any_DurationRemaining` is the highest `_DurationRemaining` of any shocker currently **shocking** ##### `_All` This one can be used to make all shockers configured go off at the same time or with the same trigger. diff --git a/ShockOsc/Models/ProgramGroup.cs b/ShockOsc/Models/ProgramGroup.cs index 9d53973..4076c25 100644 --- a/ShockOsc/Models/ProgramGroup.cs +++ b/ShockOsc/Models/ProgramGroup.cs @@ -30,6 +30,7 @@ public sealed class ProgramGroup public ChangeTrackedOscParam ParamActive { get; } public ChangeTrackedOscParam ParamCooldown { get; } public ChangeTrackedOscParam ParamCooldownPercentage { get; } + public ChangeTrackedOscParam ParamDurationRemaining { get; } public ChangeTrackedOscParam ParamIntensity { get; } public byte LastConcurrentIntensity { get; set; } = 0; @@ -51,6 +52,7 @@ public ProgramGroup(Guid id, string name, OscClient oscClient, Group? group) ParamActive = new ChangeTrackedOscParam(Name, "_Active", false, oscClient); ParamCooldown = new ChangeTrackedOscParam(Name, "_Cooldown", false, oscClient); ParamCooldownPercentage = new ChangeTrackedOscParam(Name, "_CooldownPercentage", 0f, oscClient); + ParamDurationRemaining = new ChangeTrackedOscParam(Name, "_DurationRemaining", 0f, oscClient); ParamIntensity = new ChangeTrackedOscParam(Name, "_Intensity", 0f, oscClient); } diff --git a/ShockOsc/Services/OscHandler.cs b/ShockOsc/Services/OscHandler.cs index 100d453..e5f89e4 100644 --- a/ShockOsc/Services/OscHandler.cs +++ b/ShockOsc/Services/OscHandler.cs @@ -13,6 +13,7 @@ public sealed class OscHandler private readonly ChangeTrackedOscParam _paramAnyActive; private readonly ChangeTrackedOscParam _paramAnyCooldown; private readonly ChangeTrackedOscParam _paramAnyCooldownPercentage; + private readonly ChangeTrackedOscParam _paramAnyDurationRemaining; private readonly ChangeTrackedOscParam _paramAnyIntensity; private readonly ConcurrentDictionary _lastControlLogs = new(); @@ -33,6 +34,7 @@ public OscHandler(ILogger logger, OscClient oscClient, IModuleConfig _paramAnyActive = new ChangeTrackedOscParam("_Any", "_Active", false, _oscClient); _paramAnyCooldown = new ChangeTrackedOscParam("_Any", "_Cooldown", false, _oscClient); _paramAnyCooldownPercentage = new ChangeTrackedOscParam("_Any", "_CooldownPercentage", 0f, _oscClient); + _paramAnyDurationRemaining = new ChangeTrackedOscParam("_Any", "_DurationRemaining", 0f, _oscClient); _paramAnyIntensity = new ChangeTrackedOscParam("_Any", "_Intensity", 0f, _oscClient); } @@ -77,12 +79,13 @@ public async Task SendParams() { // TODO: maybe force resend on avatar change - var (anyActiveRemote, maxIntensityRemote) = GetRemoteAnyActive(); + var (anyActiveRemote, maxIntensityRemote, maxDurationRemainingRemote) = GetRemoteAnyActive(); var anyActiveLocal = false; var maxIntensityLocal = 0f; var anyCooldown = false; var anyCooldownPercentage = 0f; + var anyDurationRemaining = 0f; foreach (var shocker in _shockOscData.ProgramGroups.Values) { @@ -108,15 +111,24 @@ public async Task SendParams() .TotalMilliseconds / cooldownTime); + // 1f = shock just started, 0f = finished (or not active) + var durationRemaining = 0f; + if (isActive && shocker.LastDuration > 0) + durationRemaining = MathUtils.Saturate( + (float)(shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) - DateTime.UtcNow) + .TotalMilliseconds / shocker.LastDuration); + await shocker.ParamActive.SetValue(isActive); await shocker.ParamCooldown.SetValue(onCoolDown); await shocker.ParamCooldownPercentage.SetValue(cooldownPercentage); await shocker.ParamIntensity.SetValue(intensity); + await shocker.ParamDurationRemaining.SetValue(durationRemaining); if (isActive) { anyActiveLocal = true; maxIntensityLocal = MathF.Max(maxIntensityLocal, intensity); + anyDurationRemaining = MathF.Max(anyDurationRemaining, durationRemaining); } if (onCoolDown) anyCooldown = true; @@ -127,13 +139,15 @@ public async Task SendParams() await _paramAnyIntensity.SetValue(MathF.Max(maxIntensityRemote, maxIntensityLocal)); await _paramAnyCooldown.SetValue(anyCooldown); await _paramAnyCooldownPercentage.SetValue(anyCooldownPercentage); + await _paramAnyDurationRemaining.SetValue(MathF.Max(maxDurationRemainingRemote, anyDurationRemaining)); } - private (bool AnyActive, float MaxIntensity) GetRemoteAnyActive() + private (bool AnyActive, float MaxIntensity, float MaxDurationRemaining) GetRemoteAnyActive() { var now = DateTimeOffset.UtcNow; var anyActive = false; var maxIntensity = 0f; + var maxDurationRemaining = 0f; foreach (var logEntry in _lastControlLogs.Values) { @@ -145,9 +159,16 @@ public async Task SendParams() anyActive = true; var intensity = MathUtils.Saturate(logEntry.ControlLog.Intensity / 100f); maxIntensity = MathF.Max(maxIntensity, intensity); + + if (logEntry.ControlLog.Duration > 0) + { + var remaining = MathUtils.Saturate( + (float)(activeUntil - now).TotalMilliseconds / logEntry.ControlLog.Duration); + maxDurationRemaining = MathF.Max(maxDurationRemaining, remaining); + } } - return (anyActive, maxIntensity); + return (anyActive, maxIntensity, maxDurationRemaining); } public void SetLastControlCommand(LastControlLogEntry controlLogs) diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index d378490..e926a49 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -68,6 +68,7 @@ public sealed class ShockOsc "Active", "Intensity", "CooldownPercentage", + "DurationRemaining", "IShock", "IVibrate", "ISound", From d1003a6685cb9f536708ead16e511bd37186c65d Mon Sep 17 00:00:00 2001 From: kekkels Date: Fri, 16 Oct 2026 02:59:03 -0400 Subject: [PATCH 2/2] add ability to scale to max duration instead of always starting from 1 --- README.md | 2 +- ShockOsc/Config/BehaviourConf.cs | 1 + ShockOsc/Services/OscHandler.cs | 34 ++++++++++++++++++--- ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor | 9 ++++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 082c7ac..ff588d0 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Add these parameters to your avatars animator & params file. - **bool** `ShockOsc/{GroupName}_Active` enabled only while the shocker is active - **bool** `ShockOsc/{GroupName}_Cooldown` enabled only while the shocker isn't active and on cooldown - **float** `ShockOsc/{GroupName}_CooldownPercentage` 0f = shocker isn't on cooldown, 1f = shocker on cooldown (0f while shocker is active) -- **float** `ShockOsc/{GroupName}_DurationRemaining` 1f = shock just started, counts down linearly to 0f when the shock ends (0f while the shocker isn't active) +- **float** `ShockOsc/{GroupName}_DurationRemaining` 1f = shock just started, counts down linearly to 0f when the shock ends (0f while the shocker isn't active). With the "Scale duration remaining to max" setting it starts at `duration / max duration` instead (e.g. 0.75f for a 7.5s shock when the max is 10s) - **float** `ShockOsc/{GroupName}_Intensity` 0..1f percentage value that represents how close the shock was to maximum intensity from `IntensityRange` (except for FixedIntensity) #### Virtual Groups (visual) diff --git a/ShockOsc/Config/BehaviourConf.cs b/ShockOsc/Config/BehaviourConf.cs index 8a617cf..c3ba107 100644 --- a/ShockOsc/Config/BehaviourConf.cs +++ b/ShockOsc/Config/BehaviourConf.cs @@ -6,4 +6,5 @@ public sealed class BehaviourConf : SharedBehaviourConfig public bool DisableWhileAfk { get; set; } = true; public bool ForceUnmute { get; set; } public uint CheckLoopIntervalMs { get; set; } = 50; + public bool ScaleDurationRemainingToMax { get; set; } } \ No newline at end of file diff --git a/ShockOsc/Services/OscHandler.cs b/ShockOsc/Services/OscHandler.cs index e5f89e4..d7f6069 100644 --- a/ShockOsc/Services/OscHandler.cs +++ b/ShockOsc/Services/OscHandler.cs @@ -3,6 +3,7 @@ using OpenShock.Desktop.ModuleBase.Config; using OpenShock.Desktop.ModuleBase.Models; using OpenShock.ShockOSC.Config; +using OpenShock.ShockOSC.Models; using OpenShock.ShockOSC.OscChangeTracker; using OpenShock.Internal.Common.Utils; @@ -111,12 +112,12 @@ public async Task SendParams() .TotalMilliseconds / cooldownTime); - // 1f = shock just started, 0f = finished (or not active) + // 1f = shock just started (or max duration, when scaling to max), 0f = finished (or not active) var durationRemaining = 0f; if (isActive && shocker.LastDuration > 0) - durationRemaining = MathUtils.Saturate( + durationRemaining = GetDurationRemaining( (float)(shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) - DateTime.UtcNow) - .TotalMilliseconds / shocker.LastDuration); + .TotalMilliseconds, shocker.LastDuration, GetMaxDuration(shocker)); await shocker.ParamActive.SetValue(isActive); await shocker.ParamCooldown.SetValue(onCoolDown); @@ -142,6 +143,29 @@ public async Task SendParams() await _paramAnyDurationRemaining.SetValue(MathF.Max(maxDurationRemainingRemote, anyDurationRemaining)); } + /// + /// Normalizes the remaining time of a shock. By default relative to the shock's own duration, with + /// relative to the maximum possible duration, so a 7.5s + /// shock with a 10s max starts at 0.75f and counts down at the same speed as a 10s one. + /// + private float GetDurationRemaining(float remainingMs, float durationMs, float maxDurationMs) + { + var denominator = _moduleConfig.Config.Behaviour.ScaleDurationRemainingToMax + ? MathF.Max(maxDurationMs, durationMs) // max can be lower than the shock if config changed mid-shock + : durationMs; + return denominator <= 0f ? 0f : MathUtils.Saturate(remainingMs / denominator); + } + + private static float GetMaxDuration(SharedBehaviourConfig config) => + config.RandomDuration ? config.DurationRange.Max : config.FixedDuration; + + private float GetGlobalMaxDuration() => GetMaxDuration(_moduleConfig.Config.Behaviour); + + private float GetMaxDuration(ProgramGroup shocker) => + shocker.ConfigGroup is { OverrideDuration: true } group + ? GetMaxDuration(group) + : GetGlobalMaxDuration(); + private (bool AnyActive, float MaxIntensity, float MaxDurationRemaining) GetRemoteAnyActive() { var now = DateTimeOffset.UtcNow; @@ -162,8 +186,8 @@ public async Task SendParams() if (logEntry.ControlLog.Duration > 0) { - var remaining = MathUtils.Saturate( - (float)(activeUntil - now).TotalMilliseconds / logEntry.ControlLog.Duration); + var remaining = GetDurationRemaining((float)(activeUntil - now).TotalMilliseconds, + logEntry.ControlLog.Duration, GetGlobalMaxDuration()); maxDurationRemaining = MathF.Max(maxDurationRemaining, remaining); } } diff --git a/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor b/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor index de58a5f..c237532 100644 --- a/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor +++ b/ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor @@ -130,6 +130,15 @@ @bind-Value="ModuleConfig.Config.Behaviour.HoldTime" @bind-Value:after="OnSettingsValueChangeAsync"/> + + + + + +