Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). 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)
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions ShockOsc/Config/BehaviourConf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
2 changes: 2 additions & 0 deletions ShockOsc/Models/ProgramGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public sealed class ProgramGroup
public ChangeTrackedOscParam<bool> ParamActive { get; }
public ChangeTrackedOscParam<bool> ParamCooldown { get; }
public ChangeTrackedOscParam<float> ParamCooldownPercentage { get; }
public ChangeTrackedOscParam<float> ParamDurationRemaining { get; }
public ChangeTrackedOscParam<float> ParamIntensity { get; }

public byte LastConcurrentIntensity { get; set; } = 0;
Expand All @@ -51,6 +52,7 @@ public ProgramGroup(Guid id, string name, OscClient oscClient, Group? group)
ParamActive = new ChangeTrackedOscParam<bool>(Name, "_Active", false, oscClient);
ParamCooldown = new ChangeTrackedOscParam<bool>(Name, "_Cooldown", false, oscClient);
ParamCooldownPercentage = new ChangeTrackedOscParam<float>(Name, "_CooldownPercentage", 0f, oscClient);
ParamDurationRemaining = new ChangeTrackedOscParam<float>(Name, "_DurationRemaining", 0f, oscClient);
ParamIntensity = new ChangeTrackedOscParam<float>(Name, "_Intensity", 0f, oscClient);
}

Expand Down
51 changes: 48 additions & 3 deletions ShockOsc/Services/OscHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -13,6 +14,7 @@ public sealed class OscHandler
private readonly ChangeTrackedOscParam<bool> _paramAnyActive;
private readonly ChangeTrackedOscParam<bool> _paramAnyCooldown;
private readonly ChangeTrackedOscParam<float> _paramAnyCooldownPercentage;
private readonly ChangeTrackedOscParam<float> _paramAnyDurationRemaining;
private readonly ChangeTrackedOscParam<float> _paramAnyIntensity;

private readonly ConcurrentDictionary<Guid, LastControlLogEntry> _lastControlLogs = new();
Expand All @@ -33,6 +35,7 @@ public OscHandler(ILogger<OscHandler> logger, OscClient oscClient, IModuleConfig
_paramAnyActive = new ChangeTrackedOscParam<bool>("_Any", "_Active", false, _oscClient);
_paramAnyCooldown = new ChangeTrackedOscParam<bool>("_Any", "_Cooldown", false, _oscClient);
_paramAnyCooldownPercentage = new ChangeTrackedOscParam<float>("_Any", "_CooldownPercentage", 0f, _oscClient);
_paramAnyDurationRemaining = new ChangeTrackedOscParam<float>("_Any", "_DurationRemaining", 0f, _oscClient);
_paramAnyIntensity = new ChangeTrackedOscParam<float>("_Any", "_Intensity", 0f, _oscClient);
}

Expand Down Expand Up @@ -77,12 +80,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)
{
Expand All @@ -108,15 +112,24 @@ public async Task SendParams()
.TotalMilliseconds /
cooldownTime);

// 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 = GetDurationRemaining(
(float)(shocker.LastExecuted.AddMilliseconds(shocker.LastDuration) - DateTime.UtcNow)
.TotalMilliseconds, shocker.LastDuration, GetMaxDuration(shocker));

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;
Expand All @@ -127,13 +140,38 @@ 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));
}

/// <summary>
/// Normalizes the remaining time of a shock. By default relative to the shock's own duration, with
/// <see cref="BehaviourConf.ScaleDurationRemainingToMax"/> 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.
/// </summary>
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 (bool AnyActive, float MaxIntensity) GetRemoteAnyActive()
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;
var anyActive = false;
var maxIntensity = 0f;
var maxDurationRemaining = 0f;

foreach (var logEntry in _lastControlLogs.Values)
{
Expand All @@ -145,9 +183,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 = GetDurationRemaining((float)(activeUntil - now).TotalMilliseconds,
logEntry.ControlLog.Duration, GetGlobalMaxDuration());
maxDurationRemaining = MathF.Max(maxDurationRemaining, remaining);
}
}

return (anyActive, maxIntensity);
return (anyActive, maxIntensity, maxDurationRemaining);
}

public void SetLastControlCommand(LastControlLogEntry controlLogs)
Expand Down
1 change: 1 addition & 0 deletions ShockOsc/Services/ShockOsc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public sealed class ShockOsc
"Active",
"Intensity",
"CooldownPercentage",
"DurationRemaining",
"IShock",
"IVibrate",
"ISound",
Expand Down
9 changes: 9 additions & 0 deletions ShockOsc/Ui/Pages/Dash/Tabs/ConfigTab.razor
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@
@bind-Value="ModuleConfig.Config.Behaviour.HoldTime"
@bind-Value:after="OnSettingsValueChangeAsync"/>
</SettingField>

<MudDivider/>

<SettingRow Label="Scale duration remaining to max"
Description="Avatar parameter _DurationRemaining starts at shock duration / max duration (e.g. 0.75 for 7.5s of 10s) instead of always starting at 1.">
<MudSwitch T="bool" Color="Color.Primary"
@bind-Value="ModuleConfig.Config.Behaviour.ScaleDurationRemainingToMax"
@bind-Value:after="OnSettingsValueChangeAsync"/>
</SettingRow>
</SettingSection>

<SettingSection Icon="@Icons.Material.Filled.Vibration" Title="PhysBone">
Expand Down
Loading