From 7a9f1b87b8830f1319d110d73c3294f72ce33fd6 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sat, 30 Sep 2023 15:01:02 +0100 Subject: [PATCH 01/19] Rewrite maths module to allow for more customisability --- VRCOSC.Modules/Maths/MathsEquationInstance.cs | 74 +++---------------- VRCOSC.Modules/Maths/MathsModule.cs | 64 +++++++++++----- 2 files changed, 58 insertions(+), 80 deletions(-) diff --git a/VRCOSC.Modules/Maths/MathsEquationInstance.cs b/VRCOSC.Modules/Maths/MathsEquationInstance.cs index b64bd22f..6a8b1555 100644 --- a/VRCOSC.Modules/Maths/MathsEquationInstance.cs +++ b/VRCOSC.Modules/Maths/MathsEquationInstance.cs @@ -24,7 +24,7 @@ namespace VRCOSC.Modules.Maths; public class MathsEquationInstance : IEquatable { [JsonProperty("input_parameter")] - public Bindable InputParameter = new(string.Empty); + public Bindable TriggerParameter = new(string.Empty); [JsonProperty("input_type")] public Bindable InputType = new(); @@ -36,13 +36,13 @@ public class MathsEquationInstance : IEquatable public Bindable OutputParameter = new(string.Empty); [JsonProperty("output_type")] - public Bindable OutputType = new(); + public Bindable OutputType = new(MathsEquationValueType.Int); public bool Equals(MathsEquationInstance? other) { if (ReferenceEquals(other, null)) return false; - return InputParameter.Value == other.InputParameter.Value && InputType.Value == other.InputType.Value && Equation.Value == other.Equation.Value && OutputParameter.Value == other.OutputParameter.Value && OutputType.Value == other.OutputType.Value; + return TriggerParameter.Value == other.TriggerParameter.Value && InputType.Value == other.InputType.Value && Equation.Value == other.Equation.Value && OutputParameter.Value == other.OutputParameter.Value && OutputType.Value == other.OutputType.Value; } [JsonConstructor] @@ -52,7 +52,7 @@ public MathsEquationInstance() public MathsEquationInstance(MathsEquationInstance other) { - InputParameter.Value = other.InputParameter.Value; + TriggerParameter.Value = other.TriggerParameter.Value; InputType.Value = other.InputType.Value; Equation.Value = other.Equation.Value; OutputParameter.Value = other.OutputParameter.Value; @@ -119,15 +119,11 @@ private void load() AutoSizeAxes = Axes.Y, ColumnDimensions = new[] { - new Dimension(maxSize: 250), + new Dimension(maxSize: 150), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 100), - new Dimension(GridSizeMode.Absolute, 15), new Dimension(), - new Dimension(GridSizeMode.Absolute, 15), - new Dimension(maxSize: 250), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 100) + new Dimension(maxSize: 150) }, RowDimensions = new[] { @@ -145,20 +141,7 @@ private void load() { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Input Parameter", - Font = FrameworkFont.Regular.With(size: 20) - } - }, - null, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Input Type", + Text = "Trigger Parameter", Font = FrameworkFont.Regular.With(size: 20) } }, @@ -187,19 +170,6 @@ private void load() Text = "Output Parameter", Font = FrameworkFont.Regular.With(size: 20) } - }, - null, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Output Type", - Font = FrameworkFont.Regular.With(size: 20) - } } } } @@ -217,15 +187,11 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) AutoSizeAxes = Axes.Y, ColumnDimensions = new[] { - new Dimension(maxSize: 250), + new Dimension(maxSize: 150), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 100), - new Dimension(GridSizeMode.Absolute, 15), new Dimension(), - new Dimension(GridSizeMode.Absolute, 15), - new Dimension(maxSize: 250), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 100) + new Dimension(maxSize: 150) }, RowDimensions = new[] { @@ -245,17 +211,8 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) CornerRadius = 5, BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, - ValidCurrent = instance.InputParameter.GetBoundCopy(), - PlaceholderText = "Input Parameter" - }, - null, - new MathsValueTypeInstanceDropdown - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Items = Enum.GetValues(typeof(MathsEquationValueType)).Cast(), - Current = instance.InputType.GetBoundCopy() + ValidCurrent = instance.TriggerParameter.GetBoundCopy(), + PlaceholderText = "Trigger Parameter" }, null, new StringTextBox @@ -284,15 +241,6 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) BorderThickness = 2, ValidCurrent = instance.OutputParameter.GetBoundCopy(), PlaceholderText = "Output Parameter" - }, - null, - new MathsValueTypeInstanceDropdown - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Items = Enum.GetValues(typeof(MathsEquationValueType)).Cast(), - Current = instance.OutputType.GetBoundCopy() } } } diff --git a/VRCOSC.Modules/Maths/MathsModule.cs b/VRCOSC.Modules/Maths/MathsModule.cs index bb4b27e4..492773a7 100644 --- a/VRCOSC.Modules/Maths/MathsModule.cs +++ b/VRCOSC.Modules/Maths/MathsModule.cs @@ -12,7 +12,9 @@ namespace VRCOSC.Modules.Maths; [ModuleAuthor("VolcanicArts")] public class MathsModule : AvatarModule { + private readonly Dictionary parameterValues = new(); private readonly Dictionary instances = new(); + private readonly List elements = new(); public MathsModule() { @@ -21,51 +23,79 @@ public MathsModule() protected override void CreateAttributes() { + CreateSetting(MathsSetting.Constants, "Constants", "Define your own constants to reuse in your equations", Array.Empty()); + CreateSetting(MathsSetting.Functions, "Functions", "Define your own functions to reuse in your equations", Array.Empty()); + CreateSetting(MathsSetting.Equations, new MathsEquationInstanceListAttribute { Name = "Equations", - Description = "Here you can write equations to run on a parameter and output to another parameter\nValues will be automatically converted to best fit the output parameter\nChanges to this require a module restart\nTo access the input parameter's value, use the argument 'p'", + Description = "Here you can write equations to run on a parameter and output to another parameter\nValues will be automatically converted to best fit the output parameter\nYou can access any parameter value by writing its name\nChanges to this setting requires a module restart", Default = new List() }); } protected override void OnModuleStart() { + parameterValues.Clear(); instances.Clear(); + elements.Clear(); - GetSettingList(MathsSetting.Equations).ForEach(instance => { instances.Add(instance.InputParameter.Value, instance); }); + GetSettingList(MathsSetting.Equations).ForEach(instance => instances.Add(instance.TriggerParameter.Value, instance)); + elements.AddRange(GetSettingList(MathsSetting.Constants).Select(constant => new Constant(constant))); + elements.AddRange(GetSettingList(MathsSetting.Functions).Select(function => new Function(function))); } protected override void OnAnyParameterReceived(ReceivedParameter parameter) { - if (!instances.TryGetValue(parameter.Name, out var instance)) return; + parameterValues[parameter.Name] = parameter; - var parameterArgument = createArgumentForParameterValue(parameter, instance.InputType.Value); - var expression = new Expression(instance.Equation.Value, parameterArgument); + if (!instances.TryGetValue(parameter.Name, out var instance)) return; + var expression = new Expression(instance.Equation.Value, parameterValues.Values.Select(createArgumentForParameterValue).Concat(elements).ToArray()); var output = expression.calculate(); SendParameter(instance.OutputParameter.Value, convertToOutputType(output, instance.OutputType.Value)); } - private static Argument createArgumentForParameterValue(ReceivedParameter parameter, MathsEquationValueType valueType) => valueType switch + private static PrimitiveElement createArgumentForParameterValue(ReceivedParameter parameter) { - MathsEquationValueType.Bool => new Argument("p", parameter.ValueAs() ? 1 : 0), - MathsEquationValueType.Int => new Argument("p", parameter.ValueAs()), - MathsEquationValueType.Float => new Argument("p", parameter.ValueAs()), - _ => throw new ArgumentOutOfRangeException(nameof(valueType), valueType, null) - }; + if (parameter.IsValueType()) return new Argument(parameter.Name, parameter.ValueAs() ? 1 : 0); + if (parameter.IsValueType()) return new Argument(parameter.Name, parameter.ValueAs()); + if (parameter.IsValueType()) return new Argument(parameter.Name, parameter.ValueAs()); + + throw new InvalidOperationException("Unknown parameter type"); + } - private static object convertToOutputType(double value, MathsEquationValueType valueType) => valueType switch + private object convertToOutputType(double value, MathsEquationValueType valueType) { - MathsEquationValueType.Bool => Convert.ToBoolean(value), - MathsEquationValueType.Int => Convert.ToInt32(value), - MathsEquationValueType.Float => Convert.ToSingle(value), - _ => throw new ArgumentOutOfRangeException(nameof(valueType), valueType, null) - }; + try + { + return valueType switch + { + MathsEquationValueType.Bool => Convert.ToBoolean(value), + MathsEquationValueType.Int => Convert.ToInt32(value), + MathsEquationValueType.Float => Convert.ToSingle(value), + _ => throw new ArgumentOutOfRangeException(nameof(valueType), valueType, null) + }; + } + catch (Exception e) + { + Log($"Warning. Value {value}. " + e.Message); + + return valueType switch + { + MathsEquationValueType.Bool => default(bool), + MathsEquationValueType.Int => default(int), + MathsEquationValueType.Float => default(float), + _ => throw new ArgumentOutOfRangeException(nameof(valueType), valueType, null) + }; + } + } private enum MathsSetting { + Constants, + Functions, Equations } } From 0c03ddad19979067265aafc322eebcf501216344 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sat, 30 Sep 2023 16:47:51 +0100 Subject: [PATCH 02/19] Add back type dropdown --- VRCOSC.Modules/Maths/MathsEquationInstance.cs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/VRCOSC.Modules/Maths/MathsEquationInstance.cs b/VRCOSC.Modules/Maths/MathsEquationInstance.cs index 6a8b1555..c019f06c 100644 --- a/VRCOSC.Modules/Maths/MathsEquationInstance.cs +++ b/VRCOSC.Modules/Maths/MathsEquationInstance.cs @@ -36,7 +36,7 @@ public class MathsEquationInstance : IEquatable public Bindable OutputParameter = new(string.Empty); [JsonProperty("output_type")] - public Bindable OutputType = new(MathsEquationValueType.Int); + public Bindable OutputType = new(); public bool Equals(MathsEquationInstance? other) { @@ -123,7 +123,9 @@ private void load() new Dimension(GridSizeMode.Absolute, 5), new Dimension(), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 150) + new Dimension(maxSize: 150), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: 100) }, RowDimensions = new[] { @@ -170,6 +172,19 @@ private void load() Text = "Output Parameter", Font = FrameworkFont.Regular.With(size: 20) } + }, + null, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Output Type", + Font = FrameworkFont.Regular.With(size: 20) + } } } } @@ -191,7 +206,9 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) new Dimension(GridSizeMode.Absolute, 5), new Dimension(), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 150) + new Dimension(maxSize: 150), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: 100) }, RowDimensions = new[] { @@ -241,6 +258,15 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) BorderThickness = 2, ValidCurrent = instance.OutputParameter.GetBoundCopy(), PlaceholderText = "Output Parameter" + }, + null, + new MathsValueTypeInstanceDropdown + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Items = Enum.GetValues(typeof(MathsEquationValueType)).Cast(), + Current = instance.OutputType.GetBoundCopy() } } } From 55c201db537d11db258352bb6515f4da64b6b9fd Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sat, 30 Sep 2023 16:48:12 +0100 Subject: [PATCH 03/19] Fix implicit multiplication breaking parameter name usage --- VRCOSC.Modules/Maths/MathsModule.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/VRCOSC.Modules/Maths/MathsModule.cs b/VRCOSC.Modules/Maths/MathsModule.cs index 492773a7..04dde2a1 100644 --- a/VRCOSC.Modules/Maths/MathsModule.cs +++ b/VRCOSC.Modules/Maths/MathsModule.cs @@ -40,7 +40,7 @@ protected override void OnModuleStart() instances.Clear(); elements.Clear(); - GetSettingList(MathsSetting.Equations).ForEach(instance => instances.Add(instance.TriggerParameter.Value, instance)); + GetSettingList(MathsSetting.Equations).ForEach(instance => instances.TryAdd(instance.TriggerParameter.Value, instance)); elements.AddRange(GetSettingList(MathsSetting.Constants).Select(constant => new Constant(constant))); elements.AddRange(GetSettingList(MathsSetting.Functions).Select(function => new Function(function))); } @@ -52,6 +52,14 @@ protected override void OnAnyParameterReceived(ReceivedParameter parameter) if (!instances.TryGetValue(parameter.Name, out var instance)) return; var expression = new Expression(instance.Equation.Value, parameterValues.Values.Select(createArgumentForParameterValue).Concat(elements).ToArray()); + expression.disableImpliedMultiplicationMode(); + + if (expression.getMissingUserDefinedArguments().Any()) + { + Log($"Missing argument value for equation {instance.TriggerParameter.Value}"); + return; + } + var output = expression.calculate(); SendParameter(instance.OutputParameter.Value, convertToOutputType(output, instance.OutputType.Value)); From 363a53d68d7fab0cb3f0bde153a434633484b451 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sat, 30 Sep 2023 16:53:12 +0100 Subject: [PATCH 04/19] Ensure only valid equations are used --- VRCOSC.Modules/Maths/MathsEquationInstance.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/VRCOSC.Modules/Maths/MathsEquationInstance.cs b/VRCOSC.Modules/Maths/MathsEquationInstance.cs index c019f06c..2bf8027b 100644 --- a/VRCOSC.Modules/Maths/MathsEquationInstance.cs +++ b/VRCOSC.Modules/Maths/MathsEquationInstance.cs @@ -1,4 +1,4 @@ -// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. using Newtonsoft.Json; @@ -229,7 +229,8 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, ValidCurrent = instance.TriggerParameter.GetBoundCopy(), - PlaceholderText = "Trigger Parameter" + PlaceholderText = "Trigger Parameter", + EmptyIsValid = false }, null, new StringTextBox @@ -243,7 +244,8 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, ValidCurrent = instance.Equation.GetBoundCopy(), - PlaceholderText = "Equation" + PlaceholderText = "Equation", + EmptyIsValid = false }, null, new StringTextBox @@ -257,7 +259,8 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, ValidCurrent = instance.OutputParameter.GetBoundCopy(), - PlaceholderText = "Output Parameter" + PlaceholderText = "Output Parameter", + EmptyIsValid = false }, null, new MathsValueTypeInstanceDropdown From 8eaae1aee9f642826639f3795f56dfd7b6361e64 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 1 Oct 2023 13:56:00 +0100 Subject: [PATCH 05/19] Use OSCQuery to query for missing parameter values --- VRCOSC.Game/App/AppManager.cs | 13 ++- VRCOSC.Game/Modules/Module.cs | 13 +++ VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs | 80 ++++++++++++++++++- VRCOSC.Game/VRCOSC.Game.csproj | 2 + VRCOSC.Modules/Maths/MathsEquationInstance.cs | 36 +-------- VRCOSC.Modules/Maths/MathsModule.cs | 49 ++++++++---- 6 files changed, 138 insertions(+), 55 deletions(-) diff --git a/VRCOSC.Game/App/AppManager.cs b/VRCOSC.Game/App/AppManager.cs index aaaf5605..6add7c9d 100644 --- a/VRCOSC.Game/App/AppManager.cs +++ b/VRCOSC.Game/App/AppManager.cs @@ -38,6 +38,7 @@ public partial class AppManager : Component private static readonly TimeSpan openvr_check_interval = TimeSpan.FromSeconds(1); private static readonly TimeSpan vrchat_check_interval = TimeSpan.FromSeconds(5); + private static readonly TimeSpan oscjson_check_interval = TimeSpan.FromSeconds(5); private readonly Queue oscMessageQueue = new(); private ScheduledDelegate? runningModulesDelegate; @@ -144,6 +145,8 @@ private void initialiseDelayedTasks() { Scheduler.AddDelayed(checkForOpenVR, openvr_check_interval.TotalMilliseconds, true); Scheduler.AddDelayed(checkForVRChat, vrchat_check_interval.TotalMilliseconds, true); + Scheduler.AddDelayed(checkForOscjson, oscjson_check_interval.TotalMilliseconds, true); + checkForOscjson(); } #endregion @@ -189,10 +192,7 @@ private void processControlParameters(VRChatOscMessage message) private void scheduleModuleEnabledParameters() { - runningModulesDelegate = Scheduler.AddDelayed(() => - { - ModuleManager.Modules.ForEach(module => sendModuleRunningState(module, ModuleManager.IsModuleRunning(module))); - }, TimeSpan.FromSeconds(1).TotalMilliseconds, true); + runningModulesDelegate = Scheduler.AddDelayed(() => ModuleManager.Modules.ForEach(module => sendModuleRunningState(module, ModuleManager.IsModuleRunning(module))), TimeSpan.FromSeconds(1).TotalMilliseconds, true); } private void cancelRunningModulesDelegate() @@ -320,6 +320,11 @@ private void checkForVRChat() if (!VRChat.IsClientOpen && State.Value == AppManagerState.Started) Stop(); } + private async void checkForOscjson() + { + await OSCClient.CheckForVRChatOSCQuery(); + } + #endregion } diff --git a/VRCOSC.Game/Modules/Module.cs b/VRCOSC.Game/Modules/Module.cs index f7cf9f66..3aac56e5 100644 --- a/VRCOSC.Game/Modules/Module.cs +++ b/VRCOSC.Game/Modules/Module.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.IEnumerableExtensions; @@ -406,6 +407,18 @@ protected T GetSetting(Enum lookup) #region Parameters + /// + /// Lets you pass a parameter name to attempt to find the current value using OSCQuery + /// + /// Returns null if no parameter is found or OSCQuery hasn't initialised + protected async Task FindParameterValue(string parameterName) => await oscClient.FindParameterValue(parameterName); + + /// + /// Lets you pass a parameter name to attempt to find the type using OSCQuery + /// + /// Returns null if no parameter is found or OSCQuery hasn't initialised + protected async Task FindParameterType(string parameterName) => await oscClient.FindParameterType(parameterName); + /// /// Allows for sending a parameter that hasn't been registered. Only use this when absolutely necessary /// diff --git a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs index f515c501..5c7a3437 100644 --- a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs +++ b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs @@ -3,7 +3,13 @@ using System; using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using osu.Framework.Logging; +using VRC.OSCQuery; using VRCOSC.Game.OSC.Client; +using Zeroconf; namespace VRCOSC.Game.OSC.VRChat; @@ -12,12 +18,12 @@ public class VRChatOscClient : OscClient public Action? OnParameterSent; public Action? OnParameterReceived; + private readonly HttpClient client = new(); + private int? port; + public VRChatOscClient() { - OnMessageSent += message => - { - OnParameterSent?.Invoke(new VRChatOscMessage(message)); - }; + OnMessageSent += message => { OnParameterSent?.Invoke(new VRChatOscMessage(message)); }; OnMessageReceived += message => { @@ -27,4 +33,70 @@ public VRChatOscClient() OnParameterReceived?.Invoke(data); }; } + + public async Task CheckForVRChatOSCQuery() + { + var hosts = await ZeroconfResolver.ResolveAsync("_oscjson._tcp.local."); + var host = hosts.FirstOrDefault(); + + if (host is null) + { + Logger.Log("No OscJson host found"); + port = null; + return; + } + + if (!host.Services.Any(s => s.Value.ServiceName.Contains("VRChat-Client"))) + { + Logger.Log("No VRChat-Client found"); + port = null; + return; + } + + var service = host.Services.Single(s => s.Value.ServiceName.Contains("VRChat-Client")); + + port = service.Value.Port; + } + + public async Task FindParameterValue(string parameterName) + { + if (port is null) return null; + + var url = $"http://127.0.0.1:{port}/avatar/parameters/{parameterName}"; + + var response = await client.GetAsync(new Uri(url)); + var content = await response.Content.ReadAsStringAsync(); + var node = JsonConvert.DeserializeObject(content); + + if (node is null) return null; + + return node.OscType switch + { + "f" => Convert.ToSingle(node.Value[0]), + "i" => Convert.ToInt32(node.Value[0]), + "b" => Convert.ToBoolean(node.Value[0]), + _ => null + }; + } + + public async Task FindParameterType(string parameterName) + { + if (port is null) return null; + + var url = $"http://127.0.0.1:{port}/avatar/parameters/{parameterName}"; + + var response = await client.GetAsync(new Uri(url)); + var content = await response.Content.ReadAsStringAsync(); + var node = JsonConvert.DeserializeObject(content); + + if (node is null) return null; + + return node.OscType switch + { + "f" => TypeCode.Single, + "i" => TypeCode.Int32, + "b" => TypeCode.Boolean, + _ => null + }; + } } diff --git a/VRCOSC.Game/VRCOSC.Game.csproj b/VRCOSC.Game/VRCOSC.Game.csproj index a90b6bde..30a51761 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -26,9 +26,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/VRCOSC.Modules/Maths/MathsEquationInstance.cs b/VRCOSC.Modules/Maths/MathsEquationInstance.cs index 2bf8027b..a9b3169e 100644 --- a/VRCOSC.Modules/Maths/MathsEquationInstance.cs +++ b/VRCOSC.Modules/Maths/MathsEquationInstance.cs @@ -35,14 +35,11 @@ public class MathsEquationInstance : IEquatable [JsonProperty("output_parameter")] public Bindable OutputParameter = new(string.Empty); - [JsonProperty("output_type")] - public Bindable OutputType = new(); - public bool Equals(MathsEquationInstance? other) { if (ReferenceEquals(other, null)) return false; - return TriggerParameter.Value == other.TriggerParameter.Value && InputType.Value == other.InputType.Value && Equation.Value == other.Equation.Value && OutputParameter.Value == other.OutputParameter.Value && OutputType.Value == other.OutputType.Value; + return TriggerParameter.Value == other.TriggerParameter.Value && InputType.Value == other.InputType.Value && Equation.Value == other.Equation.Value && OutputParameter.Value == other.OutputParameter.Value; } [JsonConstructor] @@ -56,7 +53,6 @@ public MathsEquationInstance(MathsEquationInstance other) InputType.Value = other.InputType.Value; Equation.Value = other.Equation.Value; OutputParameter.Value = other.OutputParameter.Value; - OutputType.Value = other.OutputType.Value; } } @@ -123,9 +119,7 @@ private void load() new Dimension(GridSizeMode.Absolute, 5), new Dimension(), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 150), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 100) + new Dimension(maxSize: 150) }, RowDimensions = new[] { @@ -172,19 +166,6 @@ private void load() Text = "Output Parameter", Font = FrameworkFont.Regular.With(size: 20) } - }, - null, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Output Type", - Font = FrameworkFont.Regular.With(size: 20) - } } } } @@ -206,9 +187,7 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) new Dimension(GridSizeMode.Absolute, 5), new Dimension(), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 150), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(maxSize: 100) + new Dimension(maxSize: 150) }, RowDimensions = new[] { @@ -261,15 +240,6 @@ protected override void OnInstanceAdd(MathsEquationInstance instance) ValidCurrent = instance.OutputParameter.GetBoundCopy(), PlaceholderText = "Output Parameter", EmptyIsValid = false - }, - null, - new MathsValueTypeInstanceDropdown - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Items = Enum.GetValues(typeof(MathsEquationValueType)).Cast(), - Current = instance.OutputType.GetBoundCopy() } } } diff --git a/VRCOSC.Modules/Maths/MathsModule.cs b/VRCOSC.Modules/Maths/MathsModule.cs index 04dde2a1..ee3de78b 100644 --- a/VRCOSC.Modules/Maths/MathsModule.cs +++ b/VRCOSC.Modules/Maths/MathsModule.cs @@ -1,4 +1,4 @@ -// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. using org.mariuszgromada.math.mxparser; @@ -23,8 +23,8 @@ public MathsModule() protected override void CreateAttributes() { - CreateSetting(MathsSetting.Constants, "Constants", "Define your own constants to reuse in your equations", Array.Empty()); - CreateSetting(MathsSetting.Functions, "Functions", "Define your own functions to reuse in your equations", Array.Empty()); + CreateSetting(MathsSetting.Constants, "Constants", "Define your own constants to reuse in your equations\nChanges to this setting requires a module restart", Array.Empty()); + CreateSetting(MathsSetting.Functions, "Functions", "Define your own functions to reuse in your equations\nChanges to this setting requires a module restart", Array.Empty()); CreateSetting(MathsSetting.Equations, new MathsEquationInstanceListAttribute { @@ -45,7 +45,7 @@ protected override void OnModuleStart() elements.AddRange(GetSettingList(MathsSetting.Functions).Select(function => new Function(function))); } - protected override void OnAnyParameterReceived(ReceivedParameter parameter) + protected override async void OnAnyParameterReceived(ReceivedParameter parameter) { parameterValues[parameter.Name] = parameter; @@ -54,15 +54,36 @@ protected override void OnAnyParameterReceived(ReceivedParameter parameter) var expression = new Expression(instance.Equation.Value, parameterValues.Values.Select(createArgumentForParameterValue).Concat(elements).ToArray()); expression.disableImpliedMultiplicationMode(); - if (expression.getMissingUserDefinedArguments().Any()) + foreach (var missingArgument in expression.getMissingUserDefinedArguments()) { - Log($"Missing argument value for equation {instance.TriggerParameter.Value}"); + var missingArgumentValue = await FindParameterValue(missingArgument); + + if (missingArgumentValue is null) + { + Log($"Could not retrieve missing argument value '{missingArgument}'"); + continue; + } + + if (missingArgumentValue is bool boolValue) + expression.addArguments(new Argument(missingArgument, boolValue)); + else if (missingArgumentValue is int intValue) + expression.addArguments(new Argument(missingArgument, intValue)); + else if (missingArgumentValue is float floatValue) + expression.addArguments(new Argument(missingArgument, floatValue)); + } + + var outputType = await FindParameterType(instance.OutputParameter.Value); + + if (outputType is null) + { + Log($"Could not find output parameter '{instance.OutputParameter.Value}'"); return; } var output = expression.calculate(); - SendParameter(instance.OutputParameter.Value, convertToOutputType(output, instance.OutputType.Value)); + var finalValue = convertToOutputType(output, outputType.Value); + SendParameter(instance.OutputParameter.Value, finalValue); } private static PrimitiveElement createArgumentForParameterValue(ReceivedParameter parameter) @@ -74,15 +95,15 @@ private static PrimitiveElement createArgumentForParameterValue(ReceivedParamete throw new InvalidOperationException("Unknown parameter type"); } - private object convertToOutputType(double value, MathsEquationValueType valueType) + private object convertToOutputType(double value, TypeCode valueType) { try { return valueType switch { - MathsEquationValueType.Bool => Convert.ToBoolean(value), - MathsEquationValueType.Int => Convert.ToInt32(value), - MathsEquationValueType.Float => Convert.ToSingle(value), + TypeCode.Boolean => Convert.ToBoolean(value), + TypeCode.Int32 => Convert.ToInt32(value), + TypeCode.Single => Convert.ToSingle(value), _ => throw new ArgumentOutOfRangeException(nameof(valueType), valueType, null) }; } @@ -92,9 +113,9 @@ private object convertToOutputType(double value, MathsEquationValueType valueTyp return valueType switch { - MathsEquationValueType.Bool => default(bool), - MathsEquationValueType.Int => default(int), - MathsEquationValueType.Float => default(float), + TypeCode.Boolean => default(bool), + TypeCode.Int32 => default(int), + TypeCode.Single => default(float), _ => throw new ArgumentOutOfRangeException(nameof(valueType), valueType, null) }; } From bb24ce6e7773b87849ff49bbbe3818fa38ce5b7e Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 1 Oct 2023 13:57:32 +0100 Subject: [PATCH 06/19] Ensure SpeechToText notifies about not having at least 1 model --- VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs b/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs index ee2566fb..afa3ba38 100644 --- a/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs +++ b/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs @@ -64,6 +64,12 @@ private void initProvider() speechToTextProvider.OnPartialResult += onPartialResult; speechToTextProvider.OnFinalResult += onFinalResult; + if (!GetSettingList(SpeechToTextSetting.ModelLocations).Any()) + { + Log("Please add at least 1 model in the speech to text settings"); + return; + } + speechToTextProvider.Initialise(GetSettingList(SpeechToTextSetting.ModelLocations)[selectedModel].Path.Value); } From 85d543695ee74c910dcb9b2ce7d8cc6c55e5f109 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 1 Oct 2023 14:03:22 +0100 Subject: [PATCH 07/19] Only check zeroconf if VRChat client is open --- VRCOSC.Game/App/AppManager.cs | 9 ++++++++- VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/VRCOSC.Game/App/AppManager.cs b/VRCOSC.Game/App/AppManager.cs index 6add7c9d..2e5d6cc6 100644 --- a/VRCOSC.Game/App/AppManager.cs +++ b/VRCOSC.Game/App/AppManager.cs @@ -143,6 +143,9 @@ private void initialiseVRChat() private void initialiseDelayedTasks() { + // force a check for OscJson to use + VRChat.HasOpenStateChanged(); + Scheduler.AddDelayed(checkForOpenVR, openvr_check_interval.TotalMilliseconds, true); Scheduler.AddDelayed(checkForVRChat, vrchat_check_interval.TotalMilliseconds, true); Scheduler.AddDelayed(checkForOscjson, oscjson_check_interval.TotalMilliseconds, true); @@ -314,7 +317,9 @@ private void checkForOpenVR() => Task.Run(() => private void checkForVRChat() { - if (!configManager.Get(VRCOSCSetting.AutoStartStop) || !VRChat.HasOpenStateChanged()) return; + var newOpenState = VRChat.HasOpenStateChanged(); + + if (!configManager.Get(VRCOSCSetting.AutoStartStop) || !newOpenState) return; if (VRChat.IsClientOpen && State.Value == AppManagerState.Stopped) Start(); if (!VRChat.IsClientOpen && State.Value == AppManagerState.Started) Stop(); @@ -322,6 +327,8 @@ private void checkForVRChat() private async void checkForOscjson() { + if (!VRChat.IsClientOpen) return; + await OSCClient.CheckForVRChatOSCQuery(); } diff --git a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs index 5c7a3437..a6020e7d 100644 --- a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs +++ b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs @@ -56,6 +56,7 @@ public async Task CheckForVRChatOSCQuery() var service = host.Services.Single(s => s.Value.ServiceName.Contains("VRChat-Client")); port = service.Value.Port; + Logger.Log($"Successfully found OscJson port: {port}"); } public async Task FindParameterValue(string parameterName) From b9784f5b854d372150303b4096ae5173bf00c524 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 1 Oct 2023 14:03:40 +0100 Subject: [PATCH 08/19] Guard against wrongly formatted equations --- VRCOSC.Modules/Maths/MathsEquationInstance.cs | 13 +------------ VRCOSC.Modules/Maths/MathsModule.cs | 5 ++++- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/VRCOSC.Modules/Maths/MathsEquationInstance.cs b/VRCOSC.Modules/Maths/MathsEquationInstance.cs index a9b3169e..acceaffe 100644 --- a/VRCOSC.Modules/Maths/MathsEquationInstance.cs +++ b/VRCOSC.Modules/Maths/MathsEquationInstance.cs @@ -26,9 +26,6 @@ public class MathsEquationInstance : IEquatable [JsonProperty("input_parameter")] public Bindable TriggerParameter = new(string.Empty); - [JsonProperty("input_type")] - public Bindable InputType = new(); - [JsonProperty("equation")] public Bindable Equation = new(string.Empty); @@ -39,7 +36,7 @@ public bool Equals(MathsEquationInstance? other) { if (ReferenceEquals(other, null)) return false; - return TriggerParameter.Value == other.TriggerParameter.Value && InputType.Value == other.InputType.Value && Equation.Value == other.Equation.Value && OutputParameter.Value == other.OutputParameter.Value; + return TriggerParameter.Value == other.TriggerParameter.Value && Equation.Value == other.Equation.Value && OutputParameter.Value == other.OutputParameter.Value; } [JsonConstructor] @@ -50,7 +47,6 @@ public MathsEquationInstance() public MathsEquationInstance(MathsEquationInstance other) { TriggerParameter.Value = other.TriggerParameter.Value; - InputType.Value = other.InputType.Value; Equation.Value = other.Equation.Value; OutputParameter.Value = other.OutputParameter.Value; } @@ -322,10 +318,3 @@ private void load() } } } - -public enum MathsEquationValueType -{ - Bool, - Int, - Float -} diff --git a/VRCOSC.Modules/Maths/MathsModule.cs b/VRCOSC.Modules/Maths/MathsModule.cs index ee3de78b..d5c3193b 100644 --- a/VRCOSC.Modules/Maths/MathsModule.cs +++ b/VRCOSC.Modules/Maths/MathsModule.cs @@ -2,6 +2,7 @@ // See the LICENSE file in the repository root for full license text. using org.mariuszgromada.math.mxparser; +using osu.Framework.Extensions.IEnumerableExtensions; using VRCOSC.Game.Modules; using VRCOSC.Game.Modules.Avatar; @@ -40,7 +41,9 @@ protected override void OnModuleStart() instances.Clear(); elements.Clear(); - GetSettingList(MathsSetting.Equations).ForEach(instance => instances.TryAdd(instance.TriggerParameter.Value, instance)); + GetSettingList(MathsSetting.Equations) + .Where(instance => !string.IsNullOrEmpty(instance.Equation.Value) && !string.IsNullOrEmpty(instance.TriggerParameter.Value) && !string.IsNullOrEmpty(instance.OutputParameter.Value)) + .ForEach(instance => instances.TryAdd(instance.TriggerParameter.Value, instance)); elements.AddRange(GetSettingList(MathsSetting.Constants).Select(constant => new Constant(constant))); elements.AddRange(GetSettingList(MathsSetting.Functions).Select(function => new Function(function))); } From 85480f9109089b01ab2c87ed38af52de90fbf6d3 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 1 Oct 2023 14:42:20 +0100 Subject: [PATCH 09/19] Clarify PiShock wording --- VRCOSC.Modules/PiShock/PiShockGroupInstance.cs | 10 +++++----- VRCOSC.Modules/PiShock/PiShockModule.cs | 18 +++++++++--------- .../PiShock/PiShockPhraseInstance.cs | 10 +++++----- .../PiShock/PiShockShockerInstance.cs | 10 +++++----- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/VRCOSC.Modules/PiShock/PiShockGroupInstance.cs b/VRCOSC.Modules/PiShock/PiShockGroupInstance.cs index 08b72bdd..e00e67d0 100644 --- a/VRCOSC.Modules/PiShock/PiShockGroupInstance.cs +++ b/VRCOSC.Modules/PiShock/PiShockGroupInstance.cs @@ -17,7 +17,7 @@ namespace VRCOSC.Modules.PiShock; public class PiShockGroupInstance : IEquatable { [JsonProperty("keys")] - public Bindable Keys = new(string.Empty); + public Bindable Names = new(string.Empty); [JsonConstructor] public PiShockGroupInstance() @@ -26,7 +26,7 @@ public PiShockGroupInstance() public PiShockGroupInstance(PiShockGroupInstance other) { - Keys.Value = other.Keys.Value; + Names.Value = other.Names.Value; } public bool Equals(PiShockGroupInstance? other) @@ -34,7 +34,7 @@ public bool Equals(PiShockGroupInstance? other) if (ReferenceEquals(null, other)) return false; if (!ReferenceEquals(this, other)) return false; - return Keys.Value.Equals(other.Keys.Value); + return Names.Value.Equals(other.Names.Value); } } @@ -125,8 +125,8 @@ public DrawablePiShockGroupInstance(PiShockGroupInstanceListAttribute attributeD CornerRadius = 5, BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, - ValidCurrent = instance.Keys.GetBoundCopy(), - PlaceholderText = "Key,Key2,Key3", + ValidCurrent = instance.Names.GetBoundCopy(), + PlaceholderText = "Name,Name2,Name3", EmptyIsValid = false } } diff --git a/VRCOSC.Modules/PiShock/PiShockModule.cs b/VRCOSC.Modules/PiShock/PiShockModule.cs index 9d1157fa..9149ee3a 100644 --- a/VRCOSC.Modules/PiShock/PiShockModule.cs +++ b/VRCOSC.Modules/PiShock/PiShockModule.cs @@ -44,18 +44,18 @@ protected override void CreateAttributes() CreateSetting(PiShockSetting.Shockers, new PiShockShockerInstanceListAttribute { Name = "Shockers", - Description = "Each instance represents a single shocker using a sharecode\nThe key is used as a reference to create groups of shockers", + Description = "Each instance represents a single shocker using a sharecode\nThe name is used as a readable reference and can be anything you like", Default = new List() }); CreateSetting(PiShockSetting.Groups, new PiShockGroupInstanceListAttribute { Name = "Groups", - Description = "Each instance should contain one or more shocker keys separated by a comma\nA group can be chosen by setting the Group parameter to the left number", + Description = "Each instance should contain one or more shocker names separated by a comma\nA group can be chosen by setting the Group parameter to the left number", Default = new List() }); - CreateSetting(PiShockSetting.EnableVoiceControl, "Enable Voice Control", "Enables voice control using speech to text and a phrase list", false); + CreateSetting(PiShockSetting.EnableVoiceControl, "Enable Voice Control", "Enables voice control using speech to text and the phrase list", false); CreateSetting(PiShockSetting.SpeechModelLocation, "Speech Model Location", "The folder location of the speech model you'd like to use\nRecommended default: vosk-model-small-en-us-0.15", string.Empty, "Download a model", () => OpenUrlExternally("https://alphacephei.com/vosk/models"), () => GetSetting(PiShockSetting.EnableVoiceControl)); CreateSetting(PiShockSetting.SpeechConfidence, "Speech Confidence", "How confident should VOSK be that it's recognised a phrase to execute the action? (%)", 75, 0, 100, () => GetSetting(PiShockSetting.EnableVoiceControl)); @@ -63,7 +63,7 @@ protected override void CreateAttributes() CreateSetting(PiShockSetting.PhraseList, new PiShockPhraseInstanceListAttribute { Name = "Phrase List", - Description = "The list of words or phrases and what to do when they're said\nUse the shocker keys from Shockers to reference sharecodes", + Description = "The list of words or phrases and what to do when they're said", Default = new List(), DependsOn = () => GetSetting(PiShockSetting.EnableVoiceControl) }); @@ -164,12 +164,12 @@ private async void onNewSentenceSpoken(bool success, string sentence) { Log($"Found word: {wordInstance.Phrase.Value}"); - var shockerInstance = getShockerInstanceFromKey(wordInstance.ShockerKey.Value); + var shockerInstance = getShockerInstanceFromKey(wordInstance.ShockerName.Value); if (shockerInstance is null) continue; var response = await piShockProvider!.Execute(GetSetting(PiShockSetting.Username), GetSetting(PiShockSetting.APIKey), shockerInstance.Sharecode.Value, wordInstance.Mode.Value, wordInstance.Duration.Value, wordInstance.Intensity.Value); - Log(response.Success ? $"Executing {wordInstance.Mode.Value} on {wordInstance.ShockerKey.Value} with duration {response.FinalDuration}s and intensity {response.FinalIntensity}%" : response.Message); + Log(response.Success ? $"Executing {wordInstance.Mode.Value} on {wordInstance.ShockerName.Value} with duration {response.FinalDuration}s and intensity {response.FinalIntensity}%" : response.Message); } } @@ -183,7 +183,7 @@ private async void executePiShockMode(PiShockMode mode) return; } - var shockerKeys = groupData.Keys.Value.Split(',').Where(key => !string.IsNullOrEmpty(key)).Select(key => key.Trim()); + var shockerKeys = groupData.Names.Value.Split(',').Where(key => !string.IsNullOrEmpty(key)).Select(key => key.Trim()); foreach (var shockerKey in shockerKeys) { @@ -198,7 +198,7 @@ private async Task sendPiShockData(PiShockMode mode, PiShockShockerInstance inst { var response = await piShockProvider!.Execute(GetSetting(PiShockSetting.Username), GetSetting(PiShockSetting.APIKey), instance.Sharecode.Value, mode, convertedDuration, convertedIntensity); - Log(response.Success ? $"Executing {mode} on {instance.Key.Value} with duration {response.FinalDuration}s and intensity {response.FinalIntensity}%" : response.Message); + Log(response.Success ? $"Executing {mode} on {instance.Name.Value} with duration {response.FinalDuration}s and intensity {response.FinalIntensity}%" : response.Message); if (response.Success) { @@ -213,7 +213,7 @@ private async Task sendPiShockData(PiShockMode mode, PiShockShockerInstance inst private PiShockShockerInstance? getShockerInstanceFromKey(string key) { - var instance = GetSettingList(PiShockSetting.Shockers).SingleOrDefault(shockerInstance => shockerInstance.Key.Value == key); + var instance = GetSettingList(PiShockSetting.Shockers).SingleOrDefault(shockerInstance => shockerInstance.Name.Value == key); if (instance is not null) return instance; diff --git a/VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs b/VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs index 6f6db922..8d8a827c 100644 --- a/VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs +++ b/VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs @@ -26,7 +26,7 @@ public class PiShockPhraseInstance : IEquatable public Bindable Phrase = new(string.Empty); [JsonProperty("shocker_key")] - public Bindable ShockerKey = new(string.Empty); + public Bindable ShockerName = new(string.Empty); [JsonProperty("mode")] public Bindable Mode = new(); @@ -45,7 +45,7 @@ public PiShockPhraseInstance() public PiShockPhraseInstance(PiShockPhraseInstance other) { Phrase.Value = other.Phrase.Value; - ShockerKey.Value = other.ShockerKey.Value; + ShockerName.Value = other.ShockerName.Value; Mode.Value = other.Mode.Value; Duration.Value = other.Duration.Value; Intensity.Value = other.Intensity.Value; @@ -55,7 +55,7 @@ public bool Equals(PiShockPhraseInstance? other) { if (ReferenceEquals(null, other)) return false; - return Phrase.Value.Equals(other.Phrase.Value) && ShockerKey.Value.Equals(other.ShockerKey.Value) && Mode.Value.Equals(other.Mode.Value) && Duration.Value.Equals(other.Duration.Value) && Intensity.Value.Equals(other.Intensity.Value); + return Phrase.Value.Equals(other.Phrase.Value) && ShockerName.Value.Equals(other.ShockerName.Value) && Mode.Value.Equals(other.Mode.Value) && Duration.Value.Equals(other.Duration.Value) && Intensity.Value.Equals(other.Intensity.Value); } } @@ -134,7 +134,7 @@ private void load() { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Key", + Text = "Shocker Name", Font = FrameworkFont.Regular.With(size: 20) } }, @@ -236,7 +236,7 @@ protected override void OnInstanceAdd(PiShockPhraseInstance instance) CornerRadius = 5, BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, - ValidCurrent = instance.ShockerKey.GetBoundCopy(), + ValidCurrent = instance.ShockerName.GetBoundCopy(), PlaceholderText = "Key", EmptyIsValid = false }, diff --git a/VRCOSC.Modules/PiShock/PiShockShockerInstance.cs b/VRCOSC.Modules/PiShock/PiShockShockerInstance.cs index 8cbb90af..6e1b9174 100644 --- a/VRCOSC.Modules/PiShock/PiShockShockerInstance.cs +++ b/VRCOSC.Modules/PiShock/PiShockShockerInstance.cs @@ -18,7 +18,7 @@ namespace VRCOSC.Modules.PiShock; public class PiShockShockerInstance : IEquatable { [JsonProperty("key")] - public Bindable Key = new(string.Empty); + public Bindable Name = new(string.Empty); [JsonProperty("sharecode")] public Bindable Sharecode = new(string.Empty); @@ -30,7 +30,7 @@ public PiShockShockerInstance() public PiShockShockerInstance(PiShockShockerInstance other) { - Key.Value = other.Key.Value; + Name.Value = other.Name.Value; Sharecode.Value = other.Sharecode.Value; } @@ -38,7 +38,7 @@ public bool Equals(PiShockShockerInstance? other) { if (ReferenceEquals(null, other)) return false; - return Key.Value.Equals(other.Key.Value) && Sharecode.Value.Equals(other.Sharecode.Value); + return Name.Value.Equals(other.Name.Value) && Sharecode.Value.Equals(other.Sharecode.Value); } } @@ -98,7 +98,7 @@ private void load() { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Key", + Text = "Name", Font = FrameworkFont.Regular.With(size: 20) } }, @@ -153,7 +153,7 @@ protected override void OnInstanceAdd(PiShockShockerInstance instance) CornerRadius = 5, BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, - ValidCurrent = instance.Key.GetBoundCopy(), + ValidCurrent = instance.Name.GetBoundCopy(), PlaceholderText = "Key", EmptyIsValid = false }, From 31fdffed71bb08e1a6a57dc49a22706fd2ac8244 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 1 Oct 2023 14:42:51 +0100 Subject: [PATCH 10/19] Fix Auto Start/Stop no longer working and cache OscJson port when found --- VRCOSC.Game/App/AppManager.cs | 13 ++++++------ VRCOSC.Game/App/VRChat.cs | 8 +++++--- VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs | 25 +++++++++++++++-------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/VRCOSC.Game/App/AppManager.cs b/VRCOSC.Game/App/AppManager.cs index 2e5d6cc6..35f0928d 100644 --- a/VRCOSC.Game/App/AppManager.cs +++ b/VRCOSC.Game/App/AppManager.cs @@ -143,9 +143,6 @@ private void initialiseVRChat() private void initialiseDelayedTasks() { - // force a check for OscJson to use - VRChat.HasOpenStateChanged(); - Scheduler.AddDelayed(checkForOpenVR, openvr_check_interval.TotalMilliseconds, true); Scheduler.AddDelayed(checkForVRChat, vrchat_check_interval.TotalMilliseconds, true); Scheduler.AddDelayed(checkForOscjson, oscjson_check_interval.TotalMilliseconds, true); @@ -321,13 +318,17 @@ private void checkForVRChat() if (!configManager.Get(VRCOSCSetting.AutoStartStop) || !newOpenState) return; - if (VRChat.IsClientOpen && State.Value == AppManagerState.Stopped) Start(); - if (!VRChat.IsClientOpen && State.Value == AppManagerState.Started) Stop(); + if (VRChat.ClientOpen && State.Value == AppManagerState.Stopped) Start(); + if (!VRChat.ClientOpen && State.Value == AppManagerState.Started) Stop(); } private async void checkForOscjson() { - if (!VRChat.IsClientOpen) return; + if (!VRChat.IsClientOpen()) + { + OSCClient.Reset(); + return; + } await OSCClient.CheckForVRChatOSCQuery(); } diff --git a/VRCOSC.Game/App/VRChat.cs b/VRCOSC.Game/App/VRChat.cs index aebf3d0d..17f41ed4 100644 --- a/VRCOSC.Game/App/VRChat.cs +++ b/VRCOSC.Game/App/VRChat.cs @@ -11,7 +11,7 @@ namespace VRCOSC.Game.App; public class VRChat { - public bool IsClientOpen; + public bool ClientOpen; public Player Player = null!; public AvatarConfig? AvatarConfig; @@ -37,12 +37,14 @@ public void HandleAvatarChange(VRChatOscMessage message) } } + public bool IsClientOpen() => Process.GetProcessesByName("vrchat").Any(); + public bool HasOpenStateChanged() { var clientNewOpenState = Process.GetProcessesByName("vrchat").Any(); - if (clientNewOpenState == IsClientOpen) return false; + if (clientNewOpenState == ClientOpen) return false; - IsClientOpen = clientNewOpenState; + ClientOpen = clientNewOpenState; return true; } } diff --git a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs index a6020e7d..c76b02e8 100644 --- a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs +++ b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs @@ -19,7 +19,7 @@ public class VRChatOscClient : OscClient public Action? OnParameterReceived; private readonly HttpClient client = new(); - private int? port; + public int? QueryPort { get; private set; } public VRChatOscClient() { @@ -36,34 +36,41 @@ public VRChatOscClient() public async Task CheckForVRChatOSCQuery() { + if (QueryPort is not null) return; + var hosts = await ZeroconfResolver.ResolveAsync("_oscjson._tcp.local."); var host = hosts.FirstOrDefault(); if (host is null) { Logger.Log("No OscJson host found"); - port = null; + QueryPort = null; return; } if (!host.Services.Any(s => s.Value.ServiceName.Contains("VRChat-Client"))) { Logger.Log("No VRChat-Client found"); - port = null; + QueryPort = null; return; } var service = host.Services.Single(s => s.Value.ServiceName.Contains("VRChat-Client")); - port = service.Value.Port; - Logger.Log($"Successfully found OscJson port: {port}"); + QueryPort = service.Value.Port; + Logger.Log($"Successfully found OscJson port: {QueryPort}"); + } + + public void Reset() + { + QueryPort = null; } public async Task FindParameterValue(string parameterName) { - if (port is null) return null; + if (QueryPort is null) return null; - var url = $"http://127.0.0.1:{port}/avatar/parameters/{parameterName}"; + var url = $"http://127.0.0.1:{QueryPort}/avatar/parameters/{parameterName}"; var response = await client.GetAsync(new Uri(url)); var content = await response.Content.ReadAsStringAsync(); @@ -82,9 +89,9 @@ public async Task CheckForVRChatOSCQuery() public async Task FindParameterType(string parameterName) { - if (port is null) return null; + if (QueryPort is null) return null; - var url = $"http://127.0.0.1:{port}/avatar/parameters/{parameterName}"; + var url = $"http://127.0.0.1:{QueryPort}/avatar/parameters/{parameterName}"; var response = await client.GetAsync(new Uri(url)); var content = await response.Content.ReadAsStringAsync(); From d53a94e50c078d2a7773a4fc9e1ec5f960bee3cd Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 1 Oct 2023 14:50:39 +0100 Subject: [PATCH 11/19] Clarify TickerTape wording --- .../TickerTape/TickerTapeInstance.cs | 12 +++++------ VRCOSC.Modules/TickerTape/TickerTapeModule.cs | 20 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/VRCOSC.Modules/TickerTape/TickerTapeInstance.cs b/VRCOSC.Modules/TickerTape/TickerTapeInstance.cs index c21d1367..ab7a4d87 100644 --- a/VRCOSC.Modules/TickerTape/TickerTapeInstance.cs +++ b/VRCOSC.Modules/TickerTape/TickerTapeInstance.cs @@ -22,7 +22,7 @@ namespace VRCOSC.Modules.TickerTape; public class TickerTapeInstance : IEquatable { [JsonProperty("key")] - public Bindable Key = new(string.Empty); + public Bindable Name = new(string.Empty); [JsonProperty("text")] public Bindable Text = new(string.Empty); @@ -40,7 +40,7 @@ public bool Equals(TickerTapeInstance? other) { if (ReferenceEquals(other, null)) return false; - return Key.Value == other.Key.Value && Text.Value == other.Text.Value && Direction.Value == other.Direction.Value && ScrollSpeed.Value == other.ScrollSpeed.Value && MaxLength.Value == other.MaxLength.Value; + return Name.Value == other.Name.Value && Text.Value == other.Text.Value && Direction.Value == other.Direction.Value && ScrollSpeed.Value == other.ScrollSpeed.Value && MaxLength.Value == other.MaxLength.Value; } [JsonConstructor] @@ -50,7 +50,7 @@ public TickerTapeInstance() public TickerTapeInstance(TickerTapeInstance other) { - Key.Value = other.Key.Value; + Name.Value = other.Name.Value; Text.Value = other.Text.Value; Direction.Value = other.Direction.Value; ScrollSpeed.Value = other.ScrollSpeed.Value; @@ -120,7 +120,7 @@ private void load() { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Key", + Text = "Name", Font = FrameworkFont.Regular.With(size: 20) } }, @@ -220,8 +220,8 @@ protected override void OnInstanceAdd(TickerTapeInstance instance) CornerRadius = 5, BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, - ValidCurrent = instance.Key.GetBoundCopy(), - PlaceholderText = "Key" + ValidCurrent = instance.Name.GetBoundCopy(), + PlaceholderText = "Name" }, null, new StringTextBox diff --git a/VRCOSC.Modules/TickerTape/TickerTapeModule.cs b/VRCOSC.Modules/TickerTape/TickerTapeModule.cs index ddd58557..09fd3bc2 100644 --- a/VRCOSC.Modules/TickerTape/TickerTapeModule.cs +++ b/VRCOSC.Modules/TickerTape/TickerTapeModule.cs @@ -24,7 +24,7 @@ protected override void CreateAttributes() { new() { - Key = { Value = "Example" }, + Name = { Value = "Example" }, Text = { Value = "ExampleText" }, Direction = { Value = TickerTapeDirection.Right }, ScrollSpeed = { Value = 1 }, @@ -32,7 +32,7 @@ protected override void CreateAttributes() } }, Name = "Texts", - Description = "Each text instance to register for the ChatBox\nText instances can be accessed with the '_Key' suffix\nScroll speed is the number of characters to scroll each update, defined by the chatbox update rate" + Description = "Each text instance to register for the ChatBox\nText instances can be accessed with the '_Name' suffix\nScroll speed is the number of characters to scroll each update, defined by the chatbox update rate" }); CreateVariable(TickerTapeVariable.Text, "Text", "text"); @@ -46,10 +46,10 @@ protected override void OnModuleStart() GetSettingList(TickerTapeSetting.TextList).ForEach(instance => { - if (string.IsNullOrEmpty(instance.Key.Value)) return; + if (string.IsNullOrEmpty(instance.Name.Value)) return; - if (!indexes.TryAdd(instance.Key.Value, 0)) indexes[instance.Key.Value] = 0; - SetVariableValue(TickerTapeVariable.Text, instance.Text.Value, instance.Key.Value); + if (!indexes.TryAdd(instance.Name.Value, 0)) indexes[instance.Name.Value] = 0; + SetVariableValue(TickerTapeVariable.Text, instance.Text.Value, instance.Name.Value); }); ChangeStateTo(TickerTapeState.Default); @@ -60,21 +60,21 @@ private void updateVariables() { GetSettingList(TickerTapeSetting.TextList).ForEach(instance => { - if (string.IsNullOrEmpty(instance.Key.Value) || !indexes.ContainsKey(instance.Key.Value) || string.IsNullOrEmpty(instance.Text.Value)) return; + if (string.IsNullOrEmpty(instance.Name.Value) || !indexes.ContainsKey(instance.Name.Value) || string.IsNullOrEmpty(instance.Text.Value)) return; - var position = indexes[instance.Key.Value].Modulo(instance.Text.Value.Length); + var position = indexes[instance.Name.Value].Modulo(instance.Text.Value.Length); var text = cropAndWrapText(instance.Text.Value, position, instance.MaxLength.Value); - SetVariableValue(TickerTapeVariable.Text, text, instance.Key.Value); + SetVariableValue(TickerTapeVariable.Text, text, instance.Name.Value); switch (instance.Direction.Value) { case TickerTapeDirection.Right: - indexes[instance.Key.Value] += instance.ScrollSpeed.Value; + indexes[instance.Name.Value] += instance.ScrollSpeed.Value; break; case TickerTapeDirection.Left: - indexes[instance.Key.Value] -= instance.ScrollSpeed.Value; + indexes[instance.Name.Value] -= instance.ScrollSpeed.Value; break; } }); From f53d6e2ff78ff1bfd1c2d70debc48fdfa7e31c0e Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 1 Oct 2023 14:58:11 +0100 Subject: [PATCH 12/19] Fix NaN values when there's no media source --- VRCOSC.Game/Providers/Media/MediaState.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/VRCOSC.Game/Providers/Media/MediaState.cs b/VRCOSC.Game/Providers/Media/MediaState.cs index 9199cce6..1aa343df 100644 --- a/VRCOSC.Game/Providers/Media/MediaState.cs +++ b/VRCOSC.Game/Providers/Media/MediaState.cs @@ -40,9 +40,9 @@ public enum MediaPlaybackStatus public class MediaTimelineProperties { - public TimeSpan Start { get; internal set; } - public TimeSpan End { get; internal set; } - public TimeSpan Position { get; internal set; } + public TimeSpan Start { get; internal set; } = TimeSpan.Zero; + public TimeSpan End { get; internal set; } = TimeSpan.FromSeconds(1); + public TimeSpan Position { get; internal set; } = TimeSpan.Zero; public float PositionPercentage => Position.Ticks / (float)End.Ticks; } From a3b6a6f13b5dc45ae1f8f5f1039eff09a8cd1459 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Mon, 2 Oct 2023 14:50:32 +0100 Subject: [PATCH 13/19] Create VoiceRecognition module --- .../VoiceRecognitionModule.cs | 114 +++++++++ .../VoiceRecognitionPhraseInstance.cs | 217 ++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 VRCOSC.Modules/VoiceRecognition/VoiceRecognitionModule.cs create mode 100644 VRCOSC.Modules/VoiceRecognition/VoiceRecognitionPhraseInstance.cs diff --git a/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionModule.cs b/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionModule.cs new file mode 100644 index 00000000..7201f8e2 --- /dev/null +++ b/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionModule.cs @@ -0,0 +1,114 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Avatar; +using VRCOSC.Game.Providers.SpeechToText; + +namespace VRCOSC.Modules.VoiceRecognition; + +[ModuleTitle("Voice Recognition")] +[ModuleDescription("Set parameters when words or phrases are recognised")] +[ModuleAuthor("VolcanicArts", "/~https://github.com/VolcanicArts", "/~https://avatars.githubusercontent.com/u/29819296?v=4")] +[ModuleGroup(ModuleType.General)] +public class VoiceRecognitionModule : AvatarModule +{ + private SpeechToTextProvider? speechToTextProvider; + + protected override void CreateAttributes() + { + CreateSetting(VoiceRecognitionSetting.SpeechModelLocation, "Speech Model Location", "The folder location of the speech model you'd like to use\nRecommended default: vosk-model-small-en-us-0.15", string.Empty, "Download a model", () => OpenUrlExternally("https://alphacephei.com/vosk/models")); + + CreateSetting(VoiceRecognitionSetting.SpeechConfidence, "Speech Confidence", "How confident should VOSK be that it's recognised a phrase to set the parameter? (%)", 75, 0, 100); + + CreateSetting(VoiceRecognitionSetting.PhraseList, new VoiceRecognitionPhraseInstanceListAttribute + { + Name = "Phrase List", + Description = "The list of words or phrases and what parameters to set when they're recognised\nYou are allowed to add the same phrase multiple times to affect multiple parameters\nNote that for boolean parameters use 'true' and 'false'", + Default = new List() + }); + } + + protected override void OnModuleStart() + { + speechToTextProvider = new SpeechToTextProvider(); + speechToTextProvider.OnLog += Log; + speechToTextProvider.OnFinalResult += onNewSentenceSpoken; + speechToTextProvider.Initialise(GetSetting(VoiceRecognitionSetting.SpeechModelLocation)); + } + + protected override void OnModuleStop() + { + speechToTextProvider?.Teardown(); + speechToTextProvider = null; + } + + [ModuleUpdate(ModuleUpdateMode.Custom, false, 5000)] + private void onModuleUpdate() + { + if (speechToTextProvider is not null) + { + speechToTextProvider.Update(); + speechToTextProvider.RequiredConfidence = GetSetting(VoiceRecognitionSetting.SpeechConfidence) / 100f; + } + } + + private async void onNewSentenceSpoken(bool success, string sentence) + { + if (!success) return; + + var phraseInstances = GetSettingList(VoiceRecognitionSetting.PhraseList).Where(wordInstance => sentence.Contains(wordInstance.Phrase.Value, StringComparison.InvariantCultureIgnoreCase)).ToList(); + if (!phraseInstances.Any()) return; + + Log($"Found phrase '{phraseInstances[0].Phrase.Value}'"); + + foreach (var phraseInstance in phraseInstances) + { + var parameterType = await FindParameterType(phraseInstance.ParameterName.Value); + + if (parameterType is null) + { + Log($"Could not find parameter '{phraseInstance.ParameterName.Value}'"); + return; + } + + object value; + + try + { + switch (parameterType) + { + case TypeCode.Boolean: + value = bool.Parse(phraseInstance.Value.Value); + break; + + case TypeCode.Int32: + value = int.Parse(phraseInstance.Value.Value); + break; + + case TypeCode.Single: + value = float.Parse(phraseInstance.Value.Value); + break; + + default: + Log($"Unexpected value type of {parameterType}"); + return; + } + } + catch (Exception) + { + Log($"Could not convert value '{phraseInstance.Value.Value}' for parameter '{phraseInstance.ParameterName.Value}'"); + return; + } + + SendParameter(phraseInstance.ParameterName.Value, value); + } + } + + private enum VoiceRecognitionSetting + { + SpeechModelLocation, + SpeechConfidence, + PhraseList + } +} diff --git a/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionPhraseInstance.cs b/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionPhraseInstance.cs new file mode 100644 index 00000000..eabb594f --- /dev/null +++ b/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionPhraseInstance.cs @@ -0,0 +1,217 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using VRCOSC.Game.Graphics.ModuleAttributes.Attributes; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Text; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Modules.VoiceRecognition; + +public class VoiceRecognitionPhraseInstance : IEquatable +{ + [JsonProperty("phrase")] + public Bindable Phrase = new(string.Empty); + + [JsonProperty("parameter_name")] + public Bindable ParameterName = new(string.Empty); + + [JsonProperty("value")] + public Bindable Value = new(string.Empty); + + [JsonConstructor] + public VoiceRecognitionPhraseInstance() + { + } + + public VoiceRecognitionPhraseInstance(VoiceRecognitionPhraseInstance other) + { + Phrase.Value = other.Phrase.Value; + ParameterName.Value = other.ParameterName.Value; + Value.Value = other.Value.Value; + } + + public bool Equals(VoiceRecognitionPhraseInstance? other) + { + if (ReferenceEquals(null, other)) return false; + + return Phrase.Value.Equals(other.Phrase.Value) && ParameterName.Value.Equals(other.ParameterName.Value) && Value.Value.Equals(other.Value.Value); + } +} + +public class VoiceRecognitionPhraseInstanceListAttribute : ModuleAttributeList +{ + public override Drawable GetAssociatedCard() => new PiShockPhraseInstanceAttributeCardList(this); + + protected override IEnumerable JArrayToType(JArray array) => array.Select(value => new VoiceRecognitionPhraseInstance(value.ToObject()!)).ToList(); + protected override IEnumerable GetClonedDefaults() => Default.Select(defaultValue => new VoiceRecognitionPhraseInstance(defaultValue)).ToList(); +} + +public partial class PiShockPhraseInstanceAttributeCardList : AttributeCardList +{ + public PiShockPhraseInstanceAttributeCardList(VoiceRecognitionPhraseInstanceListAttribute attributeData) + : base(attributeData) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AddToContent(new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Padding = new MarginPadding + { + Right = 35 + }, + Child = new GridContainer + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: 175), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: 100) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable?[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Phrase", + Font = FrameworkFont.Regular.With(size: 20) + } + }, + null, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Parameter Name", + Font = FrameworkFont.Regular.With(size: 20) + } + }, + null, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Value", + Font = FrameworkFont.Regular.With(size: 20) + } + } + } + } + } + }, float.MinValue); + } + + protected override void OnInstanceAdd(VoiceRecognitionPhraseInstance instance) + { + AddToList(new GridContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: 175), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: 100) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable?[] + { + new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + ValidCurrent = instance.Phrase.GetBoundCopy(), + PlaceholderText = "Phrase", + EmptyIsValid = false + }, + null, + new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + ValidCurrent = instance.ParameterName.GetBoundCopy(), + PlaceholderText = "Parameter Name", + EmptyIsValid = false + }, + null, + new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + ValidCurrent = instance.Value.GetBoundCopy(), + PlaceholderText = "Value", + EmptyIsValid = false + } + } + } + }); + } + + protected override VoiceRecognitionPhraseInstance CreateInstance() => new(); +} From dc8fd746cb2219d25c74fa6387a6dafaa3427072 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 3 Oct 2023 23:01:44 +0100 Subject: [PATCH 14/19] Fix boolean decoding not working correctly --- VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs index c76b02e8..94ae0546 100644 --- a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs +++ b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs @@ -76,14 +76,19 @@ public void Reset() var content = await response.Content.ReadAsStringAsync(); var node = JsonConvert.DeserializeObject(content); - if (node is null) return null; + if (node is null) + { + Logger.Log("Could not decode node"); + return null; + } return node.OscType switch { "f" => Convert.ToSingle(node.Value[0]), "i" => Convert.ToInt32(node.Value[0]), - "b" => Convert.ToBoolean(node.Value[0]), - _ => null + "T" => Convert.ToBoolean(node.Value[0]), + "F" => Convert.ToBoolean(node.Value[0]), + _ => throw new InvalidOperationException("Unknown type") }; } @@ -97,14 +102,19 @@ public void Reset() var content = await response.Content.ReadAsStringAsync(); var node = JsonConvert.DeserializeObject(content); - if (node is null) return null; + if (node is null) + { + Logger.Log("Could not decode node"); + return null; + } return node.OscType switch { "f" => TypeCode.Single, "i" => TypeCode.Int32, - "b" => TypeCode.Boolean, - _ => null + "T" => TypeCode.Boolean, + "F" => TypeCode.Boolean, + _ => throw new InvalidOperationException("Unknown type") }; } } From 3bc0aeb978f957d677a9c2677189260fc999e027 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 4 Oct 2023 15:12:16 +0100 Subject: [PATCH 15/19] Dependency bump --- VRCOSC.Game/VRCOSC.Game.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VRCOSC.Game/VRCOSC.Game.csproj b/VRCOSC.Game/VRCOSC.Game.csproj index 30a51761..f7c73128 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -19,8 +19,8 @@ - - + + all From 6af1169d964ba4868fadab7b8a0981f0f67a5e75 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 4 Oct 2023 15:17:42 +0100 Subject: [PATCH 16/19] Add enabled parameter to VoiceRecognition --- .../VoiceRecognitionModule.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionModule.cs b/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionModule.cs index 7201f8e2..bb8c8070 100644 --- a/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionModule.cs +++ b/VRCOSC.Modules/VoiceRecognition/VoiceRecognitionModule.cs @@ -14,6 +14,7 @@ namespace VRCOSC.Modules.VoiceRecognition; public class VoiceRecognitionModule : AvatarModule { private SpeechToTextProvider? speechToTextProvider; + private bool enabled; protected override void CreateAttributes() { @@ -27,6 +28,8 @@ protected override void CreateAttributes() Description = "The list of words or phrases and what parameters to set when they're recognised\nYou are allowed to add the same phrase multiple times to affect multiple parameters\nNote that for boolean parameters use 'true' and 'false'", Default = new List() }); + + CreateParameter(VoiceRecognitionParameter.Enable, ParameterMode.ReadWrite, "VRCOSC/VoiceRecognition/Enable", "Enable", "Enables the recognition when true"); } protected override void OnModuleStart() @@ -34,7 +37,11 @@ protected override void OnModuleStart() speechToTextProvider = new SpeechToTextProvider(); speechToTextProvider.OnLog += Log; speechToTextProvider.OnFinalResult += onNewSentenceSpoken; + speechToTextProvider.RequiredConfidence = GetSetting(VoiceRecognitionSetting.SpeechConfidence) / 100f; speechToTextProvider.Initialise(GetSetting(VoiceRecognitionSetting.SpeechModelLocation)); + + enabled = true; + SendParameter(VoiceRecognitionParameter.Enable, enabled); } protected override void OnModuleStop() @@ -46,10 +53,16 @@ protected override void OnModuleStop() [ModuleUpdate(ModuleUpdateMode.Custom, false, 5000)] private void onModuleUpdate() { - if (speechToTextProvider is not null) + speechToTextProvider?.Update(); + } + + protected override void OnRegisteredParameterReceived(AvatarParameter avatarParameter) + { + switch (avatarParameter.Lookup) { - speechToTextProvider.Update(); - speechToTextProvider.RequiredConfidence = GetSetting(VoiceRecognitionSetting.SpeechConfidence) / 100f; + case VoiceRecognitionParameter.Enable: + enabled = avatarParameter.ValueAs(); + break; } } @@ -111,4 +124,9 @@ private enum VoiceRecognitionSetting SpeechConfidence, PhraseList } + + private enum VoiceRecognitionParameter + { + Enable + } } From cc5d849cdf68c6b0216b716a7905196bb48af063 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Thu, 5 Oct 2023 16:38:08 +0100 Subject: [PATCH 17/19] Escape newline in media artist and title --- VRCOSC.Game/Extensions.cs | 3 +++ VRCOSC.Modules/Media/MediaModule.cs | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/VRCOSC.Game/Extensions.cs b/VRCOSC.Game/Extensions.cs index 54e34d6c..bd5a82de 100644 --- a/VRCOSC.Game/Extensions.cs +++ b/VRCOSC.Game/Extensions.cs @@ -86,7 +86,10 @@ public static class AssemblyExtensions public static class StringExtensions { + public const char ZERO_WIDTH = '\u200B'; + public static string Truncate(this string value, int maxChars) => value.Length <= maxChars ? value : value[..maxChars] + "..."; + public static string EscapeNewLine(this string s) => s.Replace("/n", $"/{ZERO_WIDTH}n"); public static string TrimEnd(this string s, string trimmer) => string.IsNullOrEmpty(s) || string.IsNullOrEmpty(trimmer) || !s.EndsWith(trimmer, StringComparison.OrdinalIgnoreCase) ? s : s[..^trimmer.Length]; } diff --git a/VRCOSC.Modules/Media/MediaModule.cs b/VRCOSC.Modules/Media/MediaModule.cs index e12581f1..2c15ba0f 100644 --- a/VRCOSC.Modules/Media/MediaModule.cs +++ b/VRCOSC.Modules/Media/MediaModule.cs @@ -119,11 +119,11 @@ private void sendUpdatableParameters() [ModuleUpdate(ModuleUpdateMode.ChatBox)] private void updateVariables() { - SetVariableValue(MediaVariable.Title, mediaProvider.State.Title.Truncate(GetSetting(MediaSetting.TruncateTitle))); - SetVariableValue(MediaVariable.Artist, mediaProvider.State.Artist.Truncate(GetSetting(MediaSetting.TruncateArtist))); + SetVariableValue(MediaVariable.Title, mediaProvider.State.Title.Truncate(GetSetting(MediaSetting.TruncateTitle)).EscapeNewLine()); + SetVariableValue(MediaVariable.Artist, mediaProvider.State.Artist.Truncate(GetSetting(MediaSetting.TruncateArtist)).EscapeNewLine()); SetVariableValue(MediaVariable.TrackNumber, mediaProvider.State.TrackNumber.ToString()); - SetVariableValue(MediaVariable.AlbumTitle, mediaProvider.State.AlbumTitle); - SetVariableValue(MediaVariable.AlbumArtist, mediaProvider.State.AlbumArtist); + SetVariableValue(MediaVariable.AlbumTitle, mediaProvider.State.AlbumTitle.Truncate(GetSetting(MediaSetting.TruncateTitle)).EscapeNewLine()); + SetVariableValue(MediaVariable.AlbumArtist, mediaProvider.State.AlbumArtist.Truncate(GetSetting(MediaSetting.TruncateArtist)).EscapeNewLine()); SetVariableValue(MediaVariable.AlbumTrackCount, mediaProvider.State.AlbumTrackCount.ToString()); SetVariableValue(MediaVariable.Volume, (mediaProvider.TryGetVolume() * 100).ToString("##0")); SetVariableValue(MediaVariable.ProgressVisual, getProgressVisual()); From 049769b845b2f50578ea55de313df67006476306 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sat, 7 Oct 2023 16:37:57 +0100 Subject: [PATCH 18/19] Bump framework --- VRCOSC.Game/VRCOSC.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.Game/VRCOSC.Game.csproj b/VRCOSC.Game/VRCOSC.Game.csproj index f7c73128..2afb491d 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -20,7 +20,7 @@ - + all From 7a4d2664c89b916df182c88dfe5f2a99bcc292f3 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sat, 7 Oct 2023 16:40:42 +0100 Subject: [PATCH 19/19] Version bump --- VRCOSC.Desktop/VRCOSC.Desktop.csproj | 4 ++-- VRCOSC.Game/VRCOSC.Game.csproj | 2 +- VRCOSC.Templates/VRCOSC.Templates.csproj | 2 +- .../template-default/TemplateModule/TemplateModule.csproj | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VRCOSC.Desktop/VRCOSC.Desktop.csproj b/VRCOSC.Desktop/VRCOSC.Desktop.csproj index d09b7f2a..1eaa9a88 100644 --- a/VRCOSC.Desktop/VRCOSC.Desktop.csproj +++ b/VRCOSC.Desktop/VRCOSC.Desktop.csproj @@ -6,12 +6,12 @@ game.ico app.manifest 0.0.0 - 2023.928.1 + 2023.1007.0 VRCOSC VolcanicArts VolcanicArts enable - 2023.928.1 + 2023.1007.0 diff --git a/VRCOSC.Game/VRCOSC.Game.csproj b/VRCOSC.Game/VRCOSC.Game.csproj index 2afb491d..a1ae79f2 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -4,7 +4,7 @@ enable 11 VolcanicArts.VRCOSC.SDK - 2023.928.0 + 2023.1007.0 VRCOSC SDK VolcanicArts SDK for creating custom modules with VRCOSC diff --git a/VRCOSC.Templates/VRCOSC.Templates.csproj b/VRCOSC.Templates/VRCOSC.Templates.csproj index 2dda5786..fe9ae16a 100644 --- a/VRCOSC.Templates/VRCOSC.Templates.csproj +++ b/VRCOSC.Templates/VRCOSC.Templates.csproj @@ -11,7 +11,7 @@ true NU5128 - 2023.928.0 + 2023.1007.0 VolcanicArts /~https://github.com/VolcanicArts/VRCOSC /~https://github.com/VolcanicArts/VRCOSC diff --git a/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj b/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj index ce1948ab..24ab7e89 100644 --- a/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj +++ b/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj @@ -7,7 +7,7 @@ - +