From 165b12ca72a8277aff24b3b187772618471e695c Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 30 Aug 2023 01:49:38 +0100 Subject: [PATCH 01/21] Improve ChatBox Text scroll behaviour --- VRCOSC.Game/Extensions.cs | 9 +++++++ .../ChatBoxText/ChatBoxTextModule.cs | 27 +++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/VRCOSC.Game/Extensions.cs b/VRCOSC.Game/Extensions.cs index b29d39e4..fdd79a0c 100644 --- a/VRCOSC.Game/Extensions.cs +++ b/VRCOSC.Game/Extensions.cs @@ -88,3 +88,12 @@ public static class StringExtensions { public static string Truncate(this string value, int maxChars) => value.Length <= maxChars ? value : value[..maxChars] + "..."; } + +public static class IntegerExtensions +{ + public static int Modulo(this int x, int m) + { + var r = x % m; + return r < 0 ? r + m : r; + } +} diff --git a/VRCOSC.Modules/ChatBoxText/ChatBoxTextModule.cs b/VRCOSC.Modules/ChatBoxText/ChatBoxTextModule.cs index c7b46045..37ea5b4e 100644 --- a/VRCOSC.Modules/ChatBoxText/ChatBoxTextModule.cs +++ b/VRCOSC.Modules/ChatBoxText/ChatBoxTextModule.cs @@ -1,6 +1,7 @@ // 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; using VRCOSC.Game.Modules; using VRCOSC.Game.Modules.Avatar; @@ -60,35 +61,31 @@ private void updateVariables() { if (string.IsNullOrEmpty(instance.Key.Value) || !indexes.ContainsKey(instance.Key.Value) || string.IsNullOrEmpty(instance.Text.Value)) return; - var text = instance.Text.Value; + var position = indexes[instance.Key.Value].Modulo(instance.Text.Value.Length); + var text = cropAndWrapText(instance.Text.Value, position, instance.MaxLength.Value); + + SetVariableValue(ChatBoxTextVariable.Text, text, instance.Key.Value); switch (instance.Direction.Value) { case ChatBoxTextDirection.Right: indexes[instance.Key.Value] += instance.ScrollSpeed.Value; - if (indexes[instance.Key.Value] > text.Length - 1) indexes[instance.Key.Value] = 0 + (indexes[instance.Key.Value] - text.Length); break; case ChatBoxTextDirection.Left: indexes[instance.Key.Value] -= instance.ScrollSpeed.Value; - if (indexes[instance.Key.Value] < 1) indexes[instance.Key.Value] = text.Length - (0 - indexes[instance.Key.Value]); break; } - - var index = indexes[instance.Key.Value]; - - if (instance.ScrollSpeed.Value > 0) - { - var tickerText = $"{text}{text}"; - var maxLength = Math.Min(instance.MaxLength.Value, text.Length); - - text = tickerText[index..(maxLength + index)]; - } - - SetVariableValue(ChatBoxTextVariable.Text, text, instance.Key.Value); }); } + private static string cropAndWrapText(string text, int position, int maxLength) + { + var endPos = Math.Min(position + maxLength, text.Length); + var subText = text.Substring(position, endPos - position); + return endPos != text.Length ? subText : subText + text[..Math.Min(maxLength - subText.Length, position)]; + } + private enum ChatBoxTextSetting { TextList From ce7a2f8a8a41af9b15d060157e391aa34de91be2 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 30 Aug 2023 02:04:23 +0100 Subject: [PATCH 02/21] Rename ChatBox Text to Ticker Tape --- VRCOSC.Game/ChatBox/DefaultTimeline.cs | 20 ++------- .../TickerTapeInstance.cs} | 42 +++++++++---------- .../TickerTapeModule.cs} | 39 ++++++++--------- 3 files changed, 44 insertions(+), 57 deletions(-) rename VRCOSC.Modules/{ChatBoxText/ChatBoxTextInstance.cs => TickerTape/TickerTapeInstance.cs} (87%) rename VRCOSC.Modules/{ChatBoxText/ChatBoxTextModule.cs => TickerTape/TickerTapeModule.cs} (67%) diff --git a/VRCOSC.Game/ChatBox/DefaultTimeline.cs b/VRCOSC.Game/ChatBox/DefaultTimeline.cs index 289664b3..43585f30 100644 --- a/VRCOSC.Game/ChatBox/DefaultTimeline.cs +++ b/VRCOSC.Game/ChatBox/DefaultTimeline.cs @@ -14,7 +14,6 @@ public static IEnumerable GenerateDefaultTimeline(ChatBoxManager chatBoxMa yield return generateClockClip(chatBoxManager); yield return generateHwsClip(chatBoxManager); yield return generateWeatherClip(chatBoxManager); - yield return generateChatBoxTextClip(chatBoxManager); yield return generateHeartrateClip(chatBoxManager); yield return generateMediaClip(chatBoxManager); yield return generateSpeechToTextClip(chatBoxManager); @@ -24,7 +23,7 @@ private static Clip generateClockClip(ChatBoxManager chatBoxManager) { var clip = chatBoxManager.CreateClip(); clip.Name.Value = "Clock"; - clip.Priority.Value = chatBoxManager.PriorityCount.Value - 7; + clip.Priority.Value = chatBoxManager.PriorityCount.Value - 6; clip.Start.Value = 0; clip.End.Value = 60; clip.AssociatedModules.Add("clockmodule"); @@ -37,7 +36,7 @@ private static Clip generateHwsClip(ChatBoxManager chatBoxManager) { var clip = chatBoxManager.CreateClip(); clip.Name.Value = "Hardware Stats"; - clip.Priority.Value = chatBoxManager.PriorityCount.Value - 6; + clip.Priority.Value = chatBoxManager.PriorityCount.Value - 5; clip.Start.Value = 0; clip.End.Value = 60; clip.AssociatedModules.Add("hardwarestatsmodule"); @@ -50,7 +49,7 @@ private static Clip generateWeatherClip(ChatBoxManager chatBoxManager) { var clip = chatBoxManager.CreateClip(); clip.Name.Value = "Weather"; - clip.Priority.Value = chatBoxManager.PriorityCount.Value - 5; + clip.Priority.Value = chatBoxManager.PriorityCount.Value - 4; clip.Start.Value = 0; clip.End.Value = 60; clip.AssociatedModules.Add("weathermodule"); @@ -59,19 +58,6 @@ private static Clip generateWeatherClip(ChatBoxManager chatBoxManager) return clip; } - private static Clip generateChatBoxTextClip(ChatBoxManager chatBoxManager) - { - var clip = chatBoxManager.CreateClip(); - clip.Name.Value = "ChatBox Text"; - clip.Priority.Value = chatBoxManager.PriorityCount.Value - 4; - clip.Start.Value = 0; - clip.End.Value = 60; - clip.AssociatedModules.Add("chatboxtextmodule"); - clip.GetStateFor("chatboxtextmodule", "default")!.Enabled.Value = true; - - return clip; - } - private static Clip generateHeartrateClip(ChatBoxManager chatBoxManager) { var clip = chatBoxManager.CreateClip(); diff --git a/VRCOSC.Modules/ChatBoxText/ChatBoxTextInstance.cs b/VRCOSC.Modules/TickerTape/TickerTapeInstance.cs similarity index 87% rename from VRCOSC.Modules/ChatBoxText/ChatBoxTextInstance.cs rename to VRCOSC.Modules/TickerTape/TickerTapeInstance.cs index b48ace1c..c21d1367 100644 --- a/VRCOSC.Modules/ChatBoxText/ChatBoxTextInstance.cs +++ b/VRCOSC.Modules/TickerTape/TickerTapeInstance.cs @@ -17,9 +17,9 @@ using VRCOSC.Game.Graphics.UI.Text; using VRCOSC.Game.Modules.Attributes; -namespace VRCOSC.Modules.ChatBoxText; +namespace VRCOSC.Modules.TickerTape; -public class ChatBoxTextInstance : IEquatable +public class TickerTapeInstance : IEquatable { [JsonProperty("key")] public Bindable Key = new(string.Empty); @@ -28,7 +28,7 @@ public class ChatBoxTextInstance : IEquatable public Bindable Text = new(string.Empty); [JsonProperty("direction")] - public Bindable Direction = new(); + public Bindable Direction = new(); [JsonProperty("scroll_speed")] public Bindable ScrollSpeed = new(); @@ -36,7 +36,7 @@ public class ChatBoxTextInstance : IEquatable [JsonProperty("max_length")] public Bindable MaxLength = new(); - public bool Equals(ChatBoxTextInstance? other) + public bool Equals(TickerTapeInstance? other) { if (ReferenceEquals(other, null)) return false; @@ -44,11 +44,11 @@ public bool Equals(ChatBoxTextInstance? other) } [JsonConstructor] - public ChatBoxTextInstance() + public TickerTapeInstance() { } - public ChatBoxTextInstance(ChatBoxTextInstance other) + public TickerTapeInstance(TickerTapeInstance other) { Key.Value = other.Key.Value; Text.Value = other.Text.Value; @@ -58,17 +58,17 @@ public ChatBoxTextInstance(ChatBoxTextInstance other) } } -public class ChatBoxTextInstanceListAttribute : ModuleAttributeList +public class TickerTapeInstanceListAttribute : ModuleAttributeList { - public override Drawable GetAssociatedCard() => new ChatBoxTextInstanceAttributeCardList(this); + public override Drawable GetAssociatedCard() => new TickerTapeInstanceAttributeCardList(this); - protected override IEnumerable JArrayToType(JArray array) => array.Select(value => new ChatBoxTextInstance(value.ToObject()!)).ToList(); - protected override IEnumerable GetClonedDefaults() => Default.Select(defaultValue => new ChatBoxTextInstance(defaultValue)).ToList(); + protected override IEnumerable JArrayToType(JArray array) => array.Select(value => new TickerTapeInstance(value.ToObject()!)).ToList(); + protected override IEnumerable GetClonedDefaults() => Default.Select(defaultValue => new TickerTapeInstance(defaultValue)).ToList(); } -public partial class ChatBoxTextInstanceAttributeCardList : AttributeCardList +public partial class TickerTapeInstanceAttributeCardList : AttributeCardList { - public ChatBoxTextInstanceAttributeCardList(ChatBoxTextInstanceListAttribute attributeData) + public TickerTapeInstanceAttributeCardList(TickerTapeInstanceListAttribute attributeData) : base(attributeData) { } @@ -182,7 +182,7 @@ private void load() }, float.MinValue); } - protected override void OnInstanceAdd(ChatBoxTextInstance instance) + protected override void OnInstanceAdd(TickerTapeInstance instance) { AddToList(new GridContainer { @@ -238,12 +238,12 @@ protected override void OnInstanceAdd(ChatBoxTextInstance instance) PlaceholderText = "Text" }, null, - new ChatBoxTextInstanceDropdown + new TickerTapeInstanceDropdown { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, - Items = Enum.GetValues(typeof(ChatBoxTextDirection)).Cast(), + Items = Enum.GetValues(typeof(TickerTapeDirection)).Cast(), Current = instance.Direction.GetBoundCopy() }, null, @@ -279,14 +279,14 @@ protected override void OnInstanceAdd(ChatBoxTextInstance instance) }); } - protected override ChatBoxTextInstance CreateInstance() => new(); + protected override TickerTapeInstance CreateInstance() => new(); } -public partial class ChatBoxTextInstanceDropdown : VRCOSCDropdown +public partial class TickerTapeInstanceDropdown : VRCOSCDropdown { - protected override DropdownHeader CreateHeader() => new VRCOSCSettingsDropdownHeader(); + protected override DropdownHeader CreateHeader() => new TickerTapeDropdownHeader(); - public partial class VRCOSCSettingsDropdownHeader : DropdownHeader + public partial class TickerTapeDropdownHeader : DropdownHeader { protected override LocalisableString Label { @@ -297,7 +297,7 @@ protected override LocalisableString Label protected readonly SpriteText Text; public readonly SpriteIcon Icon; - public VRCOSCSettingsDropdownHeader() + public TickerTapeDropdownHeader() { Foreground.Padding = new MarginPadding(10); @@ -356,7 +356,7 @@ private void load() } } -public enum ChatBoxTextDirection +public enum TickerTapeDirection { Right, Left diff --git a/VRCOSC.Modules/ChatBoxText/ChatBoxTextModule.cs b/VRCOSC.Modules/TickerTape/TickerTapeModule.cs similarity index 67% rename from VRCOSC.Modules/ChatBoxText/ChatBoxTextModule.cs rename to VRCOSC.Modules/TickerTape/TickerTapeModule.cs index 37ea5b4e..73616c29 100644 --- a/VRCOSC.Modules/ChatBoxText/ChatBoxTextModule.cs +++ b/VRCOSC.Modules/TickerTape/TickerTapeModule.cs @@ -5,27 +5,28 @@ using VRCOSC.Game.Modules; using VRCOSC.Game.Modules.Avatar; -namespace VRCOSC.Modules.ChatBoxText; +namespace VRCOSC.Modules.TickerTape; -[ModuleTitle("ChatBox Text")] -[ModuleDescription("Display custom text and animate it for the ChatBox")] +[ModuleTitle("Ticker Tape")] +[ModuleDescription("Display and animate text for the ChatBox")] [ModuleAuthor("VolcanicArts", "https://github.com/VolcanicArts", "https://avatars.githubusercontent.com/u/29819296?v=4")] [ModuleGroup(ModuleType.General)] -public class ChatBoxTextModule : ChatBoxModule +[ModuleLegacy("chatboxtextmodule")] +public class TickerTapeModule : ChatBoxModule { private readonly Dictionary indexes = new(); protected override void CreateAttributes() { - CreateSetting(ChatBoxTextSetting.TextList, new ChatBoxTextInstanceListAttribute + CreateSetting(TickerTapeSetting.TextList, new TickerTapeInstanceListAttribute { - Default = new List + Default = new List { new() { Key = { Value = "Example" }, Text = { Value = "ExampleText" }, - Direction = { Value = ChatBoxTextDirection.Right }, + Direction = { Value = TickerTapeDirection.Right }, ScrollSpeed = { Value = 1 }, MaxLength = { Value = 8 } } @@ -34,45 +35,45 @@ protected override void CreateAttributes() 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 (every 1.5 seconds)" }); - CreateVariable(ChatBoxTextVariable.Text, "Text", "text"); + CreateVariable(TickerTapeVariable.Text, "Text", "text"); - CreateState(ChatBoxTextState.Default, "Default", $"{GetVariableFormat(ChatBoxTextVariable.Text, "Example")}"); + CreateState(TickerTapeState.Default, "Default", $"{GetVariableFormat(TickerTapeVariable.Text, "Example")}"); } protected override void OnModuleStart() { indexes.Clear(); - GetSettingList(ChatBoxTextSetting.TextList).ForEach(instance => + GetSettingList(TickerTapeSetting.TextList).ForEach(instance => { if (string.IsNullOrEmpty(instance.Key.Value)) return; if (!indexes.TryAdd(instance.Key.Value, 0)) indexes[instance.Key.Value] = 0; - SetVariableValue(ChatBoxTextVariable.Text, instance.Text.Value, instance.Key.Value); + SetVariableValue(TickerTapeVariable.Text, instance.Text.Value, instance.Key.Value); }); - ChangeStateTo(ChatBoxTextState.Default); + ChangeStateTo(TickerTapeState.Default); } [ModuleUpdate(ModuleUpdateMode.ChatBox)] private void updateVariables() { - GetSettingList(ChatBoxTextSetting.TextList).ForEach(instance => + GetSettingList(TickerTapeSetting.TextList).ForEach(instance => { if (string.IsNullOrEmpty(instance.Key.Value) || !indexes.ContainsKey(instance.Key.Value) || string.IsNullOrEmpty(instance.Text.Value)) return; var position = indexes[instance.Key.Value].Modulo(instance.Text.Value.Length); var text = cropAndWrapText(instance.Text.Value, position, instance.MaxLength.Value); - SetVariableValue(ChatBoxTextVariable.Text, text, instance.Key.Value); + SetVariableValue(TickerTapeVariable.Text, text, instance.Key.Value); switch (instance.Direction.Value) { - case ChatBoxTextDirection.Right: + case TickerTapeDirection.Right: indexes[instance.Key.Value] += instance.ScrollSpeed.Value; break; - case ChatBoxTextDirection.Left: + case TickerTapeDirection.Left: indexes[instance.Key.Value] -= instance.ScrollSpeed.Value; break; } @@ -86,17 +87,17 @@ private static string cropAndWrapText(string text, int position, int maxLength) return endPos != text.Length ? subText : subText + text[..Math.Min(maxLength - subText.Length, position)]; } - private enum ChatBoxTextSetting + private enum TickerTapeSetting { TextList } - private enum ChatBoxTextState + private enum TickerTapeState { Default } - private enum ChatBoxTextVariable + private enum TickerTapeVariable { Text } From cab67ca84b8d6d699f59bac5981408c1f79b9c4d Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 30 Aug 2023 02:04:49 +0100 Subject: [PATCH 03/21] Allow clip state formats to migrate module name --- VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs | 1 + VRCOSC.Game/Extensions.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs b/VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs index 46552649..240e8b7e 100644 --- a/VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs +++ b/VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs @@ -57,6 +57,7 @@ protected override bool ExecuteAfterDeserialisation(SerialisableTimeline data) if (index == -1) return; clipState.States[index].Module = migration.SerialisedName; + clipState.Format = clipState.Format.Replace(migration.LegacySerialisedName.TrimEnd("module"), migration.SerialisedName.TrimEnd("module")); migrationOccurred = true; }); }); diff --git a/VRCOSC.Game/Extensions.cs b/VRCOSC.Game/Extensions.cs index fdd79a0c..54e34d6c 100644 --- a/VRCOSC.Game/Extensions.cs +++ b/VRCOSC.Game/Extensions.cs @@ -87,6 +87,7 @@ public static class AssemblyExtensions public static class StringExtensions { public static string Truncate(this string value, int maxChars) => value.Length <= maxChars ? value : value[..maxChars] + "..."; + public static string TrimEnd(this string s, string trimmer) => string.IsNullOrEmpty(s) || string.IsNullOrEmpty(trimmer) || !s.EndsWith(trimmer, StringComparison.OrdinalIgnoreCase) ? s : s[..^trimmer.Length]; } public static class IntegerExtensions From 96f12e1535de73e74942b2199678a21b3fecfe47 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 30 Aug 2023 18:32:14 +0100 Subject: [PATCH 04/21] Allow IntTextBox to define a minimum and maximum value --- VRCOSC.Game/Graphics/UI/Text/IntTextBox.cs | 4 +++- VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/VRCOSC.Game/Graphics/UI/Text/IntTextBox.cs b/VRCOSC.Game/Graphics/UI/Text/IntTextBox.cs index c67db9f4..6436f994 100644 --- a/VRCOSC.Game/Graphics/UI/Text/IntTextBox.cs +++ b/VRCOSC.Game/Graphics/UI/Text/IntTextBox.cs @@ -6,13 +6,15 @@ namespace VRCOSC.Game.Graphics.UI.Text; public partial class IntTextBox : ValidationTextBox { public bool AllowNegative { get; init; } = false; + public int? Minimum { get; init; } + public int? Maximum { get; init; } public IntTextBox() { EmptyIsValid = false; } - protected override bool IsTextValid(string text) => int.TryParse(text, out var intText) && (intText >= 0 || AllowNegative); + protected override bool IsTextValid(string text) => int.TryParse(text, out var intText) && (intText >= 0 || AllowNegative) && (Minimum is null || intText >= Minimum) && (Maximum is null || intText <= Maximum); protected override int GetConvertedText() => int.Parse(Current.Value); } diff --git a/VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs b/VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs index 6835d132..6f6db922 100644 --- a/VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs +++ b/VRCOSC.Modules/PiShock/PiShockPhraseInstance.cs @@ -35,7 +35,7 @@ public class PiShockPhraseInstance : IEquatable public Bindable Duration = new(1); [JsonProperty("intensity")] - public Bindable Intensity = new(); + public Bindable Intensity = new(1); [JsonConstructor] public PiShockPhraseInstance() @@ -262,7 +262,9 @@ protected override void OnInstanceAdd(PiShockPhraseInstance instance) BorderThickness = 2, ValidCurrent = instance.Duration.GetBoundCopy(), PlaceholderText = "Duration", - EmptyIsValid = false + EmptyIsValid = false, + Minimum = 1, + Maximum = 15 }, null, new IntTextBox @@ -277,7 +279,9 @@ protected override void OnInstanceAdd(PiShockPhraseInstance instance) BorderThickness = 2, ValidCurrent = instance.Intensity.GetBoundCopy(), PlaceholderText = "Intensity", - EmptyIsValid = false + EmptyIsValid = false, + Minimum = 1, + Maximum = 100 } } } From daf3b49903cd314d276f8a7c7fcf243c663584cc Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 30 Aug 2023 18:34:38 +0100 Subject: [PATCH 05/21] Clarify TickerTape update rate --- VRCOSC.Modules/TickerTape/TickerTapeModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.Modules/TickerTape/TickerTapeModule.cs b/VRCOSC.Modules/TickerTape/TickerTapeModule.cs index 73616c29..ddd58557 100644 --- a/VRCOSC.Modules/TickerTape/TickerTapeModule.cs +++ b/VRCOSC.Modules/TickerTape/TickerTapeModule.cs @@ -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 (every 1.5 seconds)" + 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" }); CreateVariable(TickerTapeVariable.Text, "Text", "text"); From 6b0987bcc4d695abcae9d831eb87a829a2fa29ab Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 30 Aug 2023 19:04:47 +0100 Subject: [PATCH 06/21] Add auto-updating unicode character to show time as a symbol --- VRCOSC.Modules/Clock/ClockModule.cs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/VRCOSC.Modules/Clock/ClockModule.cs b/VRCOSC.Modules/Clock/ClockModule.cs index 86466944..a5cc0f17 100644 --- a/VRCOSC.Modules/Clock/ClockModule.cs +++ b/VRCOSC.Modules/Clock/ClockModule.cs @@ -33,6 +33,7 @@ protected override void CreateAttributes() CreateVariable(ClockVariable.Minutes, "Minutes", "m"); CreateVariable(ClockVariable.Seconds, "Seconds", "s"); CreateVariable(ClockVariable.Period, "AM/PM", "period"); + CreateVariable(ClockVariable.Symbol, "Symbol", "symbol"); CreateState(ClockState.Default, "Default", $"Local Time/v{GetVariableFormat(ClockVariable.Hours)}:{GetVariableFormat(ClockVariable.Minutes)}{GetVariableFormat(ClockVariable.Period)}"); } @@ -81,6 +82,28 @@ private void updateVariables() SetVariableValue(ClockVariable.Minutes, minuteText); SetVariableValue(ClockVariable.Seconds, secondText); SetVariableValue(ClockVariable.Period, periodText); + SetVariableValue(ClockVariable.Symbol, getClockSymbol(time)); + } + + private static string getClockSymbol(DateTime time) + { + var offset = getClockSymbolCharacter(time); + + var surrogate1 = 0xD800 + ((offset - 0x10000) >> 10); + var surrogate2 = 0xDC00 + ((offset - 0x10000) & 0x3FF); + + var newCodePoint = ((surrogate1 - 0xD800) << 10) + (surrogate2 - 0xDC00) + 0x10000; + + return char.ConvertFromUtf32(newCodePoint); + } + + private static int getClockSymbolCharacter(DateTime time) + { + const int base_code_point = 128336; + const int half_past_base = 128348; + + var hour = (time.Hour + 11) % 12; + return time.Minute >= 30 ? half_past_base + hour : base_code_point + hour; } private static float getSmoothedSeconds(DateTime time) => time.Second + time.Millisecond / 1000f; @@ -126,7 +149,8 @@ private enum ClockVariable Hours, Minutes, Seconds, - Period + Period, + Symbol } private enum ClockMode From d13db9bf360189e4af390fbad5bc6e83887dfcef Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 30 Aug 2023 19:12:36 +0100 Subject: [PATCH 07/21] Fix logging sharecode instead of shocker name --- VRCOSC.Modules/PiShock/PiShockModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.Modules/PiShock/PiShockModule.cs b/VRCOSC.Modules/PiShock/PiShockModule.cs index 8fae7eb3..9d1157fa 100644 --- a/VRCOSC.Modules/PiShock/PiShockModule.cs +++ b/VRCOSC.Modules/PiShock/PiShockModule.cs @@ -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.Sharecode.Value} with duration {response.FinalDuration}s and intensity {response.FinalIntensity}%" : response.Message); + Log(response.Success ? $"Executing {mode} on {instance.Key.Value} with duration {response.FinalDuration}s and intensity {response.FinalIntensity}%" : response.Message); if (response.Success) { From 9eac541c7726e9fc02944deeb0f9e3c0397e7670 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 30 Aug 2023 23:38:51 +0100 Subject: [PATCH 08/21] Alter notification wording --- VRCOSC.Game/VRCOSCGame.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VRCOSC.Game/VRCOSCGame.cs b/VRCOSC.Game/VRCOSCGame.cs index 27c24eb9..5cd2decf 100644 --- a/VRCOSC.Game/VRCOSCGame.cs +++ b/VRCOSC.Game/VRCOSCGame.cs @@ -109,7 +109,7 @@ protected override void LoadComplete() notificationContainer.Notify(new TimedNotification { Title = "Join The Community!", - Description = "Click to join the Discord server", + Description = "Click to join the Discord server!", Icon = FontAwesome.Brands.Discord, Colour = Colour4.FromHex("7289DA"), ClickCallback = () => host.OpenUrlExternally(discord_invite_url), @@ -119,7 +119,7 @@ protected override void LoadComplete() notificationContainer.Notify(new TimedNotification { Title = "Enjoying the app?", - Description = "Click to buy me a coffee", + Description = "Click to support me!", Icon = FontAwesome.Solid.Coffee, Colour = Colour4.FromHex("ff5f5f"), ClickCallback = () => host.OpenUrlExternally(kofi_url), From 3933c5e05ad985f0a7e27a6d253bfe6c8984ac7e Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Fri, 8 Sep 2023 13:36:40 +0100 Subject: [PATCH 09/21] Allow SpeechToText to have multiple models --- .../SpeechToText/SpeechToTextModelInstance.cs | 171 ++++++++++++++++++ .../SpeechToText/SpeechToTextModule.cs | 50 ++++- 2 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs diff --git a/VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs b/VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs new file mode 100644 index 00000000..e23e8fd4 --- /dev/null +++ b/VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs @@ -0,0 +1,171 @@ +using System.Collections.Specialized; +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.Platform; +using VRCOSC.Game.Graphics.ModuleAttributes.Attributes; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Button; +using VRCOSC.Game.Graphics.UI.Text; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Modules.SpeechToText; + +public class SpeechToTextModelInstance : IEquatable +{ + [JsonProperty("path")] + public Bindable Path = new(string.Empty); + + [JsonConstructor] + public SpeechToTextModelInstance() + { + } + + public SpeechToTextModelInstance(SpeechToTextModelInstance other) + { + Path.Value = other.Path.Value; + } + + public bool Equals(SpeechToTextModelInstance? other) + { + if (ReferenceEquals(null, other)) return false; + if (!ReferenceEquals(this, other)) return false; + + return Path.Value.Equals(other.Path.Value); + } +} + +public class SpeechToTextModelInstanceListAttribute : ModuleAttributeList +{ + public override Drawable GetAssociatedCard() => new SpeechToTextModelInstanceAttributeCardList(this); + + protected override IEnumerable JArrayToType(JArray array) => array.Select(value => new SpeechToTextModelInstance(value.ToObject()!)).ToList(); + protected override IEnumerable GetClonedDefaults() => Default.Select(defaultValue => new SpeechToTextModelInstance(defaultValue)).ToList(); +} + +public partial class SpeechToTextModelInstanceAttributeCardList : AttributeCardList +{ + [Resolved] + private GameHost host { get; set; } = null!; + + private readonly List localList = new(); + + public SpeechToTextModelInstanceAttributeCardList(SpeechToTextModelInstanceListAttribute attributeData) + : base(attributeData) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Add(new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Width = 0.75f, + Child = new TextButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Text = "Download a model", + CornerRadius = 5, + FontSize = 22, + Action = () => host.OpenUrlExternally("https://alphacephei.com/vosk/models"), + BackgroundColour = ThemeManager.Current[ThemeAttribute.Action] + } + }); + } + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + localList.RemoveAll(instance => (e.OldItems?.Contains(instance) ?? false) && (!e.NewItems?.Contains(instance) ?? false)); + localList.ForEach(instance => instance.UpdatePositionText()); + } + + protected override void OnInstanceAdd(SpeechToTextModelInstance instance) + { + var drawablePiShockGroupInstance = new DrawableSpeechToTextModelInstance(AttributeData, instance); + localList.Add(drawablePiShockGroupInstance); + AddToList(drawablePiShockGroupInstance); + } + + protected override SpeechToTextModelInstance CreateInstance() => new(); + + public partial class DrawableSpeechToTextModelInstance : GridContainer + { + private readonly SpeechToTextModelInstanceListAttribute attributeData; + private readonly SpeechToTextModelInstance instance; + private readonly StringTextBox positionTextBox; + + public DrawableSpeechToTextModelInstance(SpeechToTextModelInstanceListAttribute attributeData, SpeechToTextModelInstance instance) + { + this.attributeData = attributeData; + this.instance = instance; + + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + ColumnDimensions = new[] + { + new Dimension(maxSize: 50), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(), + }; + + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }; + + Content = new[] + { + new Drawable?[] + { + positionTextBox = new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + ReadOnly = true + }, + 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.Path.GetBoundCopy(), + PlaceholderText = "C:/Some/Folder/Path", + EmptyIsValid = false + } + } + }; + + UpdatePositionText(); + } + + public void UpdatePositionText() + { + var position = attributeData.Attribute.IndexOf(instance); + positionTextBox.Text = position.ToString(); + } + } +} diff --git a/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs b/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs index 9beeabfe..ee2566fb 100644 --- a/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs +++ b/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs @@ -18,15 +18,23 @@ public class SpeechToTextModule : ChatBoxModule private bool listening; private bool playerMuted; + private int selectedModel; protected override void CreateAttributes() { - CreateSetting(SpeechToTextSetting.ModelLocation, "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(SpeechToTextSetting.ModelLocations, new SpeechToTextModelInstanceListAttribute + { + Name = "Model Locations", + Description = "The folder locations of the speech models you'd like to use\nRecommended default: vosk-model-small-en-us-0.15\nChanging the Model parameter will switch models at runtime", + Default = new List() + }); + CreateSetting(SpeechToTextSetting.FollowMute, "Follow Mute", "Only run recognition when you're muted", false); CreateSetting(SpeechToTextSetting.Confidence, "Confidence", "How confident should VOSK be to push a result to the ChatBox? (%)", 75, 0, 100); CreateParameter(SpeechToTextParameter.Reset, ParameterMode.Read, "VRCOSC/SpeechToText/Reset", "Reset", "Manually reset the state to idle to remove the generated text from the ChatBox"); CreateParameter(SpeechToTextParameter.Listen, ParameterMode.ReadWrite, "VRCOSC/SpeechToText/Listen", "Listen", "Whether Speech To Text is currently listening"); + CreateParameter(SpeechToTextParameter.Model, ParameterMode.ReadWrite, "VRCOSC/SpeechToText/Model", "Selected Model", "The (0th based) index of the model you'd like to use. This allows you to switch models in real time"); CreateVariable(SpeechToTextVariable.Text, "Text", "text"); @@ -38,6 +46,17 @@ protected override void CreateAttributes() } protected override void OnModuleStart() + { + initProvider(); + + listening = true; + selectedModel = 0; + resetState(); + SendParameter(SpeechToTextParameter.Listen, listening); + SendParameter(SpeechToTextParameter.Model, selectedModel); + } + + private void initProvider() { speechToTextProvider = new SpeechToTextProvider(); speechToTextProvider.OnLog += Log; @@ -45,16 +64,13 @@ protected override void OnModuleStart() speechToTextProvider.OnPartialResult += onPartialResult; speechToTextProvider.OnFinalResult += onFinalResult; - speechToTextProvider.Initialise(GetSetting(SpeechToTextSetting.ModelLocation)); - listening = true; - resetState(); - SendParameter(SpeechToTextParameter.Listen, listening); + speechToTextProvider.Initialise(GetSettingList(SpeechToTextSetting.ModelLocations)[selectedModel].Path.Value); } [ModuleUpdate(ModuleUpdateMode.Custom, false, 5000)] private void onModuleUpdate() { - speechToTextProvider!.Update(); + speechToTextProvider?.Update(); } protected override void OnPlayerUpdate() @@ -72,7 +88,7 @@ protected override void OnModuleStop() speechToTextProvider = null; } - protected override void OnRegisteredParameterReceived(AvatarParameter parameter) + protected override async void OnRegisteredParameterReceived(AvatarParameter parameter) { switch (parameter.Lookup) { @@ -83,6 +99,21 @@ protected override void OnRegisteredParameterReceived(AvatarParameter parameter) case SpeechToTextParameter.Listen: listening = parameter.ValueAs(); break; + + case SpeechToTextParameter.Model: + var modelIndex = parameter.ValueAs(); + + if (selectedModel != modelIndex) + { + Log($"Model change requested. Changing to model at index {modelIndex}"); + selectedModel = modelIndex; + speechToTextProvider?.Teardown(); + speechToTextProvider = null; + await Task.Delay(1000); + initProvider(); + } + + break; } } @@ -124,7 +155,7 @@ private void resetState() private enum SpeechToTextSetting { - ModelLocation, + ModelLocations, FollowMute, Confidence } @@ -132,7 +163,8 @@ private enum SpeechToTextSetting private enum SpeechToTextParameter { Reset, - Listen + Listen, + Model } private enum SpeechToTextState From dba03930b4f37c692c6fc8a1eb1947d93070d987 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Fri, 8 Sep 2023 13:42:43 +0100 Subject: [PATCH 10/21] 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 515c8a14..f60ed642 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -20,7 +20,7 @@ - + all From d477a44bfb9962c33015a8630205d71b1d09df4a Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 17 Sep 2023 10:16:38 +0100 Subject: [PATCH 11/21] Increase heartrate reconnection limit --- VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs b/VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs index 7fc84362..162fe490 100644 --- a/VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs +++ b/VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs @@ -14,7 +14,7 @@ namespace VRCOSC.Game.Modules.Bases.Heartrate; public abstract class HeartrateModule : ChatBoxModule where T : HeartrateProvider { private const int reconnection_delay = 2000; - private const int reconnection_limit = 3; + private const int reconnection_limit = 30; protected T? HeartrateProvider; From d05c440d9ce2a1f4e08015822f07fc2ee57016c7 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 17 Sep 2023 10:17:00 +0100 Subject: [PATCH 12/21] Fix typo --- VRCOSC.Game/Modules/Module.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.Game/Modules/Module.cs b/VRCOSC.Game/Modules/Module.cs index 89ac857b..6f583396 100644 --- a/VRCOSC.Game/Modules/Module.cs +++ b/VRCOSC.Game/Modules/Module.cs @@ -407,7 +407,7 @@ protected T GetSetting(Enum lookup) #region Parameters /// - /// Allows for sending a parameter that hasn't been registed. Only use this when absolutely necessary + /// Allows for sending a parameter that hasn't been registered. Only use this when absolutely necessary /// protected void SendParameter(string parameterName, T value) where T : struct { From db8f35043acd90628d5668d4eaa71951f0285b70 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 17 Sep 2023 10:18:15 +0100 Subject: [PATCH 13/21] Ensure absent modules that don't exist are also removed --- VRCOSC.Game/ChatBox/Clips/Clip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.Game/ChatBox/Clips/Clip.cs b/VRCOSC.Game/ChatBox/Clips/Clip.cs index 5ac3744e..a2ed30f9 100644 --- a/VRCOSC.Game/ChatBox/Clips/Clip.cs +++ b/VRCOSC.Game/ChatBox/Clips/Clip.cs @@ -181,7 +181,7 @@ private void removeAbsentModules(List localStates) { foreach (var clipState in localStates.ToImmutableList()) { - var stateValid = clipState.ModuleNames.All(moduleName => appManager.ModuleManager.GetModule(moduleName) is not null); + var stateValid = clipState.ModuleNames.All(moduleName => appManager.ModuleManager.IsModuleLoaded(moduleName)); if (!stateValid) localStates.Remove(clipState); } } From 31332f8a2490339cd50aefb9900005fb3e28f7aa Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Fri, 22 Sep 2023 13:58:20 +0100 Subject: [PATCH 14/21] Create Maths module --- VRCOSC.Game/Modules/Module.cs | 2 +- VRCOSC.Modules/Maths/MathsEquationInstance.cs | 359 ++++++++++++++++++ VRCOSC.Modules/Maths/MathsModule.cs | 71 ++++ VRCOSC.Modules/VRCOSC.Modules.csproj | 1 + 4 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 VRCOSC.Modules/Maths/MathsEquationInstance.cs create mode 100644 VRCOSC.Modules/Maths/MathsModule.cs diff --git a/VRCOSC.Game/Modules/Module.cs b/VRCOSC.Game/Modules/Module.cs index 6f583396..f7cf9f66 100644 --- a/VRCOSC.Game/Modules/Module.cs +++ b/VRCOSC.Game/Modules/Module.cs @@ -409,7 +409,7 @@ protected T GetSetting(Enum lookup) /// /// Allows for sending a parameter that hasn't been registered. Only use this when absolutely necessary /// - protected void SendParameter(string parameterName, T value) where T : struct + protected void SendParameter(string parameterName, object value) { scheduler.Add(() => oscClient.SendValue($"{VRChatOscConstants.ADDRESS_AVATAR_PARAMETERS_PREFIX}/{parameterName}", value)); } diff --git a/VRCOSC.Modules/Maths/MathsEquationInstance.cs b/VRCOSC.Modules/Maths/MathsEquationInstance.cs new file mode 100644 index 00000000..2bfe671f --- /dev/null +++ b/VRCOSC.Modules/Maths/MathsEquationInstance.cs @@ -0,0 +1,359 @@ +// 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 osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osuTK; +using VRCOSC.Game.Graphics.ModuleAttributes.Attributes; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI; +using VRCOSC.Game.Graphics.UI.Text; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Modules.Maths; + +public class MathsEquationInstance : IEquatable +{ + [JsonProperty("input_parameter")] + public Bindable InputParameter = new(string.Empty); + + [JsonProperty("input_type")] + public Bindable InputType = new(); + + [JsonProperty("equation")] + public Bindable Equation = new(string.Empty); + + [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 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; + } + + [JsonConstructor] + public MathsEquationInstance() + { + } + + public MathsEquationInstance(MathsEquationInstance other) + { + 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; + } +} + +public class MathsEquationInstanceListAttribute : ModuleAttributeList +{ + public override Drawable GetAssociatedCard() => new MathsEquationInstanceAttributeCardList(this); + + protected override IEnumerable JArrayToType(JArray array) => array.Select(value => new MathsEquationInstance(value.ToObject()!)).ToList(); + protected override IEnumerable GetClonedDefaults() => Default.Select(defaultValue => new MathsEquationInstance(defaultValue)).ToList(); +} + +public partial class MathsEquationInstanceAttributeCardList : AttributeCardList +{ + public MathsEquationInstanceAttributeCardList(MathsEquationInstanceListAttribute 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(maxSize: 250), + 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) + }, + 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 = "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", + 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 = "Equation", + 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 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) + } + } + } + } + } + }, float.MinValue); + } + + protected override void OnInstanceAdd(MathsEquationInstance instance) + { + AddToList(new GridContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(maxSize: 250), + 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) + }, + 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.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() + }, + 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.Equation.GetBoundCopy(), + PlaceholderText = "Equation" + }, + 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.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() + } + } + } + }); + } + + protected override MathsEquationInstance CreateInstance() => new(); +} + +public partial class MathsValueTypeInstanceDropdown : VRCOSCDropdown +{ + protected override DropdownHeader CreateHeader() => new MathsValueTypeDropdownHeader(); + + public partial class MathsValueTypeDropdownHeader : DropdownHeader + { + protected override LocalisableString Label + { + get => Text.Text; + set => Text.Text = value; + } + + protected readonly SpriteText Text; + public readonly SpriteIcon Icon; + + public MathsValueTypeDropdownHeader() + { + Foreground.Padding = new MarginPadding(10); + + AutoSizeAxes = Axes.None; + CornerRadius = 5; + Masking = true; + BorderColour = ThemeManager.Current[ThemeAttribute.Border]; + BorderThickness = 2; + Height = 30; + + Foreground.Child = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + Text = new SpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Truncate = true, + Font = FrameworkFont.Regular.With(size: 20), + Colour = ThemeManager.Current[ThemeAttribute.Text] + }, + Icon = new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronDown, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(10), + Colour = ThemeManager.Current[ThemeAttribute.Text] + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load() + { + BackgroundColour = ThemeManager.Current[ThemeAttribute.Dark]; + BackgroundColourHover = ThemeManager.Current[ThemeAttribute.Mid]; + } + } +} + +public enum MathsEquationValueType +{ + Bool, + Int, + Float +} diff --git a/VRCOSC.Modules/Maths/MathsModule.cs b/VRCOSC.Modules/Maths/MathsModule.cs new file mode 100644 index 00000000..bb4b27e4 --- /dev/null +++ b/VRCOSC.Modules/Maths/MathsModule.cs @@ -0,0 +1,71 @@ +// 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; +using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Avatar; + +namespace VRCOSC.Modules.Maths; + +[ModuleTitle("Maths")] +[ModuleDescription("Allows you to write equations to execute on a parameter, and set the output's value into another parameter")] +[ModuleAuthor("VolcanicArts")] +public class MathsModule : AvatarModule +{ + private readonly Dictionary instances = new(); + + public MathsModule() + { + License.iConfirmNonCommercialUse("VolcanicArts"); + } + + protected override void CreateAttributes() + { + 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'", + Default = new List() + }); + } + + protected override void OnModuleStart() + { + instances.Clear(); + + GetSettingList(MathsSetting.Equations).ForEach(instance => { instances.Add(instance.InputParameter.Value, instance); }); + } + + protected override void OnAnyParameterReceived(ReceivedParameter parameter) + { + if (!instances.TryGetValue(parameter.Name, out var instance)) return; + + var parameterArgument = createArgumentForParameterValue(parameter, instance.InputType.Value); + var expression = new Expression(instance.Equation.Value, parameterArgument); + + var output = expression.calculate(); + + SendParameter(instance.OutputParameter.Value, convertToOutputType(output, instance.OutputType.Value)); + } + + private static Argument createArgumentForParameterValue(ReceivedParameter parameter, MathsEquationValueType valueType) => valueType switch + { + 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) + }; + + private static object convertToOutputType(double value, MathsEquationValueType valueType) => valueType switch + { + MathsEquationValueType.Bool => Convert.ToBoolean(value), + MathsEquationValueType.Int => Convert.ToInt32(value), + MathsEquationValueType.Float => Convert.ToSingle(value), + _ => throw new ArgumentOutOfRangeException(nameof(valueType), valueType, null) + }; + + private enum MathsSetting + { + Equations + } +} diff --git a/VRCOSC.Modules/VRCOSC.Modules.csproj b/VRCOSC.Modules/VRCOSC.Modules.csproj index 64789acd..5890d79f 100644 --- a/VRCOSC.Modules/VRCOSC.Modules.csproj +++ b/VRCOSC.Modules/VRCOSC.Modules.csproj @@ -14,6 +14,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive From 18ad8af68221107ed0dccf536ce0045b5e6f7164 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Fri, 22 Sep 2023 14:21:44 +0100 Subject: [PATCH 15/21] Add missing license header --- VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs b/VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs index e23e8fd4..379c5233 100644 --- a/VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs +++ b/VRCOSC.Modules/SpeechToText/SpeechToTextModelInstance.cs @@ -1,4 +1,7 @@ -using System.Collections.Specialized; +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Collections.Specialized; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Framework.Allocation; From 062cef19bc6bb87f9679733c42d049005d898bea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Sep 2023 20:09:54 +0000 Subject: [PATCH 16/21] Bump Octokit from 7.1.0 to 7.2.0 Bumps [Octokit](https://github.com/octokit/octokit.net) from 7.1.0 to 7.2.0. - [Release notes](https://github.com/octokit/octokit.net/releases) - [Changelog](https://github.com/octokit/octokit.net/blob/main/docs/releases.md) - [Commits](https://github.com/octokit/octokit.net/compare/v7.1.0...v7.2.0) --- updated-dependencies: - dependency-name: Octokit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- 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 515c8a14..0f206741 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -19,7 +19,7 @@ - + From cbcbca86d31a0f098d03d9b4d49f3bde0fc2fe59 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sat, 23 Sep 2023 15:31:15 +0100 Subject: [PATCH 17/21] Add button to open built-in list for Maths module --- VRCOSC.Modules/Maths/MathsEquationInstance.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/VRCOSC.Modules/Maths/MathsEquationInstance.cs b/VRCOSC.Modules/Maths/MathsEquationInstance.cs index 2bfe671f..b64bd22f 100644 --- a/VRCOSC.Modules/Maths/MathsEquationInstance.cs +++ b/VRCOSC.Modules/Maths/MathsEquationInstance.cs @@ -10,10 +10,12 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; +using osu.Framework.Platform; using osuTK; using VRCOSC.Game.Graphics.ModuleAttributes.Attributes; using VRCOSC.Game.Graphics.Themes; using VRCOSC.Game.Graphics.UI; +using VRCOSC.Game.Graphics.UI.Button; using VRCOSC.Game.Graphics.UI.Text; using VRCOSC.Game.Modules.Attributes; @@ -68,6 +70,9 @@ public class MathsEquationInstanceListAttribute : ModuleAttributeList { + [Resolved] + private GameHost host { get; set; } = null!; + public MathsEquationInstanceAttributeCardList(MathsEquationInstanceListAttribute attributeData) : base(attributeData) { @@ -76,6 +81,26 @@ public MathsEquationInstanceAttributeCardList(MathsEquationInstanceListAttribute [BackgroundDependencyLoader] private void load() { + Add(new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Width = 0.75f, + Child = new TextButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Text = "See built-in functions and variables", + CornerRadius = 5, + FontSize = 22, + Action = () => host.OpenUrlExternally("https://mathparser.org/mxparser-math-collection/"), + BackgroundColour = ThemeManager.Current[ThemeAttribute.Action] + } + }); + AddToContent(new Container { Anchor = Anchor.TopCentre, From 48a48f3e10cfe0212e4cb0cc8e9b05505f6d0da9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 20:19:19 +0000 Subject: [PATCH 18/21] Bump Octokit from 7.1.0 to 8.0.0 Bumps [Octokit](https://github.com/octokit/octokit.net) from 7.1.0 to 8.0.0. - [Release notes](https://github.com/octokit/octokit.net/releases) - [Changelog](https://github.com/octokit/octokit.net/blob/main/docs/releases.md) - [Commits](https://github.com/octokit/octokit.net/compare/v7.1.0...v8.0.0) --- updated-dependencies: - dependency-name: Octokit dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- 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 515c8a14..838b9f68 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -19,7 +19,7 @@ - + From 62895ce10a7b9a206fac64553799f9e2e2673e08 Mon Sep 17 00:00:00 2001 From: BlackOfWorld Date: Wed, 27 Sep 2023 09:51:38 +0200 Subject: [PATCH 19/21] Start router programs with working directory --- VRCOSC.Game/Managers/StartupManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/VRCOSC.Game/Managers/StartupManager.cs b/VRCOSC.Game/Managers/StartupManager.cs index 7569e04b..254c2190 100644 --- a/VRCOSC.Game/Managers/StartupManager.cs +++ b/VRCOSC.Game/Managers/StartupManager.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 System; @@ -71,7 +71,10 @@ public void Start() if (Process.GetProcessesByName(processName).Any()) return; - Process.Start(new ProcessStartInfo(instance.FilePath.Value, instance.LaunchArguments.Value)); + Process.Start(new ProcessStartInfo(instance.FilePath.Value, instance.LaunchArguments.Value) + { + WorkingDirectory = Path.GetDirectoryName(instance.FilePath.Value) + }); logger.Log($"Running file {instance.FilePath.Value}" + (string.IsNullOrEmpty(instance.LaunchArguments.Value) ? string.Empty : $" with launch arguments {instance.LaunchArguments.Value}")); } catch (Exception e) From 964f7449226042b1cfe1284210873919b220cb4c Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Thu, 28 Sep 2023 16:56:51 +0100 Subject: [PATCH 20/21] Ensure heartrate provider termination is awaited --- VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs b/VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs index 162fe490..fe645fc7 100644 --- a/VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs +++ b/VRCOSC.Game/Modules/Bases/Heartrate/HeartrateModule.cs @@ -59,7 +59,7 @@ protected override void OnModuleStart() ChangeStateTo(HeartrateState.Default); } - private void attemptReconnection() + private async void attemptReconnection() { if (connectionCount >= reconnection_limit) { @@ -70,7 +70,10 @@ private void attemptReconnection() Log("Attempting reconnection..."); Thread.Sleep(reconnection_delay); - HeartrateProvider?.Teardown(); + if (HeartrateProvider is null) return; + + await HeartrateProvider.Teardown(); + HeartrateProvider?.Initialise(); connectionCount++; } From 04c9c67a2015b68d4fd38d45e271bc6bff5ad49f Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Thu, 28 Sep 2023 17:07:24 +0100 Subject: [PATCH 21/21] 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 84a87d10..26ffbff1 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.826.0 + 2023.928.0 VRCOSC VolcanicArts VolcanicArts enable - 2023.826.0 + 2023.928.0 diff --git a/VRCOSC.Game/VRCOSC.Game.csproj b/VRCOSC.Game/VRCOSC.Game.csproj index 274e49e6..a90b6bde 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -4,7 +4,7 @@ enable 11 VolcanicArts.VRCOSC.SDK - 2023.826.0 + 2023.928.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 c33ed838..2dda5786 100644 --- a/VRCOSC.Templates/VRCOSC.Templates.csproj +++ b/VRCOSC.Templates/VRCOSC.Templates.csproj @@ -11,7 +11,7 @@ true NU5128 - 2023.826.0 + 2023.928.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 7d28a8f8..ce1948ab 100644 --- a/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj +++ b/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj @@ -7,7 +7,7 @@ - +