From 4eff36628a36015b0ac4629217c97c34d6c46bd5 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 24 Dec 2024 00:49:24 +0000 Subject: [PATCH 01/74] Some more module setting refactoring --- .../Modules/Serialisation/ModuleSerialiser.cs | 4 +- .../Serialisation/SerialisableModule.cs | 2 +- .../Attributes/Settings/ListModuleSetting.cs | 21 +- .../Attributes/Settings/ModuleSetting.cs | 12 +- .../QueryableParameterModuleSetting.cs | 46 ++--- .../Attributes/Settings/ValueModuleSetting.cs | 183 +++++------------- VRCOSC.App/SDK/Modules/Module.cs | 20 +- .../QueryableParameterConverters.cs | 9 +- 8 files changed, 100 insertions(+), 197 deletions(-) diff --git a/VRCOSC.App/Modules/Serialisation/ModuleSerialiser.cs b/VRCOSC.App/Modules/Serialisation/ModuleSerialiser.cs index 3ee16a91..bcdcb8be 100644 --- a/VRCOSC.App/Modules/Serialisation/ModuleSerialiser.cs +++ b/VRCOSC.App/Modules/Serialisation/ModuleSerialiser.cs @@ -34,7 +34,7 @@ protected override bool ExecuteAfterDeserialisation(SerialisableModule data) { var setting = Reference.GetSetting(settingKey); - if (!setting.Deserialise(settingValue)) + if (!setting.InternalDeserialise(settingValue)) shouldReserialise = true; } catch (Exception) @@ -62,4 +62,4 @@ protected override bool ExecuteAfterDeserialisation(SerialisableModule data) return shouldReserialise; } -} +} \ No newline at end of file diff --git a/VRCOSC.App/Modules/Serialisation/SerialisableModule.cs b/VRCOSC.App/Modules/Serialisation/SerialisableModule.cs index 3720da75..f1209820 100644 --- a/VRCOSC.App/Modules/Serialisation/SerialisableModule.cs +++ b/VRCOSC.App/Modules/Serialisation/SerialisableModule.cs @@ -31,7 +31,7 @@ public SerialisableModule(Module module) Version = 1; Enabled = module.Enabled.Value; - module.Settings.Where(pair => !pair.Value.IsDefault()).ForEach(pair => Settings.Add(pair.Key, pair.Value.Serialise())); + module.Settings.Where(pair => !pair.Value.InternalIsDefault()).ForEach(pair => Settings.Add(pair.Key, pair.Value.InternalSerialise())); module.Parameters.Where(pair => !pair.Value.IsDefault()).ForEach(pair => Parameters.Add(pair.Key.ToLookup(), new SerialisableParameter(pair.Value))); } } diff --git a/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs b/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs index f2262a28..e3f629a2 100644 --- a/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs +++ b/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs @@ -34,7 +34,7 @@ protected ListModuleSetting(string title, string description, Type viewType, IEn Attribute.OnCollectionChanged((_, _) => OnSettingChange?.Invoke()); } - internal override bool IsDefault() => Attribute.SequenceEqual(DefaultValues); + protected override bool IsDefault() => Attribute.SequenceEqual(DefaultValues); public override void Add() => Attribute.Add(CreateItem()); @@ -47,21 +47,16 @@ public override void Remove(object item) protected abstract T CreateItem(); - public override bool GetValue(out TOut returnValue) + public override TOut GetValue() { - if (typeof(List).IsAssignableTo(typeof(TOut))) - { - returnValue = (TOut)Convert.ChangeType(Attribute.ToList(), typeof(TOut)); - return true; - } - - returnValue = (TOut)Convert.ChangeType(Array.Empty(), typeof(TOut)); - return false; + if (!typeof(List).IsAssignableTo(typeof(TOut))) throw new InvalidCastException($"{typeof(List).Name} cannot be cast to {typeof(TOut).Name}"); + + return (TOut)Convert.ChangeType(Attribute.ToList(), typeof(TOut)); } - internal override object Serialise() => Attribute.ToList(); + protected override object Serialise() => Attribute.ToList(); - internal override bool Deserialise(object? ingestValue) + protected override bool Deserialise(object? ingestValue) { if (ingestValue is not JArray jArrayValue) return false; @@ -78,7 +73,7 @@ protected ValueListModuleSetting(string title, string description, Type viewType { } - internal override bool IsDefault() => Attribute.Count == DefaultValues.Count() && Attribute.All(o => o.IsDefault); + protected override bool IsDefault() => Attribute.Count == DefaultValues.Count() && Attribute.All(o => o.IsDefault); protected override Observable CreateItem() => new(); } diff --git a/VRCOSC.App/SDK/Modules/Attributes/Settings/ModuleSetting.cs b/VRCOSC.App/SDK/Modules/Attributes/Settings/ModuleSetting.cs index db8cabe0..0fba4f89 100644 --- a/VRCOSC.App/SDK/Modules/Attributes/Settings/ModuleSetting.cs +++ b/VRCOSC.App/SDK/Modules/Attributes/Settings/ModuleSetting.cs @@ -49,10 +49,14 @@ public UserControl? ViewInstance public Observable IsEnabled { get; } = new(true); - internal abstract bool Deserialise(object? ingestValue); - internal abstract object? Serialise(); - internal abstract bool IsDefault(); - public abstract bool GetValue(out T returnValue); + internal bool InternalDeserialise(object? ingestValue) => Deserialise(ingestValue); + internal object? InternalSerialise() => Serialise(); + internal bool InternalIsDefault() => IsDefault(); + + protected abstract bool Deserialise(object? ingestValue); + protected abstract object? Serialise(); + protected abstract bool IsDefault(); + public abstract T GetValue(); protected ModuleSetting(string title, string description, Type viewType) { diff --git a/VRCOSC.App/SDK/Modules/Attributes/Settings/QueryableParameterModuleSetting.cs b/VRCOSC.App/SDK/Modules/Attributes/Settings/QueryableParameterModuleSetting.cs index f21b4699..56b00070 100644 --- a/VRCOSC.App/SDK/Modules/Attributes/Settings/QueryableParameterModuleSetting.cs +++ b/VRCOSC.App/SDK/Modules/Attributes/Settings/QueryableParameterModuleSetting.cs @@ -13,19 +13,22 @@ public class QueryableParameterListModuleSetting : ModuleSetting { public QueryableParameterList QueryableParameterList { get; } - public QueryableParameterListModuleSetting(string title, string description) + public Type? ActionType { get; } + + public QueryableParameterListModuleSetting(string title, string description, Type? actionType) : base(title, description, typeof(QueryableParameterListModuleSettingView)) { - QueryableParameterList = CreateParameterList(); + QueryableParameterList = createParameterList(); + ActionType = actionType; } - internal override bool Deserialise(object? ingestValue) + protected override bool Deserialise(object? ingestValue) { if (ingestValue is not JArray jArrayValue) return false; QueryableParameterList.Parameters.Clear(); - foreach (var ingestItem in jArrayValue.Select(CreateQueryableParameter)) + foreach (var ingestItem in jArrayValue.Select(createQueryableParameter)) { QueryableParameterList.Parameters.Add(ingestItem); } @@ -33,38 +36,17 @@ internal override bool Deserialise(object? ingestValue) return true; } - protected virtual object CreateQueryableParameter(JToken token) => token.ToObject()!; + private object createQueryableParameter(JToken token) => ActionType is null ? token.ToObject()! : token.ToObject(typeof(ActionableQueryableParameter<>).MakeGenericType(ActionType))!; + private QueryableParameterList createParameterList() => ActionType is null ? new QueryableParameterList(typeof(QueryableParameter)) : new QueryableParameterList(typeof(ActionableQueryableParameter<>).MakeGenericType(ActionType)); - internal override object Serialise() => QueryableParameterList.Parameters; + protected override object Serialise() => QueryableParameterList.Parameters; - internal override bool IsDefault() => QueryableParameterList.Parameters.Count == 0; + protected override bool IsDefault() => QueryableParameterList.Parameters.Count == 0; - public override bool GetValue(out T returnValue) + public override TOut GetValue() { - if (typeof(T) != typeof(QueryableParameterList)) - { - returnValue = default(T); - return false; - } - - returnValue = (T)Convert.ChangeType(QueryableParameterList, typeof(T)); - return true; - } + if (typeof(TOut) != typeof(QueryableParameterList)) throw new InvalidCastException($"Requested type must only be {nameof(QueryableParameterList)}"); - protected virtual QueryableParameterList CreateParameterList() => new(typeof(QueryableParameter)); -} - -public sealed class ActionableQueryableParameterListModuleSetting : QueryableParameterListModuleSetting -{ - public Type ActionType { get; } - - public ActionableQueryableParameterListModuleSetting(string title, string description, Type actionType) - : base(title, description) - { - ActionType = actionType; + return (TOut)Convert.ChangeType(QueryableParameterList, typeof(TOut)); } - - protected override object CreateQueryableParameter(JToken token) => token.ToObject(typeof(ActionableQueryableParameter<>).MakeGenericType(ActionType))!; - - protected override QueryableParameterList CreateParameterList() => new(typeof(ActionableQueryableParameter<>).MakeGenericType(ActionType)); } \ No newline at end of file diff --git a/VRCOSC.App/SDK/Modules/Attributes/Settings/ValueModuleSetting.cs b/VRCOSC.App/SDK/Modules/Attributes/Settings/ValueModuleSetting.cs index c124a134..0c92adda 100644 --- a/VRCOSC.App/SDK/Modules/Attributes/Settings/ValueModuleSetting.cs +++ b/VRCOSC.App/SDK/Modules/Attributes/Settings/ValueModuleSetting.cs @@ -22,64 +22,39 @@ protected ValueModuleSetting(string title, string description, Type viewType, T Attribute.Subscribe(_ => OnSettingChange?.Invoke()); } - internal override object Serialise() => Attribute.Value; - - internal override bool IsDefault() => Attribute.IsDefault; -} - -public class BoolModuleSetting : ValueModuleSetting -{ - public BoolModuleSetting(string title, string description, Type viewType, bool defaultValue) - : base(title, description, viewType, defaultValue) - { - } - - internal override bool Deserialise(object? ingestValue) + protected override bool Deserialise(object? ingestValue) { - if (ingestValue is not bool boolValue) return false; + if (ingestValue is not T tValue) return false; - Attribute.Value = boolValue; + Attribute.Value = tValue; return true; } - public override bool GetValue(out T returnValue) + protected override object Serialise() => Attribute.Value; + + protected override bool IsDefault() => Attribute.IsDefault; + + public override TOut GetValue() { - if (typeof(T) == typeof(bool)) - { - returnValue = (T)Convert.ChangeType(Attribute.Value, typeof(T)); - return true; - } + if (!typeof(T).IsAssignableTo(typeof(TOut))) throw new InvalidCastException($"Unable to cast {typeof(T).Name} to {typeof(TOut).Name}"); - returnValue = (T)Convert.ChangeType(false, typeof(T)); - return false; + return (TOut)Convert.ChangeType(Attribute.Value, typeof(TOut)); } } -public class StringModuleSetting : ValueModuleSetting +public class BoolModuleSetting : ValueModuleSetting { - public StringModuleSetting(string title, string description, Type viewType, string defaultValue) + public BoolModuleSetting(string title, string description, Type viewType, bool defaultValue) : base(title, description, viewType, defaultValue) { } +} - internal override bool Deserialise(object? ingestValue) - { - if (ingestValue is not string stringValue) return false; - - Attribute.Value = stringValue; - return true; - } - - public override bool GetValue(out T returnValue) +public class StringModuleSetting : ValueModuleSetting +{ + public StringModuleSetting(string title, string description, Type viewType, string defaultValue) + : base(title, description, viewType, defaultValue) { - if (typeof(T) == typeof(string)) - { - returnValue = (T)Convert.ChangeType(Attribute.Value, typeof(T)); - return true; - } - - returnValue = (T)Convert.ChangeType(string.Empty, typeof(T)); - return false; } } @@ -90,26 +65,8 @@ public IntModuleSetting(string title, string description, Type viewType, int def { } - internal override bool Deserialise(object? ingestValue) - { - // json stores as long - if (ingestValue is not long longValue) return false; - - Attribute.Value = (int)longValue; - return true; - } - - public override bool GetValue(out T returnValue) - { - if (typeof(T) == typeof(int)) - { - returnValue = (T)Convert.ChangeType(Attribute.Value, typeof(T)); - return true; - } - - returnValue = (T)Convert.ChangeType(0, typeof(T)); - return false; - } + // json stores as long + protected override bool Deserialise(object? ingestValue) => ingestValue is long longValue && base.Deserialise((int)longValue); } public class FloatModuleSetting : ValueModuleSetting @@ -119,26 +76,8 @@ public FloatModuleSetting(string title, string description, Type viewType, float { } - internal override bool Deserialise(object? ingestValue) - { - // json stores as double - if (ingestValue is not double doubleValue) return false; - - Attribute.Value = (float)doubleValue; - return true; - } - - public override bool GetValue(out T returnValue) - { - if (typeof(T) == typeof(float)) - { - returnValue = (T)Convert.ChangeType(Attribute.Value, typeof(T)); - return true; - } - - returnValue = (T)Convert.ChangeType(0f, typeof(T)); - return false; - } + // json stores as double + protected override bool Deserialise(object? ingestValue) => ingestValue is double doubleValue && base.Deserialise((float)doubleValue); } public class EnumModuleSetting : IntModuleSetting @@ -151,22 +90,17 @@ public EnumModuleSetting(string title, string description, Type viewType, int de EnumType = enumType; } - public override bool GetValue(out T returnValue) + public override TOut GetValue() { - if (typeof(T) == EnumType) - { - returnValue = (T)Enum.ToObject(EnumType, Attribute.Value); - return true; - } + if (typeof(TOut) != EnumType) throw new InvalidCastException($"{typeof(TOut).Name} is not the stored enum type {EnumType.Name}"); - returnValue = (T)Enum.ToObject(typeof(T), 0); - return false; + return (TOut)Enum.ToObject(EnumType, Attribute.Value); } } public class DropdownListModuleSetting : StringModuleSetting { - private readonly Type itemType; + internal readonly Type ItemType; public string TitlePath { get; } public string ValuePath { get; } @@ -176,7 +110,7 @@ public class DropdownListModuleSetting : StringModuleSetting public DropdownListModuleSetting(string title, string description, Type viewType, IEnumerable items, string defaultValue, string titlePath, string valuePath) : base(title, description, viewType, defaultValue) { - itemType = items.GetType().GenericTypeArguments[0]; + ItemType = items.GetType().GenericTypeArguments[0]; TitlePath = titlePath; ValuePath = valuePath; @@ -185,21 +119,17 @@ public DropdownListModuleSetting(string title, string description, Type viewType Items = items.ToList().AsReadOnly(); } - public override bool GetValue(out T returnValue) + // this specifically returns the selected value + public override TOut GetValue() { - if (typeof(T) == itemType) - { - var valueProperty = itemType.GetProperty(ValuePath); - returnValue = (T)Items.First(item => valueProperty!.GetValue(item)!.ToString()! == Attribute.Value); - return true; - } + if (typeof(TOut) != ItemType) throw new InvalidCastException($"{typeof(TOut).Name} is not the stored item type {ItemType.Name}"); - returnValue = (T)new object(); - return false; + var valueProperty = ItemType.GetProperty(ValuePath); + return (TOut)Items.First(item => valueProperty!.GetValue(item)!.ToString()! == Attribute.Value); } } -public class SliderModuleSetting : ValueModuleSetting +public class SliderModuleSetting : FloatModuleSetting { public Type ValueType { get; } public float MinValue { get; } @@ -226,33 +156,38 @@ public SliderModuleSetting(string title, string description, Type viewType, int TickFrequency = tickFrequency; } - internal override bool Deserialise(object? ingestValue) + protected override bool Deserialise(object? ingestValue) { - // json stores as double - if (ingestValue is not double doubleValue) return false; + if (!base.Deserialise(ingestValue)) return false; - var floatValue = (float)doubleValue; - Attribute.Value = Math.Clamp(floatValue, MinValue, MaxValue); + Attribute.Value = Math.Clamp(Attribute.Value, MinValue, MaxValue); return true; } - public override bool GetValue(out T returnValue) + public override TOut GetValue() { - if (typeof(T) == typeof(float) && ValueType == typeof(float)) + if (ValueType == typeof(float)) { - returnValue = (T)Convert.ChangeType(Attribute.Value, typeof(T)); - return true; + if (typeof(float).IsAssignableTo(typeof(TOut))) + { + return (TOut)Convert.ChangeType(Attribute.Value, typeof(TOut)); + } + + throw new InvalidCastException($"{typeof(TOut).Name} cannot be cast to a float"); } - if (typeof(T) == typeof(int) && ValueType == typeof(int)) + if (ValueType == typeof(int)) { - returnValue = (T)Convert.ChangeType(Attribute.Value, typeof(T)); - return true; + if (typeof(int).IsAssignableTo(typeof(TOut))) + { + return (TOut)Convert.ChangeType((int)Attribute.Value, typeof(TOut)); + } + + throw new InvalidCastException($"{typeof(TOut).Name} cannot be cast to an int"); } - returnValue = (T)Convert.ChangeType(0f, typeof(T)); - return false; + throw new InvalidOperationException($"{ValueType.Name} is neither an int nor a float"); } } @@ -266,9 +201,9 @@ public DateTimeModuleSetting(string title, string description, Type viewType, Da { } - internal override object Serialise() => Attribute.Value.UtcTicks; + protected override object Serialise() => Attribute.Value.UtcTicks; - internal override bool Deserialise(object? ingestValue) + protected override bool Deserialise(object? ingestValue) { if (ingestValue is not long ingestUtcTicks) return false; @@ -283,16 +218,4 @@ internal override bool Deserialise(object? ingestValue) return true; } - - public override bool GetValue(out T returnValue) - { - if (typeof(T) == typeof(DateTimeOffset)) - { - returnValue = (T)Convert.ChangeType(Attribute.Value, typeof(T)); - return true; - } - - returnValue = (T)Convert.ChangeType(DateTimeOffset.FromUnixTimeSeconds(0), typeof(T)); - return false; - } } \ No newline at end of file diff --git a/VRCOSC.App/SDK/Modules/Module.cs b/VRCOSC.App/SDK/Modules/Module.cs index 6a13be49..4759c965 100644 --- a/VRCOSC.App/SDK/Modules/Module.cs +++ b/VRCOSC.App/SDK/Modules/Module.cs @@ -497,12 +497,12 @@ protected void CreateKeyValuePairList(Enum lookup, string title, string descript protected void CreateQueryableParameterList(Enum lookup, string title, string description) { - addSetting(lookup, new QueryableParameterListModuleSetting(title, description)); + addSetting(lookup, new QueryableParameterListModuleSetting(title, description, null)); } protected void CreateQueryableParameterList(Enum lookup, string title, string description) where TAction : Enum { - addSetting(lookup, new ActionableQueryableParameterListModuleSetting(title, description, typeof(TAction))); + addSetting(lookup, new QueryableParameterListModuleSetting(title, description, typeof(TAction))); } private void addSetting(Enum lookup, ModuleSetting moduleSetting) @@ -883,7 +883,7 @@ internal ModuleParameter GetParameter(string lookup) } /// - /// Retrieves a 's value as a shorthand for + /// Retrieves a 's value as a shorthand for /// /// The lookup of the setting /// The value type of the setting @@ -892,11 +892,15 @@ protected T GetSettingValue(Enum lookup) { lock (loadLock) { - var moduleSetting = GetSetting(lookup); - - if (moduleSetting.GetValue(out var value)) return value; - - throw new InvalidOperationException($"Could not get the value of setting with lookup '{lookup.ToLookup()}'"); + try + { + return GetSetting(lookup).GetValue(); + } + catch (Exception e) + { + ExceptionHandler.Handle(e, $"'{FullID}' experienced a problem when getting value of setting '{lookup.ToLookup()}'"); + return default!; + } } } diff --git a/VRCOSC.App/UI/Views/Modules/Settings/QueryableParameter/QueryableParameterConverters.cs b/VRCOSC.App/UI/Views/Modules/Settings/QueryableParameter/QueryableParameterConverters.cs index 820a59a7..fe8cd784 100644 --- a/VRCOSC.App/UI/Views/Modules/Settings/QueryableParameter/QueryableParameterConverters.cs +++ b/VRCOSC.App/UI/Views/Modules/Settings/QueryableParameter/QueryableParameterConverters.cs @@ -75,14 +75,9 @@ public class QueryableParameterHasActionVisibilityConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (value is QueryableParameterListModuleSetting _) + if (value is QueryableParameterListModuleSetting moduleSetting) { - return Visibility.Collapsed; - } - - if (value is ActionableQueryableParameterListModuleSetting _) - { - return Visibility.Visible; + return moduleSetting.ActionType is null ? Visibility.Collapsed : Visibility.Visible; } return null; From 8c31adc089e23b7a8dd4adf4d34ebfc66db1b85f Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 24 Dec 2024 11:05:09 +0000 Subject: [PATCH 02/74] Fix audio processor crash when disposing during processing --- VRCOSC.App/Audio/Whisper/AudioProcessor.cs | 10 +++++++--- VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/VRCOSC.App/Audio/Whisper/AudioProcessor.cs b/VRCOSC.App/Audio/Whisper/AudioProcessor.cs index 26ab94d4..2542eb28 100644 --- a/VRCOSC.App/Audio/Whisper/AudioProcessor.cs +++ b/VRCOSC.App/Audio/Whisper/AudioProcessor.cs @@ -82,11 +82,15 @@ public void Start() audioCapture.StartCapture(); } - public void Stop() + public async Task Stop() { audioCapture?.StopCapture(); - whisper?.Dispose(); - whisper = null; + + if (whisper is not null) + { + await whisper.DisposeAsync(); + whisper = null; + } } public async Task GetResultAsync() diff --git a/VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs b/VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs index 1a58ecfa..f369a165 100644 --- a/VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs +++ b/VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs @@ -48,7 +48,8 @@ public override void Initialise() private async void onCaptureDeviceIdChanged(string newDeviceId) { - audioProcessor?.Stop(); + if (audioProcessor is not null) + await audioProcessor.Stop(); if (repeater is not null) await repeater.StopAsync(); @@ -118,7 +119,8 @@ public override async Task Teardown() audioNotificationClient = null; - audioProcessor?.Stop(); + if (audioProcessor is not null) + await audioProcessor.Stop(); if (repeater is not null) await repeater.StopAsync(); From f37718fc5841ec228d864fbc11d22ffa60fd4bc4 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 24 Dec 2024 11:22:43 +0000 Subject: [PATCH 03/74] Fix casting problems caused by IsAssignableTo --- .../Modules/Attributes/Settings/ListModuleSetting.cs | 4 ++-- .../Attributes/Settings/ValueModuleSetting.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs b/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs index e3f629a2..c473f2e6 100644 --- a/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs +++ b/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs @@ -49,9 +49,9 @@ public override void Remove(object item) public override TOut GetValue() { - if (!typeof(List).IsAssignableTo(typeof(TOut))) throw new InvalidCastException($"{typeof(List).Name} cannot be cast to {typeof(TOut).Name}"); + if (Attribute.ToList() is not TOut castList) throw new InvalidCastException($"{typeof(List).Name} cannot be cast to {typeof(TOut).Name}"); - return (TOut)Convert.ChangeType(Attribute.ToList(), typeof(TOut)); + return castList; } protected override object Serialise() => Attribute.ToList(); diff --git a/VRCOSC.App/SDK/Modules/Attributes/Settings/ValueModuleSetting.cs b/VRCOSC.App/SDK/Modules/Attributes/Settings/ValueModuleSetting.cs index 0c92adda..3fbeded7 100644 --- a/VRCOSC.App/SDK/Modules/Attributes/Settings/ValueModuleSetting.cs +++ b/VRCOSC.App/SDK/Modules/Attributes/Settings/ValueModuleSetting.cs @@ -36,9 +36,9 @@ protected override bool Deserialise(object? ingestValue) public override TOut GetValue() { - if (!typeof(T).IsAssignableTo(typeof(TOut))) throw new InvalidCastException($"Unable to cast {typeof(T).Name} to {typeof(TOut).Name}"); + if (Attribute.Value is not TOut castValue) throw new InvalidCastException($"Unable to cast {typeof(T).Name} to {typeof(TOut).Name}"); - return (TOut)Convert.ChangeType(Attribute.Value, typeof(TOut)); + return castValue; } } @@ -169,9 +169,9 @@ public override TOut GetValue() { if (ValueType == typeof(float)) { - if (typeof(float).IsAssignableTo(typeof(TOut))) + if (Attribute.Value is TOut castValue) { - return (TOut)Convert.ChangeType(Attribute.Value, typeof(TOut)); + return castValue; } throw new InvalidCastException($"{typeof(TOut).Name} cannot be cast to a float"); @@ -179,9 +179,9 @@ public override TOut GetValue() if (ValueType == typeof(int)) { - if (typeof(int).IsAssignableTo(typeof(TOut))) + if ((int)Attribute.Value is TOut castValue) { - return (TOut)Convert.ChangeType((int)Attribute.Value, typeof(TOut)); + return castValue; } throw new InvalidCastException($"{typeof(TOut).Name} cannot be cast to an int"); From 7e203363d3d78bbe2955024ae6886fed41ec39c3 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Fri, 27 Dec 2024 23:19:55 +0000 Subject: [PATCH 04/74] Fix Windows scaling positioning the windows incorrectly --- VRCOSC.App/Utils/WPFUtils.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/VRCOSC.App/Utils/WPFUtils.cs b/VRCOSC.App/Utils/WPFUtils.cs index 7cf2492e..3f033e1d 100644 --- a/VRCOSC.App/Utils/WPFUtils.cs +++ b/VRCOSC.App/Utils/WPFUtils.cs @@ -2,6 +2,7 @@ // See the LICENSE file in the repository root for full license text. using System; +using System.Drawing; using System.Windows; using System.Windows.Forms; using System.Windows.Interop; @@ -72,22 +73,25 @@ public static void SetPosition(this Window window, Window? parentWindow, ScreenC targetScreen = Screen.PrimaryScreen!; } + using var g = Graphics.FromHwnd(IntPtr.Zero); + var dpiScale = g.DpiX / 96f; + var workingArea = targetScreen.WorkingArea; var x = horizontal switch { - HorizontalPosition.Left => workingArea.Left, - HorizontalPosition.Center => workingArea.Left + (workingArea.Width - window.Width) / 2, - HorizontalPosition.Right => workingArea.Right - window.Width, - _ => throw new ArgumentOutOfRangeException() + HorizontalPosition.Left => workingArea.Left / dpiScale, + HorizontalPosition.Center => (workingArea.Left + (workingArea.Width - window.Width * dpiScale) / 2) / dpiScale, + HorizontalPosition.Right => (workingArea.Right - window.Width * dpiScale) / dpiScale, + _ => throw new ArgumentOutOfRangeException(nameof(horizontal), "Invalid horizontal position") }; var y = vertical switch { - VerticalPosition.Top => workingArea.Top, - VerticalPosition.Center => workingArea.Top + (workingArea.Height - window.Height) / 2, - VerticalPosition.Bottom => workingArea.Bottom - window.Height, - _ => throw new ArgumentOutOfRangeException() + VerticalPosition.Top => workingArea.Top / dpiScale, + VerticalPosition.Center => (workingArea.Top + (workingArea.Height - window.Height * dpiScale) / 2) / dpiScale, + VerticalPosition.Bottom => (workingArea.Bottom - window.Height * dpiScale) / dpiScale, + _ => throw new ArgumentOutOfRangeException(nameof(vertical), "Invalid vertical position") }; window.Left = x; From 7a365f51076371ca68af5f978b757a7151dfc6db Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sat, 28 Dec 2024 13:15:24 +0000 Subject: [PATCH 05/74] Small changes --- .../Attributes/Settings/ListModuleSetting.cs | 14 +++++++------- VRCOSC.App/SDK/Modules/Module.cs | 2 +- .../Settings/ListItemDropdownSettingView.xaml | 3 +-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs b/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs index c473f2e6..0bdcaf70 100644 --- a/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs +++ b/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs @@ -66,14 +66,14 @@ protected override bool Deserialise(object? ingestValue) } } -public abstract class ValueListModuleSetting : ListModuleSetting> +public abstract class ValueListModuleSetting : ListModuleSetting> where T : notnull { - protected ValueListModuleSetting(string title, string description, Type viewType, IEnumerable> defaultValues) - : base(title, description, viewType, defaultValues) + protected ValueListModuleSetting(string title, string description, Type viewType, IEnumerable defaultValues) + : base(title, description, viewType, defaultValues.Select(value => new Observable(value))) { } - protected override bool IsDefault() => Attribute.Count == DefaultValues.Count() && Attribute.All(o => o.IsDefault); + protected override bool IsDefault() => base.IsDefault() && Attribute.All(o => o.IsDefault); protected override Observable CreateItem() => new(); } @@ -81,7 +81,7 @@ protected ValueListModuleSetting(string title, string description, Type viewType public class StringListModuleSetting : ValueListModuleSetting { public StringListModuleSetting(string title, string description, Type viewType, IEnumerable defaultValues) - : base(title, description, viewType, defaultValues.Select(value => new Observable(value))) + : base(title, description, viewType, defaultValues) { } @@ -91,7 +91,7 @@ public StringListModuleSetting(string title, string description, Type viewType, public class IntListModuleSetting : ValueListModuleSetting { public IntListModuleSetting(string title, string description, Type viewType, IEnumerable defaultValues) - : base(title, description, viewType, defaultValues.Select(value => new Observable(value))) + : base(title, description, viewType, defaultValues) { } } @@ -99,7 +99,7 @@ public IntListModuleSetting(string title, string description, Type viewType, IEn public class FloatListModuleSetting : ValueListModuleSetting { public FloatListModuleSetting(string title, string description, Type viewType, IEnumerable defaultValues) - : base(title, description, viewType, defaultValues.Select(value => new Observable(value))) + : base(title, description, viewType, defaultValues) { } } \ No newline at end of file diff --git a/VRCOSC.App/SDK/Modules/Module.cs b/VRCOSC.App/SDK/Modules/Module.cs index 4759c965..b34bb51d 100644 --- a/VRCOSC.App/SDK/Modules/Module.cs +++ b/VRCOSC.App/SDK/Modules/Module.cs @@ -464,7 +464,7 @@ protected void CreateDropdown(Enum lookup, string title, string description, IEn if (titleValue.ToString() is null || valueValue.ToString() is null) { - throw new InvalidOperationException("Your titlePath and valuePath properties must be convertable to a string"); + throw new InvalidOperationException("Your titlePath and valuePath properties must be convertible to a string"); } addSetting(lookup, new DropdownListModuleSetting(title, description, typeof(ListItemDropdownSettingView), items, valueValue.ToString()!, titlePath, valuePath)); diff --git a/VRCOSC.App/UI/Views/Modules/Settings/ListItemDropdownSettingView.xaml b/VRCOSC.App/UI/Views/Modules/Settings/ListItemDropdownSettingView.xaml index 2a836e5f..afe0a505 100644 --- a/VRCOSC.App/UI/Views/Modules/Settings/ListItemDropdownSettingView.xaml +++ b/VRCOSC.App/UI/Views/Modules/Settings/ListItemDropdownSettingView.xaml @@ -6,6 +6,5 @@ mc:Ignorable="d"> + Background="{StaticResource CBackground3}" FontSize="16" /> \ No newline at end of file From e7f7c3a71a687f4c4fda0d71a3c7096b315a30d4 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 29 Dec 2024 22:00:07 +0000 Subject: [PATCH 06/74] Bump dependencies --- VRCOSC.App/VRCOSC.App.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/VRCOSC.App/VRCOSC.App.csproj b/VRCOSC.App/VRCOSC.App.csproj index 3e5b1ff7..71718401 100644 --- a/VRCOSC.App/VRCOSC.App.csproj +++ b/VRCOSC.App/VRCOSC.App.csproj @@ -34,12 +34,12 @@ - + - - - + + + From 8d7d8e1cff069d15b48fc03dc1c29e4b81b9dad0 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 29 Dec 2024 22:03:15 +0000 Subject: [PATCH 07/74] Remove nullability from Observable --- VRCOSC.App/Serialisation/Serialiser.cs | 13 ++--- .../SerialisableSettingsManager.cs | 4 +- VRCOSC.App/Utils/Observable.cs | 51 +++++++++---------- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/VRCOSC.App/Serialisation/Serialiser.cs b/VRCOSC.App/Serialisation/Serialiser.cs index 03e7a7a4..559c3153 100644 --- a/VRCOSC.App/Serialisation/Serialiser.cs +++ b/VRCOSC.App/Serialisation/Serialiser.cs @@ -154,18 +154,14 @@ public SerialisationResult Serialise() /// Attempts to convert the to the /// /// This has some special logic to handle different types automatically - protected bool TryConvertToTargetType(object? value, Type targetType, out object? outValue) + protected bool TryConvertToTargetType(object value, Type targetType, out object outValue) { try { switch (value) { - case null: - outValue = null; - return true; - case JToken token: - outValue = token.ToObject(targetType); + outValue = token.ToObject(targetType)!; return true; case var subValue when targetType.IsAssignableTo(typeof(Enum)): @@ -185,9 +181,8 @@ protected bool TryConvertToTargetType(object? value, Type targetType, out object } catch (Exception e) { - Logger.Error(e, "Error converting value to target type"); - outValue = null; - return false; + ExceptionHandler.Handle(e, $"'{FullPath}' was unable to convert {value.GetType().ToReadableName()} to {targetType.ToReadableName()}"); + throw; } } } \ No newline at end of file diff --git a/VRCOSC.App/Settings/Serialisation/SerialisableSettingsManager.cs b/VRCOSC.App/Settings/Serialisation/SerialisableSettingsManager.cs index d5d1c658..675cb4b2 100644 --- a/VRCOSC.App/Settings/Serialisation/SerialisableSettingsManager.cs +++ b/VRCOSC.App/Settings/Serialisation/SerialisableSettingsManager.cs @@ -11,10 +11,10 @@ namespace VRCOSC.App.Settings.Serialisation; public class SerialisableSettingsManager : SerialisableVersion { [JsonProperty("settings")] - public Dictionary Settings = new(); + public Dictionary Settings = new(); [JsonProperty("metadata")] - public Dictionary Metadata = new(); + public Dictionary Metadata = new(); [JsonConstructor] public SerialisableSettingsManager() diff --git a/VRCOSC.App/Utils/Observable.cs b/VRCOSC.App/Utils/Observable.cs index d8a93ea8..bab22697 100644 --- a/VRCOSC.App/Utils/Observable.cs +++ b/VRCOSC.App/Utils/Observable.cs @@ -11,37 +11,34 @@ namespace VRCOSC.App.Utils; +[JsonConverter(typeof(ObservableConverter))] public interface IObservable { public Type GetValueType(); - public object? GetValue(); - public void SetValue(object? value); + public object GetValue(); + public void SetValue(object value); public void Subscribe(Action noValueAction, bool runOnceImmediately = false); -} -[JsonConverter(typeof(ObservableConverter))] -public interface ISerialisableObservable -{ - void SerializeTo(JsonWriter writer, JsonSerializer serializer); - void DeserializeFrom(JsonReader reader, JsonSerializer serializer); + internal void SerializeTo(JsonWriter writer, JsonSerializer serializer); + internal void DeserializeFrom(JsonReader reader, JsonSerializer serializer); } -public class ObservableConverter : JsonConverter +public class ObservableConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, ISerialisableObservable? value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, IObservable? value, JsonSerializer serializer) { value?.SerializeTo(writer, serializer); } - public override ISerialisableObservable ReadJson(JsonReader reader, Type objectType, ISerialisableObservable? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override IObservable ReadJson(JsonReader reader, Type objectType, IObservable? existingValue, bool hasExistingValue, JsonSerializer serializer) { - var observable = existingValue ?? (ISerialisableObservable)Activator.CreateInstance(objectType, true)!; + var observable = existingValue ?? (IObservable)Activator.CreateInstance(objectType, true)!; observable.DeserializeFrom(reader, serializer); return observable; } } -public sealed class Observable : IObservable, INotifyPropertyChanged, ISerialisableObservable, ICloneable, IEquatable> +public sealed class Observable : IObservable, INotifyPropertyChanged, IEquatable> where T : notnull { private T value; @@ -64,14 +61,14 @@ public T Value private readonly List> actions = new(); [JsonConstructor] - public Observable() + private Observable() { } - public Observable(T initialValue = default(T)) + public Observable(T defaultValue = default!) { - DefaultValue = initialValue; - Value = initialValue; + DefaultValue = defaultValue; + Value = defaultValue; } public Observable(Observable other) @@ -82,9 +79,9 @@ public Observable(Observable other) public Type GetValueType() => typeof(T); - public object? GetValue() => Value; + public object GetValue() => Value; - public void SetValue(object? newValue) + public void SetValue(object newValue) { if (newValue is not T castValue) throw new InvalidOperationException($"Attempted to set anonymous value of type {newValue.GetType().ToReadableName()} for type {typeof(T).ToReadableName()}"); @@ -133,13 +130,6 @@ private void notifyObservers() } } - public event PropertyChangedEventHandler? PropertyChanged; - - private void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - public void SerializeTo(JsonWriter writer, JsonSerializer serializer) { try @@ -164,8 +154,6 @@ public void DeserializeFrom(JsonReader reader, JsonSerializer serializer) } } - public object Clone() => new Observable(this); - public bool Equals(Observable? other) { if (ReferenceEquals(null, other)) return false; @@ -173,4 +161,11 @@ public bool Equals(Observable? other) return EqualityComparer.Default.Equals(Value, other.Value); } + + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } \ No newline at end of file From 4fb2d3b49aae7778b678d701e1836224777ccfb1 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 31 Dec 2024 14:26:56 +0000 Subject: [PATCH 08/74] Backup all files when the app updates --- VRCOSC.App/UI/Windows/MainWindow.xaml.cs | 76 +++++++++++++++++++----- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs index 30d22dba..81affd60 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs @@ -47,6 +47,8 @@ namespace VRCOSC.App.UI.Windows; public partial class MainWindow { + private const string root_backup_directory_name = "backups"; + public static MainWindow GetInstance() => Application.Current.Dispatcher.Invoke(() => (MainWindow)Application.Current.MainWindow!); public PackagesView PackagesView = null!; @@ -81,6 +83,8 @@ private async void startApp() { SettingsManager.GetInstance().Load(); + createBackupIfUpdated(); + velopackUpdater = new VelopackUpdater(); if (!velopackUpdater.IsInstalled()) @@ -128,30 +132,74 @@ private void backupV1Files() { try { - if (!storage.Exists("framework.ini")) return; + if (storage.Exists("framework.ini")) + { + var sourceDir = storage.GetFullPath(string.Empty); + var destinationDir = storage.GetStorageForDirectory(root_backup_directory_name).GetStorageForDirectory("v1").GetFullPath(string.Empty); + + foreach (var file in Directory.GetFiles(sourceDir)) + { + var fileName = Path.GetFileName(file); + var destFile = Path.Combine(destinationDir, fileName); + File.Move(file, destFile, false); + } - var sourceDir = storage.GetFullPath(string.Empty); - var destinationDir = storage.GetStorageForDirectory("v1-backup").GetFullPath(string.Empty); + foreach (var dir in Directory.GetDirectories(sourceDir)) + { + var dirName = Path.GetFileName(dir); + if (dirName == root_backup_directory_name) continue; - foreach (var file in Directory.GetFiles(sourceDir)) - { - var fileName = Path.GetFileName(file); - var destFile = Path.Combine(destinationDir, fileName); - File.Move(file, destFile, false); + var destDir = Path.Combine(destinationDir, dirName); + Directory.Move(dir, destDir); + } } + } + catch (Exception e) + { + Logger.Error(e, "Could not backup V1 files"); + } + + // move v1-backup into backups/v1 - foreach (var dir in Directory.GetDirectories(sourceDir)) + try + { + if (storage.ExistsDirectory("v1-backup")) { - var dirName = Path.GetFileName(dir); - if (dirName == "v1-backup") continue; + var sourceDir = storage.GetFullPath("v1-backup"); + var destinationDir = storage.GetStorageForDirectory(root_backup_directory_name).GetFullPath("v1"); - var destDir = Path.Combine(destinationDir, dirName); - Directory.Move(dir, destDir); + Directory.Move(sourceDir, destinationDir); } } catch (Exception e) { - Logger.Error(e, "Could not backup V1 files"); + Logger.Error(e, "An error has occured moving the V1 backup folder"); + } + } + + private void createBackupIfUpdated() + { + var installedVersionStr = SettingsManager.GetInstance().GetValue(VRCOSCMetadata.InstalledVersion); + if (string.IsNullOrEmpty(installedVersionStr)) return; + + var newVersion = SemVersion.Parse(AppManager.Version, SemVersionStyles.Any); + var installedVersion = SemVersion.Parse(installedVersionStr, SemVersionStyles.Any); + + var hasUpdated = SemVersion.ComparePrecedence(newVersion, installedVersion) == 1; + if (!hasUpdated) return; + + Logger.Log("App has updated. Creating a backup for previous version"); + + var sourceDir = storage.GetFullPath(string.Empty); + var destDir = storage.GetStorageForDirectory(root_backup_directory_name).GetStorageForDirectory(installedVersionStr).GetFullPath(string.Empty); + + foreach (var dir in Directory.GetDirectories(sourceDir)) + { + var dirName = Path.GetFileName(dir); + // don't want `backups`. Don't need `logs` or `runtime` directories + if (dirName is root_backup_directory_name or "logs" or "runtime") continue; + + storage.GetStorageForDirectory(dirName).CopyTo(Path.Combine(destDir, dirName)); } } From 2d30408e34e7ea49ad0f647b43b585c81f267c99 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 31 Dec 2024 14:27:08 +0000 Subject: [PATCH 09/74] Small fixes --- VRCOSC.App/Profiles/ProfileManager.cs | 4 +--- VRCOSC.App/Settings/SettingsManager.cs | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/VRCOSC.App/Profiles/ProfileManager.cs b/VRCOSC.App/Profiles/ProfileManager.cs index 291df1e0..6cd00196 100644 --- a/VRCOSC.App/Profiles/ProfileManager.cs +++ b/VRCOSC.App/Profiles/ProfileManager.cs @@ -59,9 +59,7 @@ public ProfileManager() public void Load() { - Logger.Log("Loading profiles"); - var deserialisationResult = serialisationManager.Deserialise(false); - Logger.Log($"Profiles ended in {deserialisationResult}"); + serialisationManager.Deserialise(false); checkForDefault(); diff --git a/VRCOSC.App/Settings/SettingsManager.cs b/VRCOSC.App/Settings/SettingsManager.cs index 001e2f43..473c0f8a 100644 --- a/VRCOSC.App/Settings/SettingsManager.cs +++ b/VRCOSC.App/Settings/SettingsManager.cs @@ -40,12 +40,12 @@ public void Load() serialisationManager.Deserialise(); } - private void setDefault(VRCOSCSetting lookup, T? defaultValue) + private void setDefault(VRCOSCSetting lookup, T defaultValue) where T : notnull { Settings[lookup] = (IObservable)Activator.CreateInstance(typeof(Observable), defaultValue)!; } - private void setDefault(VRCOSCMetadata lookup, T? defaultValue) + private void setDefault(VRCOSCMetadata lookup, T defaultValue) where T : notnull { Metadata[lookup] = (IObservable)Activator.CreateInstance(typeof(Observable), defaultValue)!; } @@ -84,7 +84,7 @@ private void writeDefaults() setDefault(VRCOSCMetadata.AutoStartQuestionClicked, false); } - public Observable GetObservable(VRCOSCSetting lookup) + public Observable GetObservable(VRCOSCSetting lookup) where T : notnull { if (!Settings.TryGetValue(lookup, out var observable)) throw new InvalidOperationException("Setting doesn't exist"); if (observable is not Observable castObservable) throw new InvalidOperationException($"Setting is not of type {typeof(T).ToReadableName()}"); @@ -92,7 +92,7 @@ public Observable GetObservable(VRCOSCSetting lookup) return castObservable; } - public Observable GetObservable(VRCOSCMetadata lookup) + public Observable GetObservable(VRCOSCMetadata lookup) where T : notnull { if (!Metadata.TryGetValue(lookup, out var observable)) throw new InvalidOperationException("Metadata doesn't exist"); if (observable is not Observable castObservable) throw new InvalidOperationException($"Metadata is not of type {typeof(T).ToReadableName()}"); From eac44f9e23a00aa3677cc0a6bc814019b87e7040 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 31 Dec 2024 14:27:13 +0000 Subject: [PATCH 10/74] Bump Velopack --- VRCOSC.App/VRCOSC.App.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.App/VRCOSC.App.csproj b/VRCOSC.App/VRCOSC.App.csproj index 71718401..4a36b4d2 100644 --- a/VRCOSC.App/VRCOSC.App.csproj +++ b/VRCOSC.App/VRCOSC.App.csproj @@ -34,7 +34,7 @@ - + From 2d41b78b824f47aaf4cda11d7eb9105fd3081140 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 31 Dec 2024 15:52:44 +0000 Subject: [PATCH 11/74] Disallow module logs when app manager isn't running --- VRCOSC.App/UI/Views/Run/RunView.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.App/UI/Views/Run/RunView.xaml.cs b/VRCOSC.App/UI/Views/Run/RunView.xaml.cs index db6736f8..06d290ea 100644 --- a/VRCOSC.App/UI/Views/Run/RunView.xaml.cs +++ b/VRCOSC.App/UI/Views/Run/RunView.xaml.cs @@ -127,7 +127,7 @@ private void onAppManagerStateChange(AppManagerState newState) => Dispatcher.Inv private void onLogEntry(LogEntry e) => Dispatcher.Invoke(() => { - if (e.Target != LoggingTarget.Terminal) return; + if (e.Target != LoggingTarget.Terminal || AppManager.GetInstance().State.Value == AppManagerState.Stopped || AppManager.GetInstance().State.Value == AppManagerState.Waiting) return; var dateTimeText = $"[{DateTime.Now:HH:mm:ss}] {e.Message}"; From bbebedb02a2870e70e1f95f13f4d9be0be36bc10 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 31 Dec 2024 16:41:04 +0000 Subject: [PATCH 12/74] Convert all P/Invoke library and Win32 calls to source generated code --- VRCOSC.App/NativeMethods.txt | 7 ++ VRCOSC.App/UI/Windows/MainWindow.xaml.cs | 8 ++- VRCOSC.App/Utils/Extensions.cs | 88 +++++++++--------------- VRCOSC.App/VRCOSC.App.csproj | 6 +- 4 files changed, 49 insertions(+), 60 deletions(-) create mode 100644 VRCOSC.App/NativeMethods.txt diff --git a/VRCOSC.App/NativeMethods.txt b/VRCOSC.App/NativeMethods.txt new file mode 100644 index 00000000..8c011ab5 --- /dev/null +++ b/VRCOSC.App/NativeMethods.txt @@ -0,0 +1,7 @@ +ShowWindow +GetForegroundWindow +EnumWindows +GetWindowThreadProcessId +ToUnicodeEx +MapVirtualKey +GetKeyboardLayout \ No newline at end of file diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs index 81affd60..111bd00f 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs @@ -10,8 +10,10 @@ using System.Windows; using System.Windows.Forms; using System.Windows.Interop; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; using Newtonsoft.Json; -using PInvoke; using Semver; using VRCOSC.App.Actions; using VRCOSC.App.ChatBox; @@ -408,7 +410,7 @@ private void handleTrayTransition() { if (window == mainWindow) { - User32.ShowWindow(new WindowInteropHelper(mainWindow).Handle, User32.WindowShowStyle.SW_HIDE); + PInvoke.ShowWindow(new HWND(new WindowInteropHelper(mainWindow).Handle), SHOW_WINDOW_CMD.SW_HIDE); } else { @@ -418,7 +420,7 @@ private void handleTrayTransition() } else { - User32.ShowWindow(new WindowInteropHelper(mainWindow).Handle, User32.WindowShowStyle.SW_SHOWDEFAULT); + PInvoke.ShowWindow(new HWND(new WindowInteropHelper(mainWindow).Handle), SHOW_WINDOW_CMD.SW_SHOWDEFAULT); mainWindow.Activate(); } } diff --git a/VRCOSC.App/Utils/Extensions.cs b/VRCOSC.App/Utils/Extensions.cs index 99054122..e2b103fe 100644 --- a/VRCOSC.App/Utils/Extensions.cs +++ b/VRCOSC.App/Utils/Extensions.cs @@ -6,11 +6,12 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using System.Windows.Input; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using Windows.Win32.UI.WindowsAndMessaging; using NAudio.CoreAudioApi; -using PInvoke; namespace VRCOSC.App.Utils; @@ -115,23 +116,7 @@ public static class EnumExtensions public static class KeyExtensions { - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern int ToUnicodeEx( - uint wVirtKey, - uint wScanCode, - byte[] lpKeyState, - [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwszBuff, - int cchBuff, - uint wFlags, - IntPtr dwhkl); - - [DllImport("user32.dll")] - private static extern uint MapVirtualKey(uint uCode, uint uMapType); - - [DllImport("user32.dll")] - private static extern IntPtr GetKeyboardLayout(uint idThread); - - public static string ToReadableString(this Key key) + public static unsafe string ToReadableString(this Key key) { // Handle special cases for non-character keys switch (key) @@ -206,12 +191,19 @@ public static string ToReadableString(this Key key) // Only take the current key into account to stop modifiers from ruining the representation keyboardState[virtualKey] = 0x80; - var scanCode = MapVirtualKey(virtualKey, 0x02); - var sb = new StringBuilder(10); - var keyboardLayout = GetKeyboardLayout(0); + var scanCode = PInvoke.MapVirtualKey(virtualKey, MAP_VIRTUAL_KEY_TYPE.MAPVK_VK_TO_CHAR); + var output = new char[10]; - var result = ToUnicodeEx(virtualKey, scanCode, keyboardState, sb, sb.Capacity, 0, keyboardLayout); - return result > 0 ? sb.Length == 1 ? sb.ToString().ToUpperInvariant() : sb.ToString() : key.ToString(); + fixed (byte* keyboardStatePtr = keyboardState) + { + fixed (char* outputPtr = output) + { + var keyboardLayout = PInvoke.GetKeyboardLayout(0); + + var result = PInvoke.ToUnicodeEx(virtualKey, scanCode, keyboardStatePtr, new PWSTR(outputPtr), 10, 0, keyboardLayout); + return result > 0 ? output[0] != 0x0 && output[1] == 0x0 ? output[0].ToString().ToUpperInvariant() : new string(output) : key.ToString(); + } + } } } @@ -257,18 +249,18 @@ public static bool HasConstructorThatAccepts(this Type targetType, params Type[] public static class ProcessExtensions { - public static string? GetActiveWindowTitle() + public static unsafe string? GetActiveWindowTitle() { - var foregroundWindowHandle = User32.GetForegroundWindow(); + var foregroundWindowHandle = PInvoke.GetForegroundWindow(); if (foregroundWindowHandle == IntPtr.Zero) return null; - _ = User32.GetWindowThreadProcessId(foregroundWindowHandle, out int processId); - - if (processId <= 0) return null; + uint processId = 0; + var result = PInvoke.GetWindowThreadProcessId(foregroundWindowHandle, &processId); + if (result == 0 || processId == 0) return null; try { - return Process.GetProcessById(processId).ProcessName; + return Process.GetProcessById((int)processId).ProcessName; } catch (ArgumentException) { @@ -301,45 +293,29 @@ public static void SetProcessVolume(string? processName, float percentage) processAudioVolume.Volume = percentage; } - [DllImport("user32.dll", SetLastError = true)] - private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - - [DllImport("user32.dll", SetLastError = true)] - private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); - - [DllImport("user32.dll", SetLastError = true)] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - private const int sw_minimize = 6; - private const int sw_restore = 9; - - private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - public static void SetWindowVisibility(this Process process, bool visible) { - if (process == null) throw new ArgumentNullException(nameof(process)); + ArgumentNullException.ThrowIfNull(process); var windowHandle = findWindowByProcessId(process.Id); if (windowHandle == IntPtr.Zero) throw new InvalidOperationException("The process does not have a visible main window"); - ShowWindow(windowHandle, visible ? sw_restore : sw_minimize); + PInvoke.ShowWindow(new HWND(windowHandle), visible ? SHOW_WINDOW_CMD.SW_RESTORE : SHOW_WINDOW_CMD.SW_MINIMIZE); } - private static IntPtr findWindowByProcessId(int processId) + private static unsafe IntPtr findWindowByProcessId(int processId) { IntPtr windowHandle = IntPtr.Zero; - EnumWindows((hWnd, _) => + PInvoke.EnumWindows((hWnd, _) => { - GetWindowThreadProcessId(hWnd, out uint windowProcessId); + uint windowProcessId = 0; - if (windowProcessId == processId) - { - windowHandle = hWnd; - return false; - } + var result = PInvoke.GetWindowThreadProcessId(hWnd, &windowProcessId); + if (result == 0 || windowProcessId != processId) return true; - return true; + windowHandle = hWnd; + return false; }, IntPtr.Zero); return windowHandle; diff --git a/VRCOSC.App/VRCOSC.App.csproj b/VRCOSC.App/VRCOSC.App.csproj index 4a36b4d2..872fd275 100644 --- a/VRCOSC.App/VRCOSC.App.csproj +++ b/VRCOSC.App/VRCOSC.App.csproj @@ -20,6 +20,7 @@ latestmajor true 10.0.22621.52 + true @@ -28,11 +29,14 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - From b1c15ec03d04a4d699354f0777b527e1a8c7f03f Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 31 Dec 2024 18:38:00 +0000 Subject: [PATCH 13/74] Allow speech engine to be disabled --- VRCOSC.App/AppManager.cs | 2 +- VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs | 7 +++++++ VRCOSC.App/Settings/SettingsManager.cs | 3 ++- .../UI/Views/AppSettings/AppSettingsView.xaml | 13 +++++++++++++ .../UI/Views/AppSettings/AppSettingsView.xaml.cs | 12 ++++++------ 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/VRCOSC.App/AppManager.cs b/VRCOSC.App/AppManager.cs index f1e4e875..b611dfad 100644 --- a/VRCOSC.App/AppManager.cs +++ b/VRCOSC.App/AppManager.cs @@ -410,7 +410,7 @@ private async Task startAsync() await ModuleManager.GetInstance().StartAsync(); VRChatLogReader.Start(); - if (ModuleManager.GetInstance().GetRunningModulesOfType().Any()) + if (ModuleManager.GetInstance().GetRunningModulesOfType().Any() && SettingsManager.GetInstance().GetValue(VRCOSCSetting.SpeechEnabled)) { if (string.IsNullOrWhiteSpace(SettingsManager.GetInstance().GetValue(VRCOSCSetting.SpeechModelPath))) { diff --git a/VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs b/VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs index f369a165..78b92705 100644 --- a/VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs +++ b/VRCOSC.App/Audio/Whisper/WhisperSpeechEngine.cs @@ -16,6 +16,7 @@ public class WhisperSpeechEngine : SpeechEngine private Repeater? repeater; private SpeechResult? result; private AudioEndpointNotificationClient? audioNotificationClient; + private bool initialised; public override void Initialise() { @@ -44,6 +45,8 @@ public override void Initialise() }; AudioDeviceHelper.RegisterCallbackClient(audioNotificationClient); + + initialised = true; } private async void onCaptureDeviceIdChanged(string newDeviceId) @@ -111,6 +114,8 @@ private async void processResult() public override async Task Teardown() { + if (!initialised) return; + var captureDeviceId = SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SelectedMicrophoneID); captureDeviceId.Unsubscribe(onCaptureDeviceIdChanged); @@ -128,5 +133,7 @@ public override async Task Teardown() audioProcessor = null; repeater = null; result = null; + + initialised = false; } } \ No newline at end of file diff --git a/VRCOSC.App/Settings/SettingsManager.cs b/VRCOSC.App/Settings/SettingsManager.cs index 473c0f8a..656342cb 100644 --- a/VRCOSC.App/Settings/SettingsManager.cs +++ b/VRCOSC.App/Settings/SettingsManager.cs @@ -72,6 +72,7 @@ private void writeDefaults() setDefault(VRCOSCSetting.EnableAppDebug, false); setDefault(VRCOSCSetting.EnableRouter, false); setDefault(VRCOSCSetting.SelectedMicrophoneID, string.Empty); + setDefault(VRCOSCSetting.SpeechEnabled, true); setDefault(VRCOSCSetting.SpeechModelPath, string.Empty); setDefault(VRCOSCSetting.SpeechConfidence, 0.4f); setDefault(VRCOSCSetting.SpeechNoiseCutoff, 0.14f); @@ -140,7 +141,7 @@ public enum VRCOSCSetting EnableAppDebug, EnableRouter, SelectedMicrophoneID, - SelectedSpeechEngine, + SpeechEnabled, SpeechModelPath, SpeechConfidence, SpeechNoiseCutoff, diff --git a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml index 04e9425d..d6e266f3 100644 --- a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml +++ b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml @@ -315,6 +315,19 @@ + + + + + + + diff --git a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs index 62c9f73c..6478ee2c 100644 --- a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs +++ b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs @@ -122,18 +122,18 @@ public bool EnableRouter set => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.EnableRouter).Value = value; } - public SpeechEngine SelectedSpeechEngine - { - get => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SelectedSpeechEngine).Value; - set => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SelectedSpeechEngine).Value = value; - } - public string WhisperModelFilePath { get => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechModelPath).Value; set => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechModelPath).Value = value; } + public bool SpeechEnabled + { + get => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechEnabled).Value; + set => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechEnabled).Value = value; + } + public bool SpeechTranslate { get => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechTranslate).Value; From 754b1e0a1a920bca16be9f9a181c51f372ea1238 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 31 Dec 2024 18:38:51 +0000 Subject: [PATCH 14/74] Fix readable type name of objects not using the actual type name --- VRCOSC.App/Utils/Extensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VRCOSC.App/Utils/Extensions.cs b/VRCOSC.App/Utils/Extensions.cs index e2b103fe..495f1fe2 100644 --- a/VRCOSC.App/Utils/Extensions.cs +++ b/VRCOSC.App/Utils/Extensions.cs @@ -200,7 +200,7 @@ public static unsafe string ToReadableString(this Key key) { var keyboardLayout = PInvoke.GetKeyboardLayout(0); - var result = PInvoke.ToUnicodeEx(virtualKey, scanCode, keyboardStatePtr, new PWSTR(outputPtr), 10, 0, keyboardLayout); + var result = PInvoke.ToUnicodeEx(virtualKey, scanCode, keyboardStatePtr, new PWSTR(outputPtr), output.Length, 0, keyboardLayout); return result > 0 ? output[0] != 0x0 && output[1] == 0x0 ? output[0].ToString().ToUpperInvariant() : new string(output) : key.ToString(); } } @@ -216,7 +216,7 @@ public static string ToReadableName(this Type type) return Type.GetTypeCode(type) switch { TypeCode.Empty => "Null", - TypeCode.Object => "Object", + TypeCode.Object => type.Name, TypeCode.DBNull => "DBNull", TypeCode.Boolean => "Bool", TypeCode.Char => "Char", From bea838035b305708870e3cfa26c27803e74ea506 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 12 Jan 2025 21:09:33 +0000 Subject: [PATCH 15/74] Allow forks to be included as packages --- VRCOSC.App/Actions/Packages/SearchRepositoriesAction.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/VRCOSC.App/Actions/Packages/SearchRepositoriesAction.cs b/VRCOSC.App/Actions/Packages/SearchRepositoriesAction.cs index 636440ba..1c504777 100644 --- a/VRCOSC.App/Actions/Packages/SearchRepositoriesAction.cs +++ b/VRCOSC.App/Actions/Packages/SearchRepositoriesAction.cs @@ -23,7 +23,8 @@ protected override async Task Perform() { var repos = await GitHubProxy.Client.Search.SearchRepo(new SearchRepositoriesRequest { - Topic = tag + Topic = tag, + Fork = ForkQualifier.IncludeForks }).WaitAsync(TimeSpan.FromSeconds(5)); Result = repos; From 295ed3bafa7ab0f4e4a0247d49d6f13e43ec358b Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 12 Jan 2025 21:09:42 +0000 Subject: [PATCH 16/74] Ensure audio capture is started in shared mode --- VRCOSC.App/Audio/AudioCapture.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/VRCOSC.App/Audio/AudioCapture.cs b/VRCOSC.App/Audio/AudioCapture.cs index e130fab7..88cd9aab 100644 --- a/VRCOSC.App/Audio/AudioCapture.cs +++ b/VRCOSC.App/Audio/AudioCapture.cs @@ -19,7 +19,11 @@ internal class AudioCapture public AudioCapture(MMDevice device) { - capture = new WasapiCapture(device); + capture = new WasapiCapture(device) + { + ShareMode = AudioClientShareMode.Shared + }; + buffer = new MemoryStream(); capture.DataAvailable += OnDataAvailable; From f7080842068edcf182bd44b2866766582b8079c2 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 12 Jan 2025 21:10:02 +0000 Subject: [PATCH 17/74] Fix regex characters in parameters not being escaped --- VRCOSC.App/SDK/Modules/Module.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.App/SDK/Modules/Module.cs b/VRCOSC.App/SDK/Modules/Module.cs index b34bb51d..36e56baa 100644 --- a/VRCOSC.App/SDK/Modules/Module.cs +++ b/VRCOSC.App/SDK/Modules/Module.cs @@ -191,7 +191,7 @@ private static Regex parameterToRegex(string parameterName) { var pattern = "^"; // start of string pattern += @"(?:VF\d+_)?"; // VRCFury prefix - pattern += $"({parameterName.Replace("/", @"\/").Replace("*", @"(?:\S*)")})"; + pattern += $"({Regex.Escape(parameterName).Replace(@"\*", @"(?:\S*)")})"; pattern += "$"; // end of string return new Regex(pattern); From 8063eb0257cefeeed38ae6ca6bb02f8394539073 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Mon, 13 Jan 2025 01:27:59 +0000 Subject: [PATCH 18/74] Small optimisations --- VRCOSC.App/Utils/Extensions.cs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/VRCOSC.App/Utils/Extensions.cs b/VRCOSC.App/Utils/Extensions.cs index 495f1fe2..5cd05672 100644 --- a/VRCOSC.App/Utils/Extensions.cs +++ b/VRCOSC.App/Utils/Extensions.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Windows.Input; using Windows.Win32; using Windows.Win32.Foundation; @@ -97,7 +98,7 @@ public static class UriExtensions public static class StringExtensions { - public static string Pluralise(this string str) => str + (str.EndsWith("s") ? "'" : "'s"); + public static string Pluralise(this string str) => str + (str.EndsWith('s') ? "'" : "'s"); } public static class IntegerExtensions @@ -254,9 +255,9 @@ public static class ProcessExtensions var foregroundWindowHandle = PInvoke.GetForegroundWindow(); if (foregroundWindowHandle == IntPtr.Zero) return null; - uint processId = 0; + var processId = 0u; var result = PInvoke.GetWindowThreadProcessId(foregroundWindowHandle, &processId); - if (result == 0 || processId == 0) return null; + if (result == 0u || processId == 0u) return null; try { @@ -295,29 +296,35 @@ public static void SetProcessVolume(string? processName, float percentage) public static void SetWindowVisibility(this Process process, bool visible) { - ArgumentNullException.ThrowIfNull(process); - - var windowHandle = findWindowByProcessId(process.Id); + var windowHandle = findWindowByProcessId((uint)process.Id); if (windowHandle == IntPtr.Zero) throw new InvalidOperationException("The process does not have a visible main window"); - PInvoke.ShowWindow(new HWND(windowHandle), visible ? SHOW_WINDOW_CMD.SW_RESTORE : SHOW_WINDOW_CMD.SW_MINIMIZE); + var result = PInvoke.ShowWindow(new HWND(windowHandle), visible ? SHOW_WINDOW_CMD.SW_RESTORE : SHOW_WINDOW_CMD.SW_MINIMIZE); + logResult(result, $"Could not alter window state with ID {process.Id}"); } - private static unsafe IntPtr findWindowByProcessId(int processId) + private static unsafe IntPtr findWindowByProcessId(uint processId) { - IntPtr windowHandle = IntPtr.Zero; + var windowHandle = IntPtr.Zero; - PInvoke.EnumWindows((hWnd, _) => + var result = PInvoke.EnumWindows((hWnd, _) => { - uint windowProcessId = 0; + var windowProcessId = 0u; var result = PInvoke.GetWindowThreadProcessId(hWnd, &windowProcessId); - if (result == 0 || windowProcessId != processId) return true; + if (result == 0u || windowProcessId != processId) return true; windowHandle = hWnd; return false; }, IntPtr.Zero); + logResult(result, $"Could not find window process with ID {processId}"); + return windowHandle; } + + private static void logResult(BOOL result, string message) + { + if (!result) Logger.Log($"{message}. Error code: {Marshal.GetLastWin32Error()}"); + } } \ No newline at end of file From 45224931237ed0c210ef2fd20b68df9300fddfd2 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Mon, 13 Jan 2025 01:31:19 +0000 Subject: [PATCH 19/74] Remove VRChat API code --- VRCOSC.App/AppManager.cs | 3 - .../Views/AppSettings/AppSettingsView.xaml.cs | 5 +- .../UI/Views/Settings/SettingsView.xaml | 33 ---- .../UI/Views/Settings/SettingsView.xaml.cs | 42 ----- VRCOSC.App/VRCOSC.App.csproj | 1 - VRCOSC.App/VRChatAPI/AuthenticationHandler.cs | 165 ------------------ VRCOSC.App/VRChatAPI/UserHttpInfo.cs | 13 -- VRCOSC.App/VRChatAPI/VRChatAPIClient.cs | 70 -------- 8 files changed, 3 insertions(+), 329 deletions(-) delete mode 100644 VRCOSC.App/UI/Views/Settings/SettingsView.xaml delete mode 100644 VRCOSC.App/UI/Views/Settings/SettingsView.xaml.cs delete mode 100644 VRCOSC.App/VRChatAPI/AuthenticationHandler.cs delete mode 100644 VRCOSC.App/VRChatAPI/UserHttpInfo.cs delete mode 100644 VRCOSC.App/VRChatAPI/VRChatAPIClient.cs diff --git a/VRCOSC.App/AppManager.cs b/VRCOSC.App/AppManager.cs index b611dfad..09dcdb7b 100644 --- a/VRCOSC.App/AppManager.cs +++ b/VRCOSC.App/AppManager.cs @@ -34,7 +34,6 @@ using VRCOSC.App.UI.Themes; using VRCOSC.App.UI.Windows; using VRCOSC.App.Utils; -using VRCOSC.App.VRChatAPI; using Module = VRCOSC.App.SDK.Modules.Module; namespace VRCOSC.App; @@ -61,7 +60,6 @@ public class AppManager public ConnectionManager ConnectionManager = null!; public VRChatOscClient VRChatOscClient = null!; public VRChatClient VRChatClient = null!; - public VRChatAPIClient VRChatAPIClient = null!; public OVRClient OVRClient = null!; public WhisperSpeechEngine SpeechEngine = null!; @@ -88,7 +86,6 @@ public void Initialise() ConnectionManager = new ConnectionManager(); VRChatOscClient = new VRChatOscClient(); VRChatClient = new VRChatClient(VRChatOscClient); - VRChatAPIClient = new VRChatAPIClient(); OVRClient = new OVRClient(); ChatBoxWorldBlacklist.Init(); diff --git a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs index 6478ee2c..619875f0 100644 --- a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs +++ b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs @@ -14,7 +14,6 @@ using VRCOSC.App.Audio; using VRCOSC.App.Settings; using VRCOSC.App.UI.Themes; -using VRCOSC.App.UI.Views.Settings; using VRCOSC.App.Updater; using VRCOSC.App.Utils; @@ -288,4 +287,6 @@ private bool isValidIpPort(string input) const string pattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):([0-9]{1,5})$"; return Regex.IsMatch(input, pattern); } -} \ No newline at end of file +} + +public record DeviceDisplay(string ID, string Name); \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/Settings/SettingsView.xaml b/VRCOSC.App/UI/Views/Settings/SettingsView.xaml deleted file mode 100644 index 37949a89..00000000 --- a/VRCOSC.App/UI/Views/Settings/SettingsView.xaml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/VRCOSC.App/UI/Views/Settings/SettingsView.xaml.cs b/VRCOSC.App/UI/Views/Settings/SettingsView.xaml.cs deleted file mode 100644 index 1abae678..00000000 --- a/VRCOSC.App/UI/Views/Settings/SettingsView.xaml.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. -// See the LICENSE file in the repository root for full license text. - -using System.Windows; -using VRCOSC.App.VRChatAPI; - -namespace VRCOSC.App.UI.Views.Settings; - -public partial class SettingsView -{ - public SettingsView() - { - InitializeComponent(); - - AppManager.GetInstance().VRChatAPIClient.AuthHandler.State.Subscribe(newState => - { - StateDisplay.Text = newState.ToString(); - TokenDisplay.Text = newState == AuthenticationState.LoggedIn ? AppManager.GetInstance().VRChatAPIClient.AuthHandler.AuthToken : string.Empty; - }); - } - - private void Login_OnClick(object sender, RoutedEventArgs e) - { - var username = Username.Text; - var password = Password.Password; - AppManager.GetInstance().VRChatAPIClient.AuthHandler.LoginWithCredentials(username, password); - } - - private void Token_OnClick(object sender, RoutedEventArgs e) - { - var token = TokenInput.Text; - AppManager.GetInstance().VRChatAPIClient.AuthHandler.LoginWithAuthToken(token); - } - - private void TwoFactorAuth_OnClick(object sender, RoutedEventArgs e) - { - var code = TwoFactorAuth.Text; - AppManager.GetInstance().VRChatAPIClient.AuthHandler.Verify2FACode(code, false); - } -} - -public record DeviceDisplay(string ID, string Name); \ No newline at end of file diff --git a/VRCOSC.App/VRCOSC.App.csproj b/VRCOSC.App/VRCOSC.App.csproj index 872fd275..8f71b044 100644 --- a/VRCOSC.App/VRCOSC.App.csproj +++ b/VRCOSC.App/VRCOSC.App.csproj @@ -40,7 +40,6 @@ - diff --git a/VRCOSC.App/VRChatAPI/AuthenticationHandler.cs b/VRCOSC.App/VRChatAPI/AuthenticationHandler.cs deleted file mode 100644 index 9e4064a6..00000000 --- a/VRCOSC.App/VRChatAPI/AuthenticationHandler.cs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. -// See the LICENSE file in the repository root for full license text. - -using System; -using System.Text.Encodings.Web; -using Newtonsoft.Json; -using VRChat.API.Api; -using VRChat.API.Client; -using VRChat.API.Model; -using VRCOSC.App.Utils; - -namespace VRCOSC.App.VRChatAPI; - -public class AuthenticationHandler -{ - private const string base_path = "https://api.vrchat.cloud/api/1"; - private const string api_key = ""; - private const string user_agent = $"{AppManager.APP_NAME} volcanicarts"; - - public Observable State { get; } = new(); - - public IAuthenticationApi? AuthAPI { get; private set; } - public Configuration? Configuration { get; private set; } - public string? AuthToken { get; private set; } - - public void Logout() - { - if (AuthAPI?.GetCurrentUser() is null) - { - throw new InvalidOperationException("Cannot logout while already logged out"); - } - - AuthAPI.Logout(); - AuthAPI = null; - Configuration = null; - AuthToken = null; - State.Value = AuthenticationState.LoggedOut; - } - - public void LoginWithAuthToken(string authToken) - { - if (AuthAPI?.GetCurrentUser() is not null) - { - throw new InvalidOperationException("Cannot login while already logged in"); - } - - Configuration = makeConfigurationWithAuthToken(authToken); - AuthAPI = new AuthenticationApi(Configuration); - - if (!AuthAPI.VerifyAuthToken().Ok) - { - State.Value = AuthenticationState.InvalidCredentials; - return; - } - - AuthToken = authToken; - State.Value = AuthenticationState.LoggedIn; - } - - public void LoginWithCredentials(string username, string password) - { - if (AuthAPI?.GetCurrentUser() is not null) - { - throw new InvalidOperationException("Cannot login while already logged in"); - } - - Configuration = makeConfigurationWithCredentials(username, password); - AuthAPI = new AuthenticationApi(Configuration); - - var httpInfo = JsonConvert.DeserializeObject(AuthAPI.GetCurrentUserWithHttpInfo().RawContent); - - if (httpInfo is null) - { - State.Value = AuthenticationState.InvalidCredentials; - return; - } - - if (httpInfo.TwoFactorAuthTypes.Contains("totp")) - { - State.Value = AuthenticationState.Requires2FA; - return; - } - - if (httpInfo.TwoFactorAuthTypes.Contains("otp")) - { - State.Value = AuthenticationState.Requires2FAEmail; - return; - } - - AuthToken = AuthAPI.VerifyAuthToken().Token; - State.Value = AuthenticationState.LoggedIn; - } - - public void Verify2FACode(string code, bool isEmail) - { - if (AuthAPI is null || Configuration is null) - { - throw new InvalidOperationException("Cannot verify 2FA code before a login attempt"); - } - - if (isEmail) - { - var result = AuthAPI.Verify2FAEmailCode(new TwoFactorEmailCode(code)); - - if (!result.Verified) - { - State.Value = AuthenticationState.Invalid2FA; - return; - } - } - else - { - var result = AuthAPI.Verify2FA(new TwoFactorAuthCode(code)); - - if (!result.Verified) - { - State.Value = AuthenticationState.Invalid2FA; - return; - } - } - - AuthToken = AuthAPI.VerifyAuthToken().Token; - State.Value = AuthenticationState.LoggedIn; - } - - private Configuration makeConfigurationWithAuthToken(string authKey) - { - return new Configuration - { - BasePath = base_path, - UserAgent = user_agent, - Timeout = 5000, - DefaultHeaders = - { - ["Cookie"] = $"apiKey={api_key}; auth={authKey}" - } - }; - } - - private Configuration makeConfigurationWithCredentials(string username, string password) - { - return new Configuration - { - BasePath = base_path, - UserAgent = user_agent, - Username = UrlEncoder.Default.Encode(username), - Password = UrlEncoder.Default.Encode(password), - Timeout = 5000, - DefaultHeaders = - { - ["Cookie"] = $"apiKey={api_key}" - } - }; - } -} - -public enum AuthenticationState -{ - LoggedOut, - InvalidCredentials, - Requires2FA, - Requires2FAEmail, - Invalid2FA, - LoggedIn -} diff --git a/VRCOSC.App/VRChatAPI/UserHttpInfo.cs b/VRCOSC.App/VRChatAPI/UserHttpInfo.cs deleted file mode 100644 index 9f62fa2d..00000000 --- a/VRCOSC.App/VRChatAPI/UserHttpInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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.Generic; -using Newtonsoft.Json; - -namespace VRCOSC.App.VRChatAPI; - -public class UserHttpInfo -{ - [JsonProperty("requiresTwoFactorAuth")] - public List TwoFactorAuthTypes = null!; -} diff --git a/VRCOSC.App/VRChatAPI/VRChatAPIClient.cs b/VRCOSC.App/VRChatAPI/VRChatAPIClient.cs deleted file mode 100644 index d0bc32ad..00000000 --- a/VRCOSC.App/VRChatAPI/VRChatAPIClient.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. -// See the LICENSE file in the repository root for full license text. - -using VRChat.API.Api; - -namespace VRCOSC.App.VRChatAPI; - -public class VRChatAPIClient -{ - internal AuthenticationHandler AuthHandler { get; } = new(); - - public ISystemApi? System { get; private set; } - public IUsersApi? Users { get; private set; } - public IAvatarsApi? Avatars { get; private set; } - public IWorldsApi? Worlds { get; private set; } - public IFriendsApi? Friends { get; private set; } - public IEconomyApi? Economy { get; private set; } - public IFavoritesApi? Favorites { get; private set; } - public IGroupsApi? Groups { get; private set; } - public IFilesApi? Files { get; private set; } - public IInstancesApi? Instances { get; private set; } - public IPermissionsApi? Permissions { get; private set; } - public IInviteApi? Invite { get; private set; } - public IPlayermoderationApi? PlayerModeration { get; private set; } - public INotificationsApi? Notifications { get; private set; } - - public VRChatAPIClient() - { - AuthHandler.State.Subscribe(newState => - { - if (newState == AuthenticationState.LoggedIn) - { - System = new SystemApi(AuthHandler.Configuration); - Users = new UsersApi(AuthHandler.Configuration); - Avatars = new AvatarsApi(AuthHandler.Configuration); - Worlds = new WorldsApi(AuthHandler.Configuration); - Friends = new FriendsApi(AuthHandler.Configuration); - Economy = new EconomyApi(AuthHandler.Configuration); - Favorites = new FavoritesApi(AuthHandler.Configuration); - Groups = new GroupsApi(AuthHandler.Configuration); - Files = new FilesApi(AuthHandler.Configuration); - Instances = new InstancesApi(AuthHandler.Configuration); - Permissions = new PermissionsApi(AuthHandler.Configuration); - Instances = new InstancesApi(AuthHandler.Configuration); - Invite = new InviteApi(AuthHandler.Configuration); - PlayerModeration = new PlayermoderationApi(AuthHandler.Configuration); - Notifications = new NotificationsApi(AuthHandler.Configuration); - } - - if (newState == AuthenticationState.LoggedOut) - { - System = null; - Users = null; - Avatars = null; - Worlds = null; - Friends = null; - Economy = null; - Favorites = null; - Groups = null; - Files = null; - Instances = null; - Permissions = null; - Instances = null; - Invite = null; - PlayerModeration = null; - Notifications = null; - } - }); - } -} From ec492e21b43272407406b31b8779536ffe96197b Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Mon, 13 Jan 2025 02:25:30 +0000 Subject: [PATCH 20/74] Remove WinForms dependency from file picking and presenting --- VRCOSC.App/NativeMethods.txt | 6 +- .../UI/Views/ChatBox/ChatBoxView.xaml.cs | 7 +- .../UI/Views/Modules/ModulesView.xaml.cs | 7 +- VRCOSC.App/Utils/WinForms.cs | 97 ++++++------------- 4 files changed, 47 insertions(+), 70 deletions(-) diff --git a/VRCOSC.App/NativeMethods.txt b/VRCOSC.App/NativeMethods.txt index 8c011ab5..9b093f43 100644 --- a/VRCOSC.App/NativeMethods.txt +++ b/VRCOSC.App/NativeMethods.txt @@ -1,7 +1,11 @@ ShowWindow GetForegroundWindow +GetActiveWindow EnumWindows GetWindowThreadProcessId ToUnicodeEx MapVirtualKey -GetKeyboardLayout \ No newline at end of file +GetKeyboardLayout +SHOpenFolderAndSelectItems +SHParseDisplayName +ILFree \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml.cs b/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml.cs index d6586d96..534da3c6 100644 --- a/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml.cs +++ b/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml.cs @@ -405,9 +405,12 @@ private void TextBox_OnPreviewKeyDown(object sender, KeyEventArgs e) Keyboard.ClearFocus(); } - private void ImportButton_OnClick(object sender, RoutedEventArgs e) + private async void ImportButton_OnClick(object sender, RoutedEventArgs e) { - WinForms.OpenFile("chatbox.json|*.json", filePath => Dispatcher.Invoke(() => ChatBoxManager.GetInstance().Deserialise(filePath))); + var filePath = await WinForms.PickFileAsync(".json"); + if (filePath is null) return; + + Dispatcher.Invoke(() => ChatBoxManager.GetInstance().Deserialise(filePath)); } private void ExportButton_OnClick(object sender, RoutedEventArgs e) diff --git a/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs b/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs index bd42af32..01fb6e57 100644 --- a/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs +++ b/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs @@ -31,12 +31,15 @@ private void ModulesView_OnLoaded(object sender, RoutedEventArgs e) prefabsWindowManager = new WindowManager(this); } - private void ImportButton_OnClick(object sender, RoutedEventArgs e) + private async void ImportButton_OnClick(object sender, RoutedEventArgs e) { var element = (FrameworkElement)sender; var module = (Module)element.Tag; - WinForms.OpenFile("module.json|*.json", filePath => Dispatcher.Invoke(() => module.ImportConfig(filePath))); + var filePath = await WinForms.PickFileAsync(".json"); + if (filePath is null) return; + + Dispatcher.Invoke(() => module.ImportConfig(filePath)); } private void ExportButton_OnClick(object sender, RoutedEventArgs e) diff --git a/VRCOSC.App/Utils/WinForms.cs b/VRCOSC.App/Utils/WinForms.cs index 2f5a64c6..e2d74dbe 100644 --- a/VRCOSC.App/Utils/WinForms.cs +++ b/VRCOSC.App/Utils/WinForms.cs @@ -4,91 +4,58 @@ using System; using System.IO; using System.Runtime.InteropServices; -using System.Threading; using System.Threading.Tasks; -using Microsoft.Win32; +using Windows.Storage.Pickers; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell.Common; +using WinRT.Interop; namespace VRCOSC.App.Utils; public static class WinForms { - [DllImport("shell32.dll", SetLastError = true)] - private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, uint dwFlags); - - [DllImport("shell32.dll", SetLastError = true)] - private static extern void SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr bindingContext, [Out] out IntPtr pidl, uint sfgaoIn, [Out] out uint psfgaoOut); - - [STAThread] - public static void OpenFile(string filter, Action filePathCallback) + public static async Task PickFileAsync(string filter) { - var t = new Thread(() => + var picker = new FileOpenPicker { - var dlg = new OpenFileDialog - { - Multiselect = false, - Filter = filter - }; + SuggestedStartLocation = PickerLocationId.Downloads, + FileTypeFilter = { filter } + }; - if (dlg.ShowDialog() == false) return; + InitializeWithWindow.Initialize(picker, PInvoke.GetActiveWindow()); - filePathCallback.Invoke(dlg.FileName); - }); - - t.SetApartmentState(ApartmentState.STA); - t.Start(); + return (await picker.PickSingleFileAsync())?.Path; } - public static void PresentFile(string filePath) + public static unsafe void PresentFile(string filePath) { - Task.Run(() => - { - var filePtr = IntPtr.Zero; - var folderPtr = IntPtr.Zero; - - try - { - filePath = filePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - - var folderPath = Path.GetDirectoryName(filePath); - - if (folderPath == null) - { - Logger.Log($"Failed to get directory for {filePath}", level: LogLevel.Debug); - return; - } + ITEMIDLIST* pidlFolder = null; + ITEMIDLIST* pidlFile = null; - SHParseDisplayName(folderPath, IntPtr.Zero, out folderPtr, 0, out _); + filePath = filePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - if (folderPtr == IntPtr.Zero) + fixed (char* folderPathArray = Path.GetDirectoryName(filePath)) + { + fixed (char* filePathArray = filePath) + { + try { - Logger.Log($"Cannot find folder for '{folderPath}'", level: LogLevel.Error); - return; - } - - SHParseDisplayName(filePath, IntPtr.Zero, out filePtr, 0, out _); + var hrFolder = PInvoke.SHParseDisplayName(new PCWSTR(folderPathArray), null, &pidlFolder, 0, null); + if (hrFolder != 0) throw new COMException("Failed to parse folder path", hrFolder); - IntPtr[] fileArray; + var hrFile = PInvoke.SHParseDisplayName(new PCWSTR(filePathArray), null, &pidlFile, 0, null); + if (hrFile != 0) throw new COMException("Failed to parse file path", hrFile); - if (filePtr != IntPtr.Zero) - { - fileArray = new[] { filePtr }; + var hrSelect = PInvoke.SHOpenFolderAndSelectItems(pidlFolder, 1, &pidlFile, 0); + if (hrSelect != 0) throw new COMException("Failed to open folder and select item", hrSelect); } - else + finally { - Logger.Log($"Cannot find file for '{filePath}'", level: LogLevel.Debug); - fileArray = new[] { folderPtr }; + if (pidlFolder != null) PInvoke.ILFree(pidlFolder); + if (pidlFile != null) PInvoke.ILFree(pidlFile); } - - SHOpenFolderAndSelectItems(folderPtr, (uint)fileArray.Length, fileArray, 0); - } - finally - { - if (folderPtr != IntPtr.Zero) - Marshal.FreeCoTaskMem(folderPtr); - - if (filePtr != IntPtr.Zero) - Marshal.FreeCoTaskMem(filePtr); } - }); + } } -} +} \ No newline at end of file From 9552c57fcafae931a1cefaad17490dceef281651 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Mon, 13 Jan 2025 18:29:04 +0000 Subject: [PATCH 21/74] WinForms removal and style improvements --- VRCOSC.App/NativeMethods.txt | 6 +- VRCOSC.App/UI/Core/Extensions.cs | 34 +++- VRCOSC.App/UI/Core/WindowManager.cs | 11 +- .../UI/Views/ChatBox/ChatBoxPreviewView.xaml | 2 +- VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml | 2 +- .../UI/Views/ChatBox/ChatBoxView.xaml.cs | 16 +- .../UI/Views/Information/InformationView.xaml | 4 +- .../UI/Views/Modules/ModulesView.xaml.cs | 4 +- VRCOSC.App/UI/Views/Router/RouterView.xaml.cs | 2 +- .../UI/Views/Run/Tabs/ChatBoxTabView.xaml.cs | 8 +- .../ChatBox/ChatBoxClipEditWindow.xaml.cs | 14 +- VRCOSC.App/UI/Windows/MainWindow.xaml | 4 +- VRCOSC.App/UI/Windows/MainWindow.xaml.cs | 96 ++++-------- VRCOSC.App/Updater/VelopackUpdater.cs | 6 +- VRCOSC.App/Utils/Platform.cs | 147 ++++++++++++++++++ VRCOSC.App/Utils/WPFUtils.cs | 120 -------------- VRCOSC.App/Utils/WinForms.cs | 61 -------- 17 files changed, 270 insertions(+), 267 deletions(-) create mode 100644 VRCOSC.App/Utils/Platform.cs delete mode 100644 VRCOSC.App/Utils/WPFUtils.cs delete mode 100644 VRCOSC.App/Utils/WinForms.cs diff --git a/VRCOSC.App/NativeMethods.txt b/VRCOSC.App/NativeMethods.txt index 9b093f43..5c59fb92 100644 --- a/VRCOSC.App/NativeMethods.txt +++ b/VRCOSC.App/NativeMethods.txt @@ -8,4 +8,8 @@ MapVirtualKey GetKeyboardLayout SHOpenFolderAndSelectItems SHParseDisplayName -ILFree \ No newline at end of file +CoTaskMemFree +GetDpiForSystem +MonitorFromWindow +GetMonitorInfoW +DwmSetWindowAttribute \ No newline at end of file diff --git a/VRCOSC.App/UI/Core/Extensions.cs b/VRCOSC.App/UI/Core/Extensions.cs index f1865ff3..cf189853 100644 --- a/VRCOSC.App/UI/Core/Extensions.cs +++ b/VRCOSC.App/UI/Core/Extensions.cs @@ -3,12 +3,44 @@ using System; using System.Windows; +using System.Windows.Media; using System.Windows.Media.Animation; namespace VRCOSC.App.UI.Core; public static class Extensions { + public static T? FindVisualParent(this DependencyObject element, string name) where T : DependencyObject + { + while (true) + { + var parent = VisualTreeHelper.GetParent(element); + + if (parent is null) return null; + if (parent is T parentAsType && ((FrameworkElement)parent).Name == name) return parentAsType; + + element = parent; + } + } + + public static T? FindVisualChild(this DependencyObject element, string name) where T : DependencyObject + { + for (var i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) + { + var child = VisualTreeHelper.GetChild(element, i); + + if (child is T childAsType && ((FrameworkElement)child).Name == name) + return childAsType; + + var childOfChild = FindVisualChild(child, name); + + if (childOfChild is not null) + return childOfChild; + } + + return null; + } + public static void FadeInFromZero(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) { element.Opacity = 0; @@ -61,4 +93,4 @@ public static void FadeOut(this FrameworkElement element, double durationMillise }; storyboard.Begin(element); } -} +} \ No newline at end of file diff --git a/VRCOSC.App/UI/Core/WindowManager.cs b/VRCOSC.App/UI/Core/WindowManager.cs index 521a231c..d832cf0e 100644 --- a/VRCOSC.App/UI/Core/WindowManager.cs +++ b/VRCOSC.App/UI/Core/WindowManager.cs @@ -29,6 +29,8 @@ public WindowManager(DependencyObject dp) private void parentWindowClosing(object? sender, CancelEventArgs e) { + if (e.Cancel) return; + foreach (var childWindow in childWindows.ToList().Cast()) { childWindow.Close(); @@ -40,6 +42,13 @@ private void childWindowClosed(object? sender, EventArgs e) childWindows.Remove((IManagedWindow)sender!); } + private void childWindowOnSourceInitialized(object? sender, EventArgs e) + { + var window = (Window)sender!; + window.ApplyDefaultStyling(); + window.SetPositionFrom(parentWindow); + } + public void TrySpawnChild(IManagedWindow childWindow) { if (childWindow is not Window) throw new InvalidOperationException($"An {nameof(IManagedWindow)} must extend {nameof(Window)}"); @@ -55,7 +64,7 @@ public void TrySpawnChild(IManagedWindow childWindow) window = (Window)childWindow; window.Closed += childWindowClosed; - window.SetPosition(parentWindow, ScreenChoice.SameAsParent, HorizontalPosition.Center, VerticalPosition.Center); + window.SourceInitialized += childWindowOnSourceInitialized; window.Show(); childWindows.Add(childWindow); diff --git a/VRCOSC.App/UI/Views/ChatBox/ChatBoxPreviewView.xaml b/VRCOSC.App/UI/Views/ChatBox/ChatBoxPreviewView.xaml index 4c266ce2..f017a304 100644 --- a/VRCOSC.App/UI/Views/ChatBox/ChatBoxPreviewView.xaml +++ b/VRCOSC.App/UI/Views/ChatBox/ChatBoxPreviewView.xaml @@ -16,4 +16,4 @@ - + \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml b/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml index 365fdb10..13075dfb 100644 --- a/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml +++ b/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml @@ -450,7 +450,7 @@ Right Click Menu <--> - diff --git a/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml.cs b/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml.cs index 534da3c6..85448257 100644 --- a/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml.cs +++ b/VRCOSC.App/UI/Views/ChatBox/ChatBoxView.xaml.cs @@ -272,7 +272,7 @@ private void Timeline_MouseMove(object sender, MouseEventArgs e) if (mouseXPercentageOffset == -1) { - mouseXPercentageOffset = mouseXPercentage - ((double)draggingClip.Start.Value / timelineLength); + mouseXPercentageOffset = mouseXPercentage - (double)draggingClip.Start.Value / timelineLength; } var clipLayer = draggingClip.Layer.Value; @@ -291,7 +291,7 @@ private void Timeline_MouseMove(object sender, MouseEventArgs e) End = { Value = newEnd } })); - if ((newStart >= lowerBound && newEnd <= upperBound) || (noneIntersect && newStart >= 0 && newEnd <= timelineLength)) + if (newStart >= lowerBound && newEnd <= upperBound || noneIntersect && newStart >= 0 && newEnd <= timelineLength) { draggingClip.Start.Value = newStart; draggingClip.End.Value = newEnd; @@ -349,7 +349,13 @@ private void EditClip_ButtonClick(object sender, RoutedEventArgs e) { clipEditWindow = new ChatBoxClipEditWindow(SelectedClip); clipEditWindow.Closed += (_, _) => clipEditWindowCache.Remove(clipEditWindow); - clipEditWindow.SetPosition(this, ScreenChoice.SameAsParent, HorizontalPosition.Center, VerticalPosition.Center); + + clipEditWindow.SourceInitialized += (_, _) => + { + clipEditWindow.ApplyDefaultStyling(); + clipEditWindow.SetPositionFrom(this); + }; + clipEditWindowCache.Add(clipEditWindow); clipEditWindow.ShowDialog(); } @@ -407,7 +413,7 @@ private void TextBox_OnPreviewKeyDown(object sender, KeyEventArgs e) private async void ImportButton_OnClick(object sender, RoutedEventArgs e) { - var filePath = await WinForms.PickFileAsync(".json"); + var filePath = await Platform.PickFileAsync(".json"); if (filePath is null) return; Dispatcher.Invoke(() => ChatBoxManager.GetInstance().Deserialise(filePath)); @@ -416,7 +422,7 @@ private async void ImportButton_OnClick(object sender, RoutedEventArgs e) private void ExportButton_OnClick(object sender, RoutedEventArgs e) { var filePath = AppManager.GetInstance().Storage.GetFullPath($"profiles/{ProfileManager.GetInstance().ActiveProfile.Value.ID}/chatbox.json"); - WinForms.PresentFile(filePath); + Platform.PresentFile(filePath); } private void ClearButton_OnClick(object sender, RoutedEventArgs e) diff --git a/VRCOSC.App/UI/Views/Information/InformationView.xaml b/VRCOSC.App/UI/Views/Information/InformationView.xaml index 4bb7c5dc..5fd566fe 100644 --- a/VRCOSC.App/UI/Views/Information/InformationView.xaml +++ b/VRCOSC.App/UI/Views/Information/InformationView.xaml @@ -10,7 +10,7 @@ - + @@ -18,7 +18,7 @@ - + diff --git a/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs b/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs index 01fb6e57..acbe3bea 100644 --- a/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs +++ b/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs @@ -36,7 +36,7 @@ private async void ImportButton_OnClick(object sender, RoutedEventArgs e) var element = (FrameworkElement)sender; var module = (Module)element.Tag; - var filePath = await WinForms.PickFileAsync(".json"); + var filePath = await Platform.PickFileAsync(".json"); if (filePath is null) return; Dispatcher.Invoke(() => module.ImportConfig(filePath)); @@ -48,7 +48,7 @@ private void ExportButton_OnClick(object sender, RoutedEventArgs e) var module = (Module)element.Tag; var filePath = AppManager.GetInstance().Storage.GetFullPath($"profiles/{ProfileManager.GetInstance().ActiveProfile.Value.ID}/modules/{module.FullID}.json"); - WinForms.PresentFile(filePath); + Platform.PresentFile(filePath); } private void ParametersButton_OnClick(object sender, RoutedEventArgs e) diff --git a/VRCOSC.App/UI/Views/Router/RouterView.xaml.cs b/VRCOSC.App/UI/Views/Router/RouterView.xaml.cs index 41f49303..da2d44c8 100644 --- a/VRCOSC.App/UI/Views/Router/RouterView.xaml.cs +++ b/VRCOSC.App/UI/Views/Router/RouterView.xaml.cs @@ -10,7 +10,7 @@ namespace VRCOSC.App.UI.Views.Router; public partial class RouterView { - private static readonly Uri router_docs_uri = new("https://vrcosc.com/docs/V2/router"); + private static readonly Uri router_docs_uri = new("https://vrcosc.com/docs/v2/router"); public RouterManager RouterManager { get; } diff --git a/VRCOSC.App/UI/Views/Run/Tabs/ChatBoxTabView.xaml.cs b/VRCOSC.App/UI/Views/Run/Tabs/ChatBoxTabView.xaml.cs index 86dff831..9bfda4a9 100644 --- a/VRCOSC.App/UI/Views/Run/Tabs/ChatBoxTabView.xaml.cs +++ b/VRCOSC.App/UI/Views/Run/Tabs/ChatBoxTabView.xaml.cs @@ -19,7 +19,13 @@ public ChatBoxTabView() private void PopoutChatBox_OnClick(object sender, RoutedEventArgs e) { var previewWindow = new ChatBoxPreviewWindow(); - previewWindow.SetPosition(this, ScreenChoice.SameAsParent, HorizontalPosition.Center, VerticalPosition.Center); + + previewWindow.SourceInitialized += (_, _) => + { + previewWindow.ApplyDefaultStyling(); + previewWindow.SetPositionFrom(this); + }; + previewWindow.Show(); } diff --git a/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml.cs b/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml.cs index b4ca646f..93d8e1be 100644 --- a/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml.cs +++ b/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml.cs @@ -14,11 +14,12 @@ using VRCOSC.App.Modules; using VRCOSC.App.SDK.Modules; using VRCOSC.App.Settings; +using VRCOSC.App.UI.Core; using VRCOSC.App.Utils; namespace VRCOSC.App.UI.Windows.ChatBox; -public partial class ChatBoxClipEditWindow +public partial class ChatBoxClipEditWindow : IManagedWindow { public Clip ReferenceClip { get; } @@ -113,7 +114,13 @@ private void VariableSettingButton_OnClick(object sender, RoutedEventArgs e) var variableInstance = (ClipVariable)element.Tag; var clipVariableWindow = new ClipVariableEditWindow(variableInstance); - clipVariableWindow.SetPosition(this, ScreenChoice.SameAsParent, HorizontalPosition.Center, VerticalPosition.Center); + + clipVariableWindow.SourceInitialized += (_, _) => + { + clipVariableWindow.ApplyDefaultStyling(); + clipVariableWindow.SetPositionFrom(this); + }; + clipVariableWindow.ShowDialog(); } @@ -243,6 +250,9 @@ private void VariableInstance_DragLeave(object sender, DragEventArgs e) e.Effects = DragDropEffects.None; } } + + // only allow 1 clip edit window at once + public object GetComparer() => ChatBoxManager.GetInstance(); } public class TextBoxParsingConverter : IValueConverter diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml b/VRCOSC.App/UI/Windows/MainWindow.xaml index eb1aca2c..16e702e5 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml @@ -9,8 +9,8 @@ mc:Ignorable="d" Title="VRCOSC" Width="1366" Height="768" MinWidth="960" MinHeight="540" Closing="MainWindow_OnClosing" - Closed="MainWindow_OnClosed" - Loaded="MainWindow_OnLoaded"> + Loaded="MainWindow_OnLoaded" + SourceInitialized="MainWindow_OnSourceInitialized"> - + \ No newline at end of file diff --git a/VRCOSC.App/Modules/ModuleManager.cs b/VRCOSC.App/Modules/ModuleManager.cs index 9ee4160d..8026f964 100644 --- a/VRCOSC.App/Modules/ModuleManager.cs +++ b/VRCOSC.App/Modules/ModuleManager.cs @@ -88,6 +88,7 @@ public void AvatarChange(AvatarConfig? avatarConfig) public IEnumerable GetModulesOfType() => modules.Where(module => module.GetType().IsAssignableTo(typeof(T))).Cast(); public IEnumerable GetRunningModulesOfType() => RunningModules.Where(module => module.GetType().IsAssignableTo(typeof(T))).Cast(); + public IEnumerable GetEnabledModulesOfType() => modules.Where(module => module.GetType().IsAssignableTo(typeof(T)) && module.Enabled.Value).Cast(); public Module GetModuleOfID(string moduleID) => modules.First(module => module.FullID == moduleID); diff --git a/VRCOSC.App/Packages/PackageManager.cs b/VRCOSC.App/Packages/PackageManager.cs index 00575b1a..9ba90011 100644 --- a/VRCOSC.App/Packages/PackageManager.cs +++ b/VRCOSC.App/Packages/PackageManager.cs @@ -141,19 +141,17 @@ private async Task downloadRelease(PackageSource source, PackageRelease release, { var tasks = release.Assets.Select(assetName => Task.Run(async () => { - { - var fileDownload = new FileDownload(); - await fileDownload.DownloadFileAsync(new Uri($"{source.URL}/releases/download/{release.Version}/{assetName}"), targetDirectory.GetFullPath(assetName, true)); + var fileDownload = new FileDownload(); + await fileDownload.DownloadFileAsync(new Uri($"{source.URL}/releases/download/{release.Version}/{assetName}"), targetDirectory.GetFullPath(assetName, true)); - if (assetName.EndsWith(".zip")) - { - var zipPath = targetDirectory.GetFullPath(assetName); - var extractPath = targetDirectory.GetFullPath(string.Empty); + if (assetName.EndsWith(".zip")) + { + var zipPath = targetDirectory.GetFullPath(assetName); + var extractPath = targetDirectory.GetFullPath(string.Empty); - ZipFile.ExtractToDirectory(zipPath, extractPath); + ZipFile.ExtractToDirectory(zipPath, extractPath); - targetDirectory.Delete(assetName); - } + targetDirectory.Delete(assetName); } })); diff --git a/VRCOSC.App/UI/Core/Extensions.cs b/VRCOSC.App/UI/Core/Extensions.cs index 2ecd9077..e2b7d347 100644 --- a/VRCOSC.App/UI/Core/Extensions.cs +++ b/VRCOSC.App/UI/Core/Extensions.cs @@ -5,6 +5,7 @@ using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; +using System.Windows.Threading; namespace VRCOSC.App.UI.Core; @@ -45,7 +46,7 @@ public static void FadeInFromZero(this FrameworkElement element, double duration { element.Opacity = 0; FadeIn(element, durationMilliseconds, onCompleted); - }); + }, DispatcherPriority.Render); public static void FadeIn(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) => Application.Current.Dispatcher.Invoke(() => { @@ -64,13 +65,13 @@ public static void FadeIn(this FrameworkElement element, double durationMillisec storyboard.Children.Add(fadeInAnimation); storyboard.Completed += (_, _) => onCompleted?.Invoke(); storyboard.Begin(element); - }); + }, DispatcherPriority.Render); public static void FadeOutFromOne(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) => Application.Current.Dispatcher.Invoke(() => { element.Opacity = 1; FadeOut(element, durationMilliseconds, onCompleted); - }); + }, DispatcherPriority.Render); public static void FadeOut(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) => Application.Current.Dispatcher.Invoke(() => { @@ -92,5 +93,5 @@ public static void FadeOut(this FrameworkElement element, double durationMillise onCompleted?.Invoke(); }; storyboard.Begin(element); - }); + }, DispatcherPriority.Render); } \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/Packages/PackagesView.xaml.cs b/VRCOSC.App/UI/Views/Packages/PackagesView.xaml.cs index c44ef0e0..c29d8c68 100644 --- a/VRCOSC.App/UI/Views/Packages/PackagesView.xaml.cs +++ b/VRCOSC.App/UI/Views/Packages/PackagesView.xaml.cs @@ -38,7 +38,7 @@ private async void RefreshButton_OnClick(object sender, RoutedEventArgs e) await AppManager.GetInstance().StopAsync(); } - _ = MainWindow.GetInstance().ShowLoadingOverlay("Refreshing All Packages", new DynamicAsyncProgressAction("Refreshing Packages", () => PackageManager.GetInstance().RefreshAllSources(true))); + _ = MainWindow.GetInstance().ShowLoadingOverlay(new CallbackProgressAction("Refreshing All Packages", () => PackageManager.GetInstance().RefreshAllSources(true))); } private void filterDataGrid(string filterText) @@ -97,21 +97,18 @@ private void InstallButton_OnClick(object sender, RoutedEventArgs e) var element = (FrameworkElement)sender; var packageSource = (PackageSource)element.Tag; - var action = PackageManager.GetInstance().InstallPackage(packageSource); - _ = MainWindow.GetInstance().ShowLoadingOverlay($"Installing {packageSource.DisplayName}", new DynamicAsyncProgressAction("Foo", () => action)); + _ = MainWindow.GetInstance().ShowLoadingOverlay(new CallbackProgressAction($"Installing {packageSource.DisplayName}", () => PackageManager.GetInstance().InstallPackage(packageSource))); } private void UninstallButton_OnClick(object sender, RoutedEventArgs e) { var element = (FrameworkElement)sender; - var package = (PackageSource)element.Tag; - - var result = MessageBox.Show($"Are you sure you want to uninstall {package.DisplayName}?", "Uninstall Warning", MessageBoxButton.YesNo); + var packageSource = (PackageSource)element.Tag; + var result = MessageBox.Show($"Are you sure you want to uninstall {packageSource.DisplayName}?", "Uninstall Warning", MessageBoxButton.YesNo); if (result != MessageBoxResult.Yes) return; - var action = PackageManager.GetInstance().UninstallPackage(package); - _ = MainWindow.GetInstance().ShowLoadingOverlay($"Uninstalling {package.DisplayName}", new DynamicAsyncProgressAction("Foo", () => action)); + _ = MainWindow.GetInstance().ShowLoadingOverlay(new CallbackProgressAction($"Uninstalling {packageSource.DisplayName}", () => PackageManager.GetInstance().UninstallPackage(packageSource))); } private void InstalledVersion_OnSelectionChanged(object sender, SelectionChangedEventArgs e) @@ -120,8 +117,7 @@ private void InstalledVersion_OnSelectionChanged(object sender, SelectionChanged var packageSource = (PackageSource)element.Tag; var packageRelease = packageSource.FilteredReleases[element.SelectedIndex]; - var action = PackageManager.GetInstance().InstallPackage(packageSource, packageRelease); - _ = MainWindow.GetInstance().ShowLoadingOverlay($"Installing {packageSource.DisplayName} - {packageRelease.Version}", new DynamicAsyncProgressAction("Foo", () => action)); + _ = MainWindow.GetInstance().ShowLoadingOverlay(new CallbackProgressAction($"Installing {packageSource.DisplayName} - {packageRelease.Version}", () => PackageManager.GetInstance().InstallPackage(packageSource, packageRelease))); } private void InstalledVersion_LostMouseCapture(object sender, MouseEventArgs e) diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml b/VRCOSC.App/UI/Windows/MainWindow.xaml index 0cece70a..022d61be 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml @@ -3,8 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:b="http://schemas.microsoft.com/xaml/behaviors" - xmlns:utils="clr-namespace:VRCOSC.App.Utils" xmlns:fa6="http://schemas.fontawesome.com/icons/fonts" mc:Ignorable="d" Title="VRCOSC" Width="1366" Height="768" MinWidth="960" MinHeight="540" @@ -78,7 +76,7 @@ - + @@ -134,45 +132,24 @@ - - - - - - - - - - - - - - - - - - - - - - + - + - + + - + + + + diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs index fc6bc2ad..1651694f 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs @@ -186,17 +186,12 @@ private void createBackupIfUpdated() private async void load() { - var doFirstTimeSetup = !SettingsManager.GetInstance().GetValue(VRCOSCMetadata.FirstTimeSetupComplete); - await PackageManager.GetInstance().Load(); - if (doFirstTimeSetup) + if (!SettingsManager.GetInstance().GetValue(VRCOSCMetadata.FirstTimeSetupComplete)) { await PackageManager.GetInstance().InstallPackage(PackageManager.GetInstance().OfficialModulesSource, reloadAll: false, refreshBeforeInstall: false); - } - if (doFirstTimeSetup) - { var ftsWindow = new FirstTimeInstallWindow(); ftsWindow.SourceInitialized += (_, _) => @@ -211,18 +206,13 @@ private async void load() } var appUpdated = false; - var installedVersion = SettingsManager.GetInstance().GetValue(VRCOSCMetadata.InstalledVersion); if (!string.IsNullOrEmpty(installedVersion)) { var cachedInstalledVersion = SemVersion.Parse(installedVersion, SemVersionStyles.Any); var newInstalledVersion = SemVersion.Parse(AppManager.Version, SemVersionStyles.Any); - - if (SemVersion.ComparePrecedence(cachedInstalledVersion, newInstalledVersion) != 0) - { - appUpdated = true; - } + appUpdated = SemVersion.ComparePrecedence(cachedInstalledVersion, newInstalledVersion) != 0; } if (appUpdated || string.IsNullOrEmpty(installedVersion)) @@ -235,7 +225,7 @@ private async void load() if (!appUpdated && !cacheOutdated) { loadComplete(); - WelcomeOverlay.FadeOutFromOne(1000); + fadeOutLoadingOverlay(1000, true); } if (velopackUpdater.IsInstalled()) @@ -273,7 +263,7 @@ private async void load() } loadComplete(); - WelcomeOverlay.FadeOutFromOne(500); + fadeOutLoadingOverlay(500, false); } } @@ -287,6 +277,12 @@ private void loadComplete() AppManager.GetInstance().InitialLoadComplete(); } + private void fadeOutLoadingOverlay(float duration, bool collapseSpinner) + { + if (collapseSpinner) LoadingSpinner.Visibility = Visibility.Collapsed; + LoadingOverlay.FadeOut(duration); + } + private void copyOpenVrFiles() { var runtimeOVRStorage = storage.GetStorageForDirectory("runtime/openvr"); @@ -354,9 +350,6 @@ private async void MainWindow_OnClosing(object? sender, CancelEventArgs e) } OVRDeviceManager.GetInstance().Serialise(); - RouterManager.GetInstance().Serialise(); - StartupManager.GetInstance().Serialise(); - SettingsManager.GetInstance().Serialise(); trayIcon?.Dispose(); } @@ -433,37 +426,19 @@ private void transitionTray(bool inTray) #endregion - public Task ShowLoadingOverlay(string title, ProgressAction progressAction) + public Task ShowLoadingOverlay(ProgressAction progressAction) => Dispatcher.Invoke(() => { - LoadingTitle.Text = title; + LoadingTitle.Text = progressAction.Title; + LoadingProgressBar.Visibility = progressAction.UseProgressBar ? Visibility.Visible : Visibility.Collapsed; + LoadingSpinner.Visibility = progressAction.UseProgressBar ? Visibility.Collapsed : Visibility.Visible; - progressAction.OnComplete += HideLoadingOverlay; - - _ = Task.Run(async () => - { - while (!progressAction.IsComplete) - { - Dispatcher.Invoke(() => - { - LoadingDescription.Text = progressAction.Title; - ProgressBar.Value = progressAction.GetProgress(); - }); - await Task.Delay(TimeSpan.FromSeconds(1d / 10d)); - } - - Dispatcher.Invoke(() => - { - LoadingDescription.Text = "Finished!"; - ProgressBar.Value = 1; - }); - }); + progressAction.OnProgressChanged += p => Dispatcher.Invoke(() => LoadingProgressBar.Value = p); + progressAction.OnComplete += () => LoadingOverlay.FadeOut(150, () => LoadingProgressBar.Value = 0); LoadingOverlay.FadeIn(150); return progressAction.Execute(); - } - - public void HideLoadingOverlay() => LoadingOverlay.FadeOut(150); + }); private void setContent(object userControl) { From dd25fdb0e31b1cdfc61dfa9a6e55375fbeb2bded Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 14 Jan 2025 21:48:56 +0000 Subject: [PATCH 35/74] Fix SpacedStackPanel not invalidating measure on Spacing change --- VRCOSC.App/UI/Core/SpacedListView.cs | 5 ++--- VRCOSC.App/UI/Core/SpacedStackPanel.cs | 9 +++++++-- VRCOSC.App/UI/Styles/SpacedListView.xaml | 6 +++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/VRCOSC.App/UI/Core/SpacedListView.cs b/VRCOSC.App/UI/Core/SpacedListView.cs index b926beec..58cf2421 100644 --- a/VRCOSC.App/UI/Core/SpacedListView.cs +++ b/VRCOSC.App/UI/Core/SpacedListView.cs @@ -15,12 +15,11 @@ static SpacedListView() DefaultStyleKeyProperty.OverrideMetadata(typeof(SpacedListView), new FrameworkPropertyMetadata(typeof(SpacedListView))); } - public static readonly DependencyProperty SpacingProperty = - DependencyProperty.Register(nameof(Spacing), typeof(double), typeof(SpacedListView), new PropertyMetadata(5.0)); + public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register(nameof(Spacing), typeof(double), typeof(SpacedListView), new PropertyMetadata(0d)); public double Spacing { get => (double)GetValue(SpacingProperty); set => SetValue(SpacingProperty, value); } -} +} \ No newline at end of file diff --git a/VRCOSC.App/UI/Core/SpacedStackPanel.cs b/VRCOSC.App/UI/Core/SpacedStackPanel.cs index 0123841b..6eab1270 100644 --- a/VRCOSC.App/UI/Core/SpacedStackPanel.cs +++ b/VRCOSC.App/UI/Core/SpacedStackPanel.cs @@ -13,7 +13,12 @@ namespace VRCOSC.App.UI.Core; public class SpacedStackPanel : StackPanel { public static readonly DependencyProperty SpacingProperty = - DependencyProperty.Register(nameof(Spacing), typeof(double), typeof(SpacedStackPanel), new PropertyMetadata(5.0)); + DependencyProperty.Register(nameof(Spacing), typeof(double), typeof(SpacedStackPanel), new PropertyMetadata(0d, SpacingChanged)); + + private static void SpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is SpacedStackPanel control) control.InvalidateMeasure(); + } public double Spacing { @@ -98,4 +103,4 @@ protected override Size ArrangeOverride(Size arrangeSize) return arrangeSize; } } -} +} \ No newline at end of file diff --git a/VRCOSC.App/UI/Styles/SpacedListView.xaml b/VRCOSC.App/UI/Styles/SpacedListView.xaml index 7f7b0652..4fabf2ef 100644 --- a/VRCOSC.App/UI/Styles/SpacedListView.xaml +++ b/VRCOSC.App/UI/Styles/SpacedListView.xaml @@ -8,7 +8,7 @@ @@ -17,7 +17,7 @@ + PanningMode="VerticalOnly" CanContentScroll="False" VerticalAlignment="Top"> @@ -28,4 +28,4 @@ - + \ No newline at end of file From cc47b1efc4c8e68ad01a4f0691ffb167d996f29b Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 14 Jan 2025 21:50:57 +0000 Subject: [PATCH 36/74] Fix package overlay description position --- VRCOSC.App/UI/Views/Packages/PackagesView.xaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/VRCOSC.App/UI/Views/Packages/PackagesView.xaml b/VRCOSC.App/UI/Views/Packages/PackagesView.xaml index 6ac15ce9..65e1ac12 100644 --- a/VRCOSC.App/UI/Views/Packages/PackagesView.xaml +++ b/VRCOSC.App/UI/Views/Packages/PackagesView.xaml @@ -160,7 +160,8 @@ + HorizontalAlignment="Center" + Spacing="5"> + FontSize="12" HorizontalAlignment="Center" /> From facb01c297b8e1d928e4ff8fc03e38d48ab574f6 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Tue, 14 Jan 2025 23:13:11 +0000 Subject: [PATCH 37/74] Optimise more things when loading --- .../Modules/Serialisation/ModuleSerialiser.cs | 8 ++++---- .../UI/Views/AppSettings/AppSettingsView.xaml.cs | 16 +++++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/VRCOSC.App/Modules/Serialisation/ModuleSerialiser.cs b/VRCOSC.App/Modules/Serialisation/ModuleSerialiser.cs index bcdcb8be..5b699e17 100644 --- a/VRCOSC.App/Modules/Serialisation/ModuleSerialiser.cs +++ b/VRCOSC.App/Modules/Serialisation/ModuleSerialiser.cs @@ -26,7 +26,7 @@ protected override bool ExecuteAfterDeserialisation(SerialisableModule data) Reference.Enabled.Value = data.Enabled; - data.Settings.ForEach(settingPair => + foreach (var settingPair in data.Settings) { var (settingKey, settingValue) = settingPair; @@ -42,9 +42,9 @@ protected override bool ExecuteAfterDeserialisation(SerialisableModule data) // setting doesn't exist shouldReserialise = true; } - }); + } - data.Parameters.ForEach(parameterPair => + foreach (var parameterPair in data.Parameters) { var (parameterKey, parameterValue) = parameterPair; @@ -58,7 +58,7 @@ protected override bool ExecuteAfterDeserialisation(SerialisableModule data) { // parameter doesn't exist } - }); + } return shouldReserialise; } diff --git a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs index 619875f0..c9b8aa72 100644 --- a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs +++ b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs @@ -175,7 +175,6 @@ public AppSettingsView() DataContext = this; - SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SelectedMicrophoneID).Subscribe(updateDeviceListAndSelection, true); SettingsManager.GetInstance().GetObservable(VRCOSCSetting.UseCustomEndpoints).Subscribe(value => UsingCustomEndpoints.Value = value ? Visibility.Visible : Visibility.Collapsed, true); SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechModelPath).Subscribe(_ => OnPropertyChanged(nameof(WhisperModelFilePath))); @@ -188,8 +187,10 @@ public void FocusAutomationTab() AutomationTabButton.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); } - private void updateDeviceListAndSelection(string? newDeviceId) => Dispatcher.Invoke(() => + private void updateDeviceListAndSelection() { + var selectedMicrophone = SettingsManager.GetInstance().GetValue(VRCOSCSetting.SelectedMicrophoneID); + audioInputDevices = [ new DeviceDisplay(string.Empty, "-- Use Default --") @@ -199,10 +200,14 @@ private void updateDeviceListAndSelection(string? newDeviceId) => Dispatcher.Inv MicrophoneComboBox.ItemsSource = audioInputDevices; - if (newDeviceId is null) return; + if (string.IsNullOrEmpty(selectedMicrophone)) + { + MicrophoneComboBox.SelectedIndex = 0; + return; + } - MicrophoneComboBox.SelectedItem = audioInputDevices.SingleOrDefault(device => device.ID == newDeviceId); - }); + MicrophoneComboBox.SelectedItem = audioInputDevices.SingleOrDefault(device => device.ID == selectedMicrophone); + } private void MicrophoneComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { @@ -256,6 +261,7 @@ private void PackagesTabButton_OnClick(object sender, RoutedEventArgs e) private void SpeechTabButton_OnClick(object sender, RoutedEventArgs e) { + updateDeviceListAndSelection(); setPage(6); } From 4e870d202182a812f05c82677e9050ad7bc6a2e6 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 15 Jan 2025 21:31:00 +0000 Subject: [PATCH 38/74] Increase OSCQuery request timeout --- VRCOSC.App/OSC/VRChat/VRChatOscClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.App/OSC/VRChat/VRChatOscClient.cs b/VRCOSC.App/OSC/VRChat/VRChatOscClient.cs index 6d7582f1..cc54ad2a 100644 --- a/VRCOSC.App/OSC/VRChat/VRChatOscClient.cs +++ b/VRCOSC.App/OSC/VRChat/VRChatOscClient.cs @@ -23,7 +23,7 @@ public class VRChatOscClient : OscClient public VRChatOscClient() { - client.Timeout = TimeSpan.FromMilliseconds(50); + client.Timeout = TimeSpan.FromMilliseconds(100); OnMessageSent += message => { OnParameterSent?.Invoke(new VRChatOscMessage(message)); }; From 6d960a78ed310259d184ed71c0c842ac96c21e3b Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 15 Jan 2025 21:31:30 +0000 Subject: [PATCH 39/74] Change UI for run view waiting overlay --- VRCOSC.App/UI/Views/Run/RunView.xaml | 37 +++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/VRCOSC.App/UI/Views/Run/RunView.xaml b/VRCOSC.App/UI/Views/Run/RunView.xaml index 9be76ecc..b4fff1fd 100644 --- a/VRCOSC.App/UI/Views/Run/RunView.xaml +++ b/VRCOSC.App/UI/Views/Run/RunView.xaml @@ -126,38 +126,41 @@ - + + + + - - + - - - - - + + Foreground="{StaticResource CForeground1}" Padding="75 0" TextAlignment="Center" /> - + - + Margin="0 -3 0 0" + HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" /> + Margin="0 -3 0 0" + HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" /> From dd097a9e38da2e62ee144f9f9ff2fdabb6892564 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 15 Jan 2025 21:32:32 +0000 Subject: [PATCH 40/74] Fix ValueListModuleSetting not converting values correctly --- .../SDK/Modules/Attributes/Settings/ListModuleSetting.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs b/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs index 0bdcaf70..dd0aa07a 100644 --- a/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs +++ b/VRCOSC.App/SDK/Modules/Attributes/Settings/ListModuleSetting.cs @@ -73,6 +73,13 @@ protected ValueListModuleSetting(string title, string description, Type viewType { } + public override TOut GetValue() + { + if (Attribute.Select(o => o.Value).ToList() is not TOut castList) throw new InvalidCastException($"{typeof(List).Name} cannot be cast to {typeof(TOut).Name}"); + + return castList; + } + protected override bool IsDefault() => base.IsDefault() && Attribute.All(o => o.IsDefault); protected override Observable CreateItem() => new(); From 679cc3e6823ed65da926b015c80749541b8e0f20 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 15 Jan 2025 21:32:46 +0000 Subject: [PATCH 41/74] Optimise SpacedStackPanel --- VRCOSC.App/UI/Core/SpacedStackPanel.cs | 74 ++++++++++++-------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/VRCOSC.App/UI/Core/SpacedStackPanel.cs b/VRCOSC.App/UI/Core/SpacedStackPanel.cs index 6eab1270..efead713 100644 --- a/VRCOSC.App/UI/Core/SpacedStackPanel.cs +++ b/VRCOSC.App/UI/Core/SpacedStackPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. -using System; -using System.Linq; using System.Windows; using System.Windows.Controls; @@ -30,41 +28,41 @@ protected override Size MeasureOverride(Size constraint) { var sizeAvailable = base.MeasureOverride(constraint); + var visibleChildCount = 0; + if (Orientation == Orientation.Horizontal) { - double totalChildWidth = 0; + var totalChildWidth = 0d; foreach (UIElement child in InternalChildren) { - if (child.Visibility == Visibility.Visible) - { - child.Measure(constraint); - totalChildWidth += child.DesiredSize.Width; - } - } + if (child.Visibility != Visibility.Visible) continue; - totalChildWidth += Spacing * (InternalChildren.Cast().Count(c => c.Visibility == Visibility.Visible) - 1); + visibleChildCount++; + child.Measure(constraint); + totalChildWidth += child.DesiredSize.Width; + } - totalChildWidth = Math.Max(totalChildWidth, 0); + var totalSpacing = visibleChildCount > 1 ? Spacing * (visibleChildCount - 1) : 0; - return new Size(totalChildWidth, sizeAvailable.Height); + return new Size(totalChildWidth + totalSpacing, sizeAvailable.Height); } - else // Vertical Orientation + else { - double totalChildHeight = 0; + var totalChildHeight = 0d; foreach (UIElement child in InternalChildren) { - if (child.Visibility == Visibility.Visible) - { - child.Measure(new Size(constraint.Width, double.PositiveInfinity)); - totalChildHeight += child.DesiredSize.Height; - } + if (child.Visibility != Visibility.Visible) continue; + + visibleChildCount++; + child.Measure(new Size(constraint.Width, double.PositiveInfinity)); + totalChildHeight += child.DesiredSize.Height; } - totalChildHeight += Spacing * (InternalChildren.Cast().Count(c => c.Visibility == Visibility.Visible) - 1); + var totalSpacing = visibleChildCount > 1 ? Spacing * (visibleChildCount - 1) : 0; - return new Size(Math.Max(sizeAvailable.Width, 0), Math.Max(totalChildHeight, 0)); + return new Size(sizeAvailable.Width, totalChildHeight + totalSpacing); } } @@ -72,35 +70,31 @@ protected override Size ArrangeOverride(Size arrangeSize) { if (Orientation == Orientation.Horizontal) { - double x = 0; + var x = 0d; foreach (UIElement child in InternalChildren) { - if (child.Visibility == Visibility.Visible) - { - var childRect = new Rect(x, 0, child.DesiredSize.Width, arrangeSize.Height); - child.Arrange(childRect); - x += child.DesiredSize.Width + Spacing; - } - } + if (child.Visibility != Visibility.Visible) continue; - return arrangeSize; + var childRect = new Rect(x, 0, child.DesiredSize.Width, arrangeSize.Height); + child.Arrange(childRect); + x += child.DesiredSize.Width + Spacing; + } } - else // Vertical Orientation + else { - double y = 0; + var y = 0d; foreach (UIElement child in InternalChildren) { - if (child.Visibility == Visibility.Visible) - { - var childRect = new Rect(0, y, arrangeSize.Width, child.DesiredSize.Height); - child.Arrange(childRect); - y += child.DesiredSize.Height + Spacing; - } - } + if (child.Visibility != Visibility.Visible) continue; - return arrangeSize; + var childRect = new Rect(0, y, arrangeSize.Width, child.DesiredSize.Height); + child.Arrange(childRect); + y += child.DesiredSize.Height + Spacing; + } } + + return arrangeSize; } } \ No newline at end of file From 8b3e5620bd628630d20078f0d7d2a3810d24abb9 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 15 Jan 2025 21:34:15 +0000 Subject: [PATCH 42/74] More loading speed optimisations --- VRCOSC.App/AppManager.cs | 4 +- VRCOSC.App/Modules/ModuleManager.cs | 3 +- VRCOSC.App/Modules/ModulePackage.cs | 2 +- VRCOSC.App/OSC/ConnectionManager.cs | 6 +- VRCOSC.App/Packages/PackageManager.cs | 6 +- VRCOSC.App/Packages/PackageSource.cs | 12 +- .../Serialisation/PackageManagerSerialiser.cs | 3 +- VRCOSC.App/Profiles/ProfileManager.cs | 11 +- VRCOSC.App/Router/RouterManager.cs | 9 +- .../SDK/Providers/PiShock/PiShockProvider.cs | 4 +- VRCOSC.App/Startup/StartupManager.cs | 9 +- VRCOSC.App/UI/Core/Extensions.cs | 17 +- VRCOSC.App/UI/Views/Run/RunView.xaml | 10 +- VRCOSC.App/UI/Windows/MainWindow.xaml | 8 +- VRCOSC.App/UI/Windows/MainWindow.xaml.cs | 320 +++++++++--------- VRCOSC.App/Updater/VelopackUpdater.cs | 27 +- 16 files changed, 222 insertions(+), 229 deletions(-) diff --git a/VRCOSC.App/AppManager.cs b/VRCOSC.App/AppManager.cs index b2f0add7..4211a6cb 100644 --- a/VRCOSC.App/AppManager.cs +++ b/VRCOSC.App/AppManager.cs @@ -140,12 +140,12 @@ public void Initialise() }); OVRHelper.OnError += m => Logger.Log($"[OpenVR] {m}"); - - OVRDeviceManager.GetInstance().Deserialise(); } public void InitialLoadComplete() { + OVRDeviceManager.GetInstance().Deserialise(); + VRChatOscClient.Init(ConnectionManager); ConnectionManager.Init(); diff --git a/VRCOSC.App/Modules/ModuleManager.cs b/VRCOSC.App/Modules/ModuleManager.cs index 8026f964..e8336354 100644 --- a/VRCOSC.App/Modules/ModuleManager.cs +++ b/VRCOSC.App/Modules/ModuleManager.cs @@ -337,7 +337,8 @@ private List retrieveModuleInstances(string packageId, AssemblyLoadConte foreach (var assembly in assemblyLoadContext.Assemblies) { - var moduleTypes = assembly.ExportedTypes.Where(type => type.IsSubclassOf(typeof(Module)) && !type.IsAbstract); + // change to use metadata methods? + var moduleTypes = assembly.GetExportedTypes().Where(type => type.IsSubclassOf(typeof(Module)) && !type.IsAbstract); foreach (var moduleType in moduleTypes) { diff --git a/VRCOSC.App/Modules/ModulePackage.cs b/VRCOSC.App/Modules/ModulePackage.cs index 0257ced4..b7655e03 100644 --- a/VRCOSC.App/Modules/ModulePackage.cs +++ b/VRCOSC.App/Modules/ModulePackage.cs @@ -19,4 +19,4 @@ public ModulePackage(Assembly assembly, bool remote) Assembly = assembly; Remote = remote; } -} +} \ No newline at end of file diff --git a/VRCOSC.App/OSC/ConnectionManager.cs b/VRCOSC.App/OSC/ConnectionManager.cs index 6596e1bc..4d77ffe7 100644 --- a/VRCOSC.App/OSC/ConnectionManager.cs +++ b/VRCOSC.App/OSC/ConnectionManager.cs @@ -36,7 +36,7 @@ public class ConnectionManager private Repeater? refreshTask; - public void Init() + public void Init() => Task.Run(() => { refreshTask = new Repeater($"{nameof(ConnectionManager)}-{nameof(refreshServices)}", refreshServices); refreshTask.Start(TimeSpan.FromMilliseconds(refresh_interval)); @@ -64,7 +64,7 @@ public void Init() Logger.Log($"Receiving OSC on {VRCOSCReceivePort}"); Logger.Log($"Hosting OSCQuery on {VRCOSCQueryPort}"); - } + }).ConfigureAwait(false); public void Reset() { @@ -192,6 +192,8 @@ private void handleMatchedService(SRVRecord srvRecord) var instanceName = domainName[0]; var serviceName = string.Join(".", domainName.Skip(1)); + // TODO: If there's multiple instances of the VRChat client on the network, prioritise the one with the same LAN IP as the PC that we're running on + if (instanceName.Contains(vrchat_client_name) && serviceName.Contains(osc_service_name)) { if (port == VRChatReceivePort) return; diff --git a/VRCOSC.App/Packages/PackageManager.cs b/VRCOSC.App/Packages/PackageManager.cs index 9ba90011..9c061011 100644 --- a/VRCOSC.App/Packages/PackageManager.cs +++ b/VRCOSC.App/Packages/PackageManager.cs @@ -47,8 +47,8 @@ public PackageManager() var baseStorage = AppManager.GetInstance().Storage; storage = baseStorage.GetStorageForDirectory("packages/remote"); - builtinSources.Add(OfficialModulesSource = new PackageSource(this, "VolcanicArts", "VRCOSC-Modules", PackageType.Official)); - builtinSources.Add(new PackageSource(this, "DJDavid98", "VRCOSC-BluetoothHeartrate", PackageType.Curated)); + builtinSources.Add(OfficialModulesSource = new PackageSource("VolcanicArts", "VRCOSC-Modules", PackageType.Official)); + builtinSources.Add(new PackageSource("DJDavid98", "VRCOSC-BluetoothHeartrate", PackageType.Curated)); serialisationManager = new SerialisationManager(); serialisationManager.RegisterSerialiser(1, new PackageManagerSerialiser(baseStorage, this)); @@ -188,7 +188,7 @@ private async Task loadCommunityPackages() foreach (var repo in repos.Items.Where(repo => repo.Name != "VRCOSC")) { - var packageSource = new PackageSource(this, repo.Owner.HtmlUrl.Split('/').Last(), repo.Name); + var packageSource = new PackageSource(repo.Owner.HtmlUrl.Split('/').Last(), repo.Name, PackageType.Community); if (builtinSources.Any(comparedSource => comparedSource.InternalReference == packageSource.InternalReference)) continue; Sources.Add(packageSource); diff --git a/VRCOSC.App/Packages/PackageSource.cs b/VRCOSC.App/Packages/PackageSource.cs index 5fdfbf98..685fb93d 100644 --- a/VRCOSC.App/Packages/PackageSource.cs +++ b/VRCOSC.App/Packages/PackageSource.cs @@ -22,14 +22,13 @@ public class PackageSource { private readonly HttpClient httpClient = new(); - private readonly PackageManager packageManager; public string RepoOwner { get; } public string RepoName { get; } public PackageType PackageType { get; } public PackageRepository? Repository { get; private set; } - public string InstalledVersion => packageManager.GetInstalledVersion(this); + public string InstalledVersion => PackageManager.GetInstance().GetInstalledVersion(this); public string InternalReference => $"{RepoOwner}#{RepoName}"; public string URL => $"https://github.com/{RepoOwner}/{RepoName}"; @@ -45,7 +44,7 @@ private List filterReleases(bool includeInstalledRelease, bool i || !packageRelease.IsPreRelease || (packageRelease.Version == InstalledVersion && includeInstalledRelease)).ToList(); - public bool IsInstalled() => packageManager.IsInstalled(this); + public bool IsInstalled() => PackageManager.GetInstance().IsInstalled(this); public bool IsUpdateAvailable() { @@ -53,7 +52,7 @@ public bool IsUpdateAvailable() var installedVersion = SemVersion.Parse(InstalledVersion, SemVersionStyles.Any); var latestVersion = SemVersion.Parse(LatestVersion, SemVersionStyles.Any); - return installedVersion.ComparePrecedenceTo(latestVersion) < 0; + return SemVersion.ComparePrecedence(latestVersion, installedVersion) == 1; } public PackageRelease? GetLatestNonPreRelease() @@ -63,15 +62,14 @@ public bool IsUpdateAvailable() var latestNonPreRelease = filterReleases(true, false).First(); var installedVersion = SemVersion.Parse(InstalledVersion, SemVersionStyles.Any); var latestNonPreReleaseVersion = SemVersion.Parse(latestNonPreRelease.Version, SemVersionStyles.Any); - return installedVersion.ComparePrecedenceTo(latestNonPreReleaseVersion) == -1 ? latestNonPreRelease : null; + return SemVersion.ComparePrecedence(latestNonPreReleaseVersion, installedVersion) == 1 ? latestNonPreRelease : null; } public bool IsUnavailable() => State is PackageSourceState.MissingRepo or PackageSourceState.NoReleases or PackageSourceState.InvalidPackageFile or PackageSourceState.Unknown; public bool IsAvailable() => State is PackageSourceState.Valid; - public PackageSource(PackageManager packageManager, string repoOwner, string repoName, PackageType packageType = PackageType.Community) + public PackageSource(string repoOwner, string repoName, PackageType packageType) { - this.packageManager = packageManager; RepoOwner = repoOwner; RepoName = repoName; PackageType = packageType; diff --git a/VRCOSC.App/Packages/Serialisation/PackageManagerSerialiser.cs b/VRCOSC.App/Packages/Serialisation/PackageManagerSerialiser.cs index 36946601..f41977fe 100644 --- a/VRCOSC.App/Packages/Serialisation/PackageManagerSerialiser.cs +++ b/VRCOSC.App/Packages/Serialisation/PackageManagerSerialiser.cs @@ -29,7 +29,8 @@ protected override bool ExecuteAfterDeserialisation(SerialisablePackageManager d if (packageSource is null) { - var newPackageSource = new PackageSource(Reference, serialisablePackageSource.Owner, serialisablePackageSource.Name); + // if the source hasn't already been added, we know this is a community package + var newPackageSource = new PackageSource(serialisablePackageSource.Owner, serialisablePackageSource.Name, PackageType.Community); newPackageSource.InjectCachedData(serialisablePackageSource.Repository!); Reference.Sources.Add(newPackageSource); } diff --git a/VRCOSC.App/Profiles/ProfileManager.cs b/VRCOSC.App/Profiles/ProfileManager.cs index 6cd00196..ba2aecfe 100644 --- a/VRCOSC.App/Profiles/ProfileManager.cs +++ b/VRCOSC.App/Profiles/ProfileManager.cs @@ -38,9 +38,11 @@ public bool EnableAutomaticSwitching set => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.AutomaticProfileSwitching).Value = value; } - private readonly SerialisationManager serialisationManager; + private SerialisationManager serialisationManager = null!; - public ProfileManager() + public void Serialise() => serialisationManager.Serialise(); + + public void Load() { serialisationManager = new SerialisationManager(); serialisationManager.RegisterSerialiser(1, new ProfileManagerSerialiser(storage, this)); @@ -53,12 +55,7 @@ public ProfileManager() }); DefaultProfile.Subscribe(newProfile => Logger.Log($"Default profile changed to {newProfile.ID}")); - } - public void Serialise() => serialisationManager.Serialise(); - - public void Load() - { serialisationManager.Deserialise(false); checkForDefault(); diff --git a/VRCOSC.App/Router/RouterManager.cs b/VRCOSC.App/Router/RouterManager.cs index 432d5df7..8402a681 100644 --- a/VRCOSC.App/Router/RouterManager.cs +++ b/VRCOSC.App/Router/RouterManager.cs @@ -20,22 +20,17 @@ public class RouterManager public static RouterManager GetInstance() => instance ??= new RouterManager(); public ObservableCollection Routes { get; } = new(); - private readonly SerialisationManager serialisationManager; + private SerialisationManager serialisationManager = null!; private readonly List<(RouterInstance, OscSender)> senders = new(); private bool started; - private RouterManager() + public void Load() { serialisationManager = new SerialisationManager(); serialisationManager.RegisterSerialiser(1, new RouterManagerSerialiser(AppManager.GetInstance().Storage, this)); - } - - public void Serialise() => serialisationManager.Serialise(); - public void Load() - { started = false; Routes.Clear(); diff --git a/VRCOSC.App/SDK/Providers/PiShock/PiShockProvider.cs b/VRCOSC.App/SDK/Providers/PiShock/PiShockProvider.cs index f0a62fa1..e7a75791 100644 --- a/VRCOSC.App/SDK/Providers/PiShock/PiShockProvider.cs +++ b/VRCOSC.App/SDK/Providers/PiShock/PiShockProvider.cs @@ -46,7 +46,7 @@ public async Task Execute(string username, string apiKey, strin } catch (Exception e) { - ExceptionHandler.Handle(e, $"{nameof(PiShockProvider)} has experienced an exception"); + Logger.Error(e, $"{nameof(PiShockProvider)} has experienced an exception"); return new PiShockResponse(false, e.Message); } } @@ -68,7 +68,7 @@ public async Task Execute(string username, string apiKey, strin } catch (Exception e) { - ExceptionHandler.Handle(e, $"{nameof(PiShockProvider)} has experienced an exception"); + Logger.Error(e, $"{nameof(PiShockProvider)} has experienced an exception"); return null; } } diff --git a/VRCOSC.App/Startup/StartupManager.cs b/VRCOSC.App/Startup/StartupManager.cs index 889ab509..a783d2a4 100644 --- a/VRCOSC.App/Startup/StartupManager.cs +++ b/VRCOSC.App/Startup/StartupManager.cs @@ -18,18 +18,13 @@ public class StartupManager public ObservableCollection Instances { get; } = []; - private readonly SerialisationManager serialisationManager; + private SerialisationManager serialisationManager = null!; - private StartupManager() + public void Load() { serialisationManager = new SerialisationManager(); serialisationManager.RegisterSerialiser(1, new StartupManagerSerialiser(AppManager.GetInstance().Storage, this)); - } - - public void Serialise() => serialisationManager.Serialise(); - public void Load() - { serialisationManager.Deserialise(); Instances.OnCollectionChanged((newItems, _) => diff --git a/VRCOSC.App/UI/Core/Extensions.cs b/VRCOSC.App/UI/Core/Extensions.cs index e2b7d347..cf189853 100644 --- a/VRCOSC.App/UI/Core/Extensions.cs +++ b/VRCOSC.App/UI/Core/Extensions.cs @@ -5,7 +5,6 @@ using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; -using System.Windows.Threading; namespace VRCOSC.App.UI.Core; @@ -42,13 +41,13 @@ public static class Extensions return null; } - public static void FadeInFromZero(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) => Application.Current.Dispatcher.Invoke(() => + public static void FadeInFromZero(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) { element.Opacity = 0; FadeIn(element, durationMilliseconds, onCompleted); - }, DispatcherPriority.Render); + } - public static void FadeIn(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) => Application.Current.Dispatcher.Invoke(() => + public static void FadeIn(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) { element.Visibility = Visibility.Visible; @@ -65,15 +64,15 @@ public static void FadeIn(this FrameworkElement element, double durationMillisec storyboard.Children.Add(fadeInAnimation); storyboard.Completed += (_, _) => onCompleted?.Invoke(); storyboard.Begin(element); - }, DispatcherPriority.Render); + } - public static void FadeOutFromOne(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) => Application.Current.Dispatcher.Invoke(() => + public static void FadeOutFromOne(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) { element.Opacity = 1; FadeOut(element, durationMilliseconds, onCompleted); - }, DispatcherPriority.Render); + } - public static void FadeOut(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) => Application.Current.Dispatcher.Invoke(() => + public static void FadeOut(this FrameworkElement element, double durationMilliseconds, Action? onCompleted = null) { var fadeOutAnimation = new DoubleAnimation { @@ -93,5 +92,5 @@ public static void FadeOut(this FrameworkElement element, double durationMillise onCompleted?.Invoke(); }; storyboard.Begin(element); - }, DispatcherPriority.Render); + } } \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/Run/RunView.xaml b/VRCOSC.App/UI/Views/Run/RunView.xaml index b4fff1fd..c7384e6b 100644 --- a/VRCOSC.App/UI/Views/Run/RunView.xaml +++ b/VRCOSC.App/UI/Views/Run/RunView.xaml @@ -43,8 +43,8 @@ - - + + @@ -103,7 +103,7 @@ + Icon="Solid_Play" IsEnabled="True"/> @@ -111,10 +111,10 @@ + Icon="Solid_Stop" IsEnabled="False" /> + Icon="Solid_ArrowRotateRight" IsEnabled="False" /> diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml b/VRCOSC.App/UI/Windows/MainWindow.xaml index 022d61be..372f43ff 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml @@ -5,10 +5,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:fa6="http://schemas.fontawesome.com/icons/fonts" mc:Ignorable="d" - Title="VRCOSC" Width="1366" Height="768" MinWidth="960" MinHeight="540" - Closing="MainWindow_OnClosing" - Loaded="MainWindow_OnLoaded" - SourceInitialized="MainWindow_OnSourceInitialized"> + Width="1366" Height="768" MinWidth="960" MinHeight="540"> - - - - - - - Center <--> - - - - - - - Clip element list <--> - - - - - - - - - - - - - - - - - - - - - Options <--> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Options <--> - - - - - - - - - - - - - - - - - - - - - - - - - - - Event-specific Options <--> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Footer <--> - - - - - - - - - - - - - - - Variables View <--> - - - - - - - - - - + + + + + + + Elements <--> + + + + + + + + + + + + + + + + + + Options <--> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListViewItem}}, Converter={StaticResource IndexConverter}}" + /> + + + + + + + + + + + + + + + + + + + + + Options <--> + + + + + + + + + + + + + + + + + + + + + + + + + + + Event-specific Options <--> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Variables <--> + + + + + + + + + + + + + + + + + + + + + + + Footer <--> + + + + + + + + + + + + \ No newline at end of file diff --git a/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml.cs b/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml.cs index 2654f7ed..e344b8f1 100644 --- a/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml.cs +++ b/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml.cs @@ -44,6 +44,7 @@ public ChatBoxClipEditWindow(Clip referenceClip) SourceInitialized += OnSourceInitialized; Loaded += OnLoaded; + Closed += OnClosed; } private void OnSourceInitialized(object? sender, EventArgs e) @@ -262,29 +263,6 @@ private void VariableInstance_DragLeave(object sender, DragEventArgs e) public object GetComparer() => ChatBoxManager.GetInstance(); } -public class TextBoxParsingConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string str) - { - return str.Replace("\\n", Environment.NewLine); - } - - return value; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string str) - { - return str.Replace(Environment.NewLine, "\\n"); - } - - return value; - } -} - public class IsModuleSelectedConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) diff --git a/VRCOSC.App/UI/Windows/Modules/ModuleSettingsWindow.xaml b/VRCOSC.App/UI/Windows/Modules/ModuleSettingsWindow.xaml index 78699bf8..aef7ff37 100644 --- a/VRCOSC.App/UI/Windows/Modules/ModuleSettingsWindow.xaml +++ b/VRCOSC.App/UI/Windows/Modules/ModuleSettingsWindow.xaml @@ -8,7 +8,7 @@ Title="ModuleSettingsWindow" MinWidth="600" MinHeight="600" Width="600" Height="768" Closing="ModuleSettingsWindow_OnClosing"> - + From 4a6442570e29363276844ef9728bb66a67ee1ea9 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 5 Feb 2025 21:55:01 +0000 Subject: [PATCH 64/74] Fix connection mode warning box styling --- VRCOSC.App/AppManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.App/AppManager.cs b/VRCOSC.App/AppManager.cs index d9255e6d..8ded8f63 100644 --- a/VRCOSC.App/AppManager.cs +++ b/VRCOSC.App/AppManager.cs @@ -363,7 +363,7 @@ public async void RequestStart() { if (!IsAdministrator) { - MessageBox.Show($"An OSC connection mode of {ConnectionMode.LAN} requires VRCOSC to be ran as administrator. Please restart the app as administrator"); + MessageBox.Show($"An OSC connection mode of {ConnectionMode.LAN} requires VRCOSC to be ran as administrator. Please restart the app as administrator", "Permission Warning", MessageBoxButton.OK, MessageBoxImage.Warning); return; } From 0fb3a62663d00aef30c6ae49d418f50e3617eb36 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 5 Feb 2025 21:55:31 +0000 Subject: [PATCH 65/74] Allow registered parameters to properly parse the regex matches --- VRCOSC.App/SDK/Modules/Module.cs | 11 +++++--- .../SDK/Parameters/ReceivedParameter.cs | 26 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/VRCOSC.App/SDK/Modules/Module.cs b/VRCOSC.App/SDK/Modules/Module.cs index 9606b4d3..b99becd7 100644 --- a/VRCOSC.App/SDK/Modules/Module.cs +++ b/VRCOSC.App/SDK/Modules/Module.cs @@ -195,7 +195,7 @@ private static Regex parameterToRegex(string parameterName) { var pattern = "^"; // start of string pattern += @"(?:VF\d+_)?"; // VRCFury prefix - pattern += $"({Regex.Escape(parameterName).Replace(@"\*", @"(?:\S*)")})"; + pattern += $"(?:{Regex.Escape(parameterName).Replace(@"\*", @"(\S*?)")})"; pattern += "$"; // end of string return new Regex(pattern); @@ -958,17 +958,20 @@ internal void OnParameterReceived(VRChatOscMessage message) Enum? lookup = null; ModuleParameter? parameterData = null; + Match? match = null; foreach (var (parameterLookup, moduleParameter) in readParameters) { - if (!parameterNameRegex[parameterLookup].IsMatch(receivedParameter.Name)) continue; + var localMatch = parameterNameRegex[parameterLookup].Match(receivedParameter.Name); + if (!localMatch.Success) continue; lookup = parameterLookup; parameterData = moduleParameter; + match = localMatch; break; } - if (lookup is null || parameterData is null) return; + if (lookup is null || parameterData is null || match is null) return; if (receivedParameter.Type != parameterData.ExpectedType) { @@ -976,7 +979,7 @@ internal void OnParameterReceived(VRChatOscMessage message) return; } - var registeredParameter = new RegisteredParameter(receivedParameter, lookup, parameterData); + var registeredParameter = new RegisteredParameter(receivedParameter, lookup, parameterData, match); List waitingParameters; diff --git a/VRCOSC.App/SDK/Parameters/ReceivedParameter.cs b/VRCOSC.App/SDK/Parameters/ReceivedParameter.cs index 4a8aaaa9..82c4dc75 100644 --- a/VRCOSC.App/SDK/Parameters/ReceivedParameter.cs +++ b/VRCOSC.App/SDK/Parameters/ReceivedParameter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using VRCOSC.App.SDK.Modules; using VRCOSC.App.Utils; @@ -80,41 +81,40 @@ public sealed class RegisteredParameter : ReceivedParameter public Enum Lookup { get; } private readonly ModuleParameter moduleParameter; + private readonly Match match; + private readonly List wildcards = []; - internal RegisteredParameter(ReceivedParameter other, Enum lookup, ModuleParameter moduleParameter) + internal RegisteredParameter(ReceivedParameter other, Enum lookup, ModuleParameter moduleParameter, Match match) : base(other) { Lookup = lookup; this.moduleParameter = moduleParameter; + this.match = match; decodeWildcards(); } private void decodeWildcards() { - var referenceSections = Name.Split("/"); - var originalSections = moduleParameter.Name.Value.Split("/"); - - for (int i = 0; i < originalSections.Length; i++) + for (var index = 1; index < match.Groups.Count; index++) { - if (originalSections[i] != "*") continue; - - var referenceValue = referenceSections[i]; + var group = match.Groups[index]; + var value = group.Captures[0].Value; - if (int.TryParse(referenceValue, out var referenceValueInt)) + if (int.TryParse(value, out var valueInt)) { - wildcards.Add(new Wildcard(referenceValueInt)); + wildcards.Add(new Wildcard(valueInt)); continue; } - if (float.TryParse(referenceValue, out var referenceValueFloat)) + if (float.TryParse(value, out var valueFloat)) { - wildcards.Add(new Wildcard(referenceValueFloat)); + wildcards.Add(new Wildcard(valueFloat)); continue; } - wildcards.Add(new Wildcard(referenceValue)); + wildcards.Add(new Wildcard(value)); } } From 0c309cf48852776feee29e99699dd9b758e5d044 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Thu, 6 Feb 2025 18:45:55 +0000 Subject: [PATCH 66/74] Add useful WPF extensions --- VRCOSC.App/UI/Core/Converters.cs | 17 +++++++++ VRCOSC.App/UI/Core/Extensions.cs | 29 +++++++++++++++ VRCOSC.App/UI/Core/VRCOSCImage.xaml | 8 +++++ VRCOSC.App/UI/Core/VRCOSCImage.xaml.cs | 49 ++++++++++++++++++++++++++ VRCOSC.App/Utils/ImageLoader.cs | 3 +- VRCOSC.App/VRCOSC.App.csproj | 1 + 6 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 VRCOSC.App/UI/Core/VRCOSCImage.xaml create mode 100644 VRCOSC.App/UI/Core/VRCOSCImage.xaml.cs diff --git a/VRCOSC.App/UI/Core/Converters.cs b/VRCOSC.App/UI/Core/Converters.cs index 2c070151..7fd98359 100644 --- a/VRCOSC.App/UI/Core/Converters.cs +++ b/VRCOSC.App/UI/Core/Converters.cs @@ -120,4 +120,21 @@ public class TextBoxParsingConverter : IValueConverter public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => value is string str ? str.Replace(Environment.NewLine, "\\n") : value; +} + +public class BorderClipConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values is not [double width, double height, CornerRadius radius]) return DependencyProperty.UnsetValue; + + if (width < double.Epsilon || height < double.Epsilon) return Geometry.Empty; + + var clip = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft); + clip.Freeze(); + + return clip; + } + + public object[]? ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => null; } \ No newline at end of file diff --git a/VRCOSC.App/UI/Core/Extensions.cs b/VRCOSC.App/UI/Core/Extensions.cs index 9c35dcc9..995f17cf 100644 --- a/VRCOSC.App/UI/Core/Extensions.cs +++ b/VRCOSC.App/UI/Core/Extensions.cs @@ -143,4 +143,33 @@ public static void FadeOut(this FrameworkElement element, double durationMillise }; storyboard.Begin(element); } + + public static void AnimateHeight(this FrameworkElement element, double targetHeight, double durationMilliseconds, IEasingFunction easingFunction, Action? onCompleted = null) + { + var currentHeight = element.Height; + + element.Height = currentHeight; + element.UpdateLayout(); + + var heightAnimation = new DoubleAnimation + { + From = currentHeight, + To = targetHeight, + Duration = TimeSpan.FromMilliseconds(durationMilliseconds), + EasingFunction = easingFunction + }; + + Storyboard.SetTargetProperty(heightAnimation, new PropertyPath(FrameworkElement.HeightProperty)); + + var storyboard = new Storyboard(); + storyboard.Children.Add(heightAnimation); + + storyboard.Completed += (_, _) => + { + if (double.IsNaN(targetHeight)) element.Height = double.NaN; + onCompleted?.Invoke(); + }; + + storyboard.Begin(element); + } } \ No newline at end of file diff --git a/VRCOSC.App/UI/Core/VRCOSCImage.xaml b/VRCOSC.App/UI/Core/VRCOSCImage.xaml new file mode 100644 index 00000000..fe660f85 --- /dev/null +++ b/VRCOSC.App/UI/Core/VRCOSCImage.xaml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/VRCOSC.App/UI/Core/VRCOSCImage.xaml.cs b/VRCOSC.App/UI/Core/VRCOSCImage.xaml.cs new file mode 100644 index 00000000..d9f37f5b --- /dev/null +++ b/VRCOSC.App/UI/Core/VRCOSCImage.xaml.cs @@ -0,0 +1,49 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Windows; +using VRCOSC.App.Utils; +using WpfAnimatedGif; + +// ReSharper disable InconsistentNaming + +namespace VRCOSC.App.UI.Core; + +public partial class VRCOSCImage +{ + public VRCOSCImage() + { + InitializeComponent(); + } + + public static readonly DependencyProperty UrlProperty = + DependencyProperty.Register(nameof(Url), typeof(string), typeof(VRCOSCImage), new PropertyMetadata(string.Empty, onUrlChanged)); + + private static void onUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((VRCOSCImage)d).updateImage((string)e.NewValue); + } + + public string Url + { + get => (string)GetValue(UrlProperty); + set => SetValue(UrlProperty, value); + } + + private void updateImage(string url) + { + ImageLoader.RetrieveFromURL(url, (bitmapImage, cached) => + { + Image.Source = bitmapImage; + Image.FadeInFromZero(cached ? 0 : 150); + + return; + + // TOOD: Figure out performance issues + if (url.EndsWith(".gif")) + { + ImageBehavior.SetAnimatedSource(Image, bitmapImage); + } + }); + } +} \ No newline at end of file diff --git a/VRCOSC.App/Utils/ImageLoader.cs b/VRCOSC.App/Utils/ImageLoader.cs index f0f12512..d53641ed 100644 --- a/VRCOSC.App/Utils/ImageLoader.cs +++ b/VRCOSC.App/Utils/ImageLoader.cs @@ -21,11 +21,10 @@ public static void RetrieveFromURL(string url, Action onLoade var bitmap = new BitmapImage(); bitmap.BeginInit(); - bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.UriSource = new Uri(url, UriKind.Absolute); bitmap.EndInit(); bitmap.DownloadCompleted += (_, _) => onLoaded.Invoke(bitmap, false); cache.Add(url, bitmap); } -} +} \ No newline at end of file diff --git a/VRCOSC.App/VRCOSC.App.csproj b/VRCOSC.App/VRCOSC.App.csproj index 15f3d2df..61ac7c26 100644 --- a/VRCOSC.App/VRCOSC.App.csproj +++ b/VRCOSC.App/VRCOSC.App.csproj @@ -45,6 +45,7 @@ + From e8c926c45a432d87000628e51f505913bef4fd9b Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Thu, 6 Feb 2025 20:29:09 +0000 Subject: [PATCH 67/74] Improve log reading and add user join/leave events --- .../SDK/Handlers/IVRCClientEventHandler.cs | 10 +- VRCOSC.App/SDK/VRChat/VRChatLogReader.cs | 118 +++++++++++------- 2 files changed, 80 insertions(+), 48 deletions(-) diff --git a/VRCOSC.App/SDK/Handlers/IVRCClientEventHandler.cs b/VRCOSC.App/SDK/Handlers/IVRCClientEventHandler.cs index 7caa7fec..f03cb098 100644 --- a/VRCOSC.App/SDK/Handlers/IVRCClientEventHandler.cs +++ b/VRCOSC.App/SDK/Handlers/IVRCClientEventHandler.cs @@ -1,10 +1,14 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. +using VRCOSC.App.SDK.VRChat; + namespace VRCOSC.App.SDK.Handlers; public interface IVRCClientEventHandler { - public void OnWorldExit(); - public void OnWorldEnter(string worldID); -} + public void OnInstanceEnter(VRChatClientEventInstanceEnter eventArgs); + public void OnInstanceExit(VRChatClientEventInstanceExit eventArgs); + public void OnUserJoined(VRChatClientEventUserJoined eventArgs); + public void OnUserLeft(VRChatClientEventUserLeft eventArgs); +} \ No newline at end of file diff --git a/VRCOSC.App/SDK/VRChat/VRChatLogReader.cs b/VRCOSC.App/SDK/VRChat/VRChatLogReader.cs index 380b4e68..8f1c1510 100644 --- a/VRCOSC.App/SDK/VRChat/VRChatLogReader.cs +++ b/VRCOSC.App/SDK/VRChat/VRChatLogReader.cs @@ -11,23 +11,28 @@ using VRCOSC.App.SDK.Handlers; using VRCOSC.App.Utils; +// ReSharper disable NotAccessedPositionalProperty.Global +// ReSharper disable MissingBlankLines + namespace VRCOSC.App.SDK.VRChat; -internal class VRChatLogReader +internal static class VRChatLogReader { private static readonly string logfile_location = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData).Replace("Roaming", "LocalLow"), "VRChat", "VRChat"); private const string logfile_pattern = "output_log_*"; private static readonly Regex datetime_regex = new(@"^(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}).+$"); - private static readonly Regex world_exit_regex = new("^.+Fetching world information for (wrld_[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"); - private static readonly Regex world_enter_regex = new(@"^.+Finished entering world\.$"); + private static readonly Regex instance_exit_regex = new("^.+OnLeftRoom$"); + private static readonly Regex instance_change_regex = new("^.+Destination set: (.+):.+$"); + private static readonly Regex instance_enter_regex = new(@"^.+Finished entering world\.$"); + private static readonly Regex user_joined_regex = new(@"^.+OnPlayerJoined .+ \((.+)\)$"); + private static readonly Regex user_left_regex = new(@"^.+OnPlayerLeft .+ \((.+)\)$"); - private static readonly List line_buffer = new(); + private static readonly List line_buffer = []; private static string? logFile; private static long byteOffset; private static Repeater? processTask; private static readonly object process_lock = new(); - private static DateTime? startDateTime; private static string? currentWorldID { get; set; } @@ -35,8 +40,6 @@ internal class VRChatLogReader internal static void Start() { - startDateTime = DateTime.Now; - reset(); if (!Directory.Exists(logfile_location)) @@ -46,7 +49,7 @@ internal static void Start() } processTask = new Repeater($"{nameof(VRChatLogReader)}-{nameof(process)}", process); - processTask.Start(TimeSpan.FromMilliseconds(200), true); + processTask.Start(TimeSpan.FromMilliseconds(50), true); } internal static async void Stop() @@ -72,8 +75,14 @@ private static void process() readLinesFromFile(); if (line_buffer.Count == 0) return; - checkWorldExit(); - checkWorldEnter(); + foreach (var logLine in line_buffer) + { + checkInstanceExit(logLine); + checkInstanceChange(logLine); + checkInstanceEnter(logLine); + checkUserLeft(logLine); + checkUserJoined(logLine); + } line_buffer.Clear(); } @@ -103,9 +112,11 @@ private static void readLinesFromFile() while (linesRead < 100 && streamReader.ReadLine() is { } line) { - if (!string.IsNullOrWhiteSpace(line) && isValidLogLine(line)) + var dateTime = parseDate(line); + + if (!string.IsNullOrWhiteSpace(line) && dateTime is not null) { - line_buffer.Add(line); + line_buffer.Add(new LogLine(dateTime.Value, line)); linesRead++; } @@ -118,53 +129,70 @@ private static void readLinesFromFile() } } - // checks that there's a date at the start of the line and that the date is after we've started - private static bool isValidLogLine(string line) + private static DateTime? parseDate(string line) { var foundDateTime = datetime_regex.Matches(line).LastOrDefault()?.Groups.Values.LastOrDefault()?.Value; - if (foundDateTime is null) return false; + if (foundDateTime is null) return null; - var parsedDate = DateTime.ParseExact(foundDateTime, "yyyy.MM.dd HH:mm:ss", null); - return parsedDate > startDateTime; + return DateTime.ParseExact(foundDateTime, "yyyy.MM.dd HH:mm:ss", null); } - private static void checkWorldExit() + private static IEnumerable handlers => ModuleManager.GetInstance().GetRunningModulesOfType(); + + private static void checkInstanceExit(LogLine logLine) { - string? newWorldID = null; + var match = instance_exit_regex.Match(logLine.Line); + if (!match.Success) return; - foreach (var line in line_buffer) - { - var worldIDGroup = world_exit_regex.Matches(line).LastOrDefault()?.Groups.Values.LastOrDefault(); - if (worldIDGroup is null) continue; + handlers.ForEach(handler => handler.OnInstanceExit(new VRChatClientEventInstanceExit(logLine.DateTime))); + } - var worldIDCapture = worldIDGroup.Captures.FirstOrDefault(); - if (worldIDCapture is null) continue; + private static void checkInstanceChange(LogLine logLine) + { + var match = instance_change_regex.Match(logLine.Line); + if (!match.Success) return; - newWorldID = worldIDCapture.Value; - } + currentWorldID = match.Groups[1].Captures[0].Value; + } - if (newWorldID is not null && newWorldID != currentWorldID) - { - currentWorldID = newWorldID; - Logger.Log("Detected world leave"); + private static void checkInstanceEnter(LogLine logLine) + { + var match = instance_enter_regex.Match(logLine.Line); + if (!match.Success) return; - ModuleManager.GetInstance().GetRunningModulesOfType().ForEach(handler => handler.OnWorldExit()); + if (currentWorldID is null) + { + Logger.Log("Entered world without knowing the world Id"); + return; } + + OnWorldEnter?.Invoke(currentWorldID); + handlers.ForEach(handler => handler.OnInstanceEnter(new VRChatClientEventInstanceEnter(logLine.DateTime, currentWorldID))); } - private static void checkWorldEnter() + private static void checkUserLeft(LogLine logLine) { - foreach (var line in line_buffer.AsEnumerable().Reverse()) - { - if (world_enter_regex.IsMatch(line)) - { - Logger.Log($"Detected world enter to '{currentWorldID}'"); - OnWorldEnter?.Invoke(currentWorldID!); - ModuleManager.GetInstance().GetRunningModulesOfType().ForEach(handler => handler.OnWorldEnter(currentWorldID!)); + var match = user_left_regex.Match(logLine.Line); + if (!match.Success) return; - currentWorldID = null; - break; - } - } + var userId = match.Groups[1].Captures[0].Value; + handlers.ForEach(handler => handler.OnUserLeft(new VRChatClientEventUserLeft(logLine.DateTime, userId))); } -} \ No newline at end of file + + private static void checkUserJoined(LogLine logLine) + { + var match = user_joined_regex.Match(logLine.Line); + if (!match.Success) return; + + var userId = match.Groups[1].Captures[0].Value; + handlers.ForEach(handler => handler.OnUserJoined(new VRChatClientEventUserJoined(logLine.DateTime, userId))); + } + + private readonly record struct LogLine(DateTime DateTime, string Line); +} + +public record VRChatClientEvent(DateTime DateTime); +public record VRChatClientEventInstanceExit(DateTime DateTime) : VRChatClientEvent(DateTime); +public record VRChatClientEventInstanceEnter(DateTime DateTime, string WorldId) : VRChatClientEvent(DateTime); +public record VRChatClientEventUserJoined(DateTime DateTime, string UserId) : VRChatClientEvent(DateTime); +public record VRChatClientEventUserLeft(DateTime DateTime, string UserId) : VRChatClientEvent(DateTime); \ No newline at end of file From 9f30fb51dea940642457ea545b5178e38969115f Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Thu, 6 Feb 2025 20:59:05 +0000 Subject: [PATCH 68/74] Standardise log event names --- .../SDK/Handlers/IVRCClientEventHandler.cs | 4 +- VRCOSC.App/SDK/VRChat/VRChatLogReader.cs | 38 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/VRCOSC.App/SDK/Handlers/IVRCClientEventHandler.cs b/VRCOSC.App/SDK/Handlers/IVRCClientEventHandler.cs index f03cb098..a79ba625 100644 --- a/VRCOSC.App/SDK/Handlers/IVRCClientEventHandler.cs +++ b/VRCOSC.App/SDK/Handlers/IVRCClientEventHandler.cs @@ -7,8 +7,8 @@ namespace VRCOSC.App.SDK.Handlers; public interface IVRCClientEventHandler { - public void OnInstanceEnter(VRChatClientEventInstanceEnter eventArgs); - public void OnInstanceExit(VRChatClientEventInstanceExit eventArgs); + public void OnInstanceJoined(VRChatClientEventInstanceJoined eventArgs); + public void OnInstanceLeft(VRChatClientEventInstanceLeft eventArgs); public void OnUserJoined(VRChatClientEventUserJoined eventArgs); public void OnUserLeft(VRChatClientEventUserLeft eventArgs); } \ No newline at end of file diff --git a/VRCOSC.App/SDK/VRChat/VRChatLogReader.cs b/VRCOSC.App/SDK/VRChat/VRChatLogReader.cs index 8f1c1510..658fa111 100644 --- a/VRCOSC.App/SDK/VRChat/VRChatLogReader.cs +++ b/VRCOSC.App/SDK/VRChat/VRChatLogReader.cs @@ -22,9 +22,9 @@ internal static class VRChatLogReader private const string logfile_pattern = "output_log_*"; private static readonly Regex datetime_regex = new(@"^(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}).+$"); - private static readonly Regex instance_exit_regex = new("^.+OnLeftRoom$"); + private static readonly Regex instance_left_regex = new("^.+OnLeftRoom$"); private static readonly Regex instance_change_regex = new("^.+Destination set: (.+):.+$"); - private static readonly Regex instance_enter_regex = new(@"^.+Finished entering world\.$"); + private static readonly Regex instance_joined_regex = new(@"^.+Finished entering world\.$"); private static readonly Regex user_joined_regex = new(@"^.+OnPlayerJoined .+ \((.+)\)$"); private static readonly Regex user_left_regex = new(@"^.+OnPlayerLeft .+ \((.+)\)$"); @@ -34,7 +34,7 @@ internal static class VRChatLogReader private static Repeater? processTask; private static readonly object process_lock = new(); - private static string? currentWorldID { get; set; } + private static string? currentWorldId { get; set; } public static Action? OnWorldEnter; @@ -65,7 +65,7 @@ private static void reset() line_buffer.Clear(); logFile = null; byteOffset = 0; - currentWorldID = null; + currentWorldId = null; } private static void process() @@ -77,9 +77,9 @@ private static void process() foreach (var logLine in line_buffer) { - checkInstanceExit(logLine); + checkInstanceLeft(logLine); checkInstanceChange(logLine); - checkInstanceEnter(logLine); + checkInstanceJoined(logLine); checkUserLeft(logLine); checkUserJoined(logLine); } @@ -139,12 +139,12 @@ private static void readLinesFromFile() private static IEnumerable handlers => ModuleManager.GetInstance().GetRunningModulesOfType(); - private static void checkInstanceExit(LogLine logLine) + private static void checkInstanceLeft(LogLine logLine) { - var match = instance_exit_regex.Match(logLine.Line); + var match = instance_left_regex.Match(logLine.Line); if (!match.Success) return; - handlers.ForEach(handler => handler.OnInstanceExit(new VRChatClientEventInstanceExit(logLine.DateTime))); + handlers.ForEach(handler => handler.OnInstanceLeft(new VRChatClientEventInstanceLeft(logLine.DateTime))); } private static void checkInstanceChange(LogLine logLine) @@ -152,22 +152,22 @@ private static void checkInstanceChange(LogLine logLine) var match = instance_change_regex.Match(logLine.Line); if (!match.Success) return; - currentWorldID = match.Groups[1].Captures[0].Value; + currentWorldId = match.Groups[1].Captures[0].Value; } - private static void checkInstanceEnter(LogLine logLine) + private static void checkInstanceJoined(LogLine logLine) { - var match = instance_enter_regex.Match(logLine.Line); + var match = instance_joined_regex.Match(logLine.Line); if (!match.Success) return; - if (currentWorldID is null) + if (currentWorldId is null) { Logger.Log("Entered world without knowing the world Id"); return; } - OnWorldEnter?.Invoke(currentWorldID); - handlers.ForEach(handler => handler.OnInstanceEnter(new VRChatClientEventInstanceEnter(logLine.DateTime, currentWorldID))); + OnWorldEnter?.Invoke(currentWorldId); + handlers.ForEach(handler => handler.OnInstanceJoined(new VRChatClientEventInstanceJoined(logLine.DateTime, currentWorldId))); } private static void checkUserLeft(LogLine logLine) @@ -192,7 +192,7 @@ private static void checkUserJoined(LogLine logLine) } public record VRChatClientEvent(DateTime DateTime); -public record VRChatClientEventInstanceExit(DateTime DateTime) : VRChatClientEvent(DateTime); -public record VRChatClientEventInstanceEnter(DateTime DateTime, string WorldId) : VRChatClientEvent(DateTime); -public record VRChatClientEventUserJoined(DateTime DateTime, string UserId) : VRChatClientEvent(DateTime); -public record VRChatClientEventUserLeft(DateTime DateTime, string UserId) : VRChatClientEvent(DateTime); \ No newline at end of file +public record VRChatClientEventInstanceLeft(DateTime DateTime) : VRChatClientEvent(DateTime); +public record VRChatClientEventInstanceJoined(DateTime DateTime, string WorldId) : VRChatClientEvent(DateTime); +public record VRChatClientEventUserLeft(DateTime DateTime, string UserId) : VRChatClientEvent(DateTime); +public record VRChatClientEventUserJoined(DateTime DateTime, string UserId) : VRChatClientEvent(DateTime); \ No newline at end of file From 2fcd728afa16e337592a619436b73665535c70aa Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Fri, 7 Feb 2025 20:47:12 +0000 Subject: [PATCH 69/74] Don't need to parse F when finding a parameter --- VRCOSC.App/OSC/VRChat/VRChatOscClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VRCOSC.App/OSC/VRChat/VRChatOscClient.cs b/VRCOSC.App/OSC/VRChat/VRChatOscClient.cs index 6638ce5e..00949025 100644 --- a/VRCOSC.App/OSC/VRChat/VRChatOscClient.cs +++ b/VRCOSC.App/OSC/VRChat/VRChatOscClient.cs @@ -117,8 +117,8 @@ public void Init(ConnectionManager connectionManager) { "f" => Convert.ToSingle(node.Value[0]), "i" => Convert.ToInt32(node.Value[0]), + // T gets returned for true and false "T" => Convert.ToBoolean(node.Value[0]), - "F" => Convert.ToBoolean(node.Value[0]), _ => throw new InvalidOperationException($"Unknown type '{node.OscType}'") }; From 3a5c6c3a4e559737415354dd563540718941edbc Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Fri, 7 Feb 2025 20:48:07 +0000 Subject: [PATCH 70/74] Use BorderClipConverter where possible --- .../UI/Styles/HeaderFooterListViewStyle.xaml | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/VRCOSC.App/UI/Styles/HeaderFooterListViewStyle.xaml b/VRCOSC.App/UI/Styles/HeaderFooterListViewStyle.xaml index 5a40ae6f..7259d526 100644 --- a/VRCOSC.App/UI/Styles/HeaderFooterListViewStyle.xaml +++ b/VRCOSC.App/UI/Styles/HeaderFooterListViewStyle.xaml @@ -5,6 +5,7 @@ + - + \ No newline at end of file From 72b5daea2f9afeeda3496a27fd9e631a8712c070 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Sun, 9 Feb 2025 21:17:01 +0000 Subject: [PATCH 71/74] Change AvatarConfig to use ParameterType --- VRCOSC.App/SDK/VRChat/AvatarConfig.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/VRCOSC.App/SDK/VRChat/AvatarConfig.cs b/VRCOSC.App/SDK/VRChat/AvatarConfig.cs index 7ca210f2..7df799f7 100644 --- a/VRCOSC.App/SDK/VRChat/AvatarConfig.cs +++ b/VRCOSC.App/SDK/VRChat/AvatarConfig.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Newtonsoft.Json; +using VRCOSC.App.SDK.Parameters; namespace VRCOSC.App.SDK.VRChat; @@ -34,16 +35,16 @@ public class AvatarConfigParameter public class AddressTypePair { [JsonProperty("address")] - public string Address = null!; + public string? Address; [JsonProperty("type")] - private string type = null!; + private string? type; - public Type Type => type switch + public ParameterType Type => type switch { - "Bool" => typeof(bool), - "Float" => typeof(float), - "Int" => typeof(int), + "Bool" => ParameterType.Bool, + "Float" => ParameterType.Float, + "Int" => ParameterType.Int, _ => throw new InvalidOperationException($"Cannot parse type {type}") }; -} +} \ No newline at end of file From ffe8d84c13b547716e628aaef7ddf50fe1f2b350 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 12 Feb 2025 17:43:52 +0000 Subject: [PATCH 72/74] Add support for dollies --- VRCOSC.App/AppManager.cs | 8 + VRCOSC.App/Dolly/Dolly.cs | 23 +++ VRCOSC.App/Dolly/DollyManager.cs | 153 ++++++++++++++++++ VRCOSC.App/Dolly/DollySection.cs | 56 +++++++ .../Serialisation/DollyManagerSerialiser.cs | 29 ++++ .../Serialisation/SerialisableDollyManager.cs | 49 ++++++ VRCOSC.App/MainApp.xaml | 1 + VRCOSC.App/OSC/VRChat/VRChatOscConstants.cs | 10 +- VRCOSC.App/OSC/VRChat/VRChatOscMessage.cs | 1 + VRCOSC.App/UI/Core/TextButton.cs | 20 +++ VRCOSC.App/UI/Styles/TextButtonStyle.xaml | 31 ++++ VRCOSC.App/UI/Views/Dolly/DollyView.xaml | 98 +++++++++++ VRCOSC.App/UI/Views/Dolly/DollyView.xaml.cs | 95 +++++++++++ VRCOSC.App/UI/Windows/MainWindow.xaml | 12 +- VRCOSC.App/UI/Windows/MainWindow.xaml.cs | 57 ++----- 15 files changed, 589 insertions(+), 54 deletions(-) create mode 100644 VRCOSC.App/Dolly/Dolly.cs create mode 100644 VRCOSC.App/Dolly/DollyManager.cs create mode 100644 VRCOSC.App/Dolly/DollySection.cs create mode 100644 VRCOSC.App/Dolly/Serialisation/DollyManagerSerialiser.cs create mode 100644 VRCOSC.App/Dolly/Serialisation/SerialisableDollyManager.cs create mode 100644 VRCOSC.App/UI/Core/TextButton.cs create mode 100644 VRCOSC.App/UI/Styles/TextButtonStyle.xaml create mode 100644 VRCOSC.App/UI/Views/Dolly/DollyView.xaml create mode 100644 VRCOSC.App/UI/Views/Dolly/DollyView.xaml.cs diff --git a/VRCOSC.App/AppManager.cs b/VRCOSC.App/AppManager.cs index 8ded8f63..6f0bea42 100644 --- a/VRCOSC.App/AppManager.cs +++ b/VRCOSC.App/AppManager.cs @@ -21,6 +21,7 @@ using VRCOSC.App.Audio; using VRCOSC.App.Audio.Whisper; using VRCOSC.App.ChatBox; +using VRCOSC.App.Dolly; using VRCOSC.App.Modules; using VRCOSC.App.OSC; using VRCOSC.App.OSC.VRChat; @@ -250,6 +251,11 @@ private void processOscMessageQueue() sendControlParameters(); } + if (message.IsDollyEvent) + { + DollyManager.GetInstance().HandleDollyEvent(message); + } + if (message.IsAvatarParameter) { var wasPlayerUpdated = VRChatClient.Player.Update(message.ParameterName, message.ParameterValue); @@ -589,9 +595,11 @@ public void ChangeProfile(Profile newProfile) => Application.Current.Dispatcher. ChatBoxManager.GetInstance().Unload(); ModuleManager.GetInstance().UnloadAllModules(); + DollyManager.GetInstance().Unload(); ProfileManager.GetInstance().ActiveProfile.Value = newProfile; + DollyManager.GetInstance().Load(); ModuleManager.GetInstance().LoadAllModules(); ChatBoxManager.GetInstance().Load(); RouterManager.GetInstance().Load(); diff --git a/VRCOSC.App/Dolly/Dolly.cs b/VRCOSC.App/Dolly/Dolly.cs new file mode 100644 index 00000000..de51d12d --- /dev/null +++ b/VRCOSC.App/Dolly/Dolly.cs @@ -0,0 +1,23 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System; +using VRCOSC.App.Utils; + +namespace VRCOSC.App.Dolly; + +public class Dolly +{ + public Guid Id { get; } + public Observable Name { get; } = new("New Dolly"); + + public Dolly() + { + Id = Guid.NewGuid(); + } + + public Dolly(Guid id) + { + Id = id; + } +} \ No newline at end of file diff --git a/VRCOSC.App/Dolly/DollyManager.cs b/VRCOSC.App/Dolly/DollyManager.cs new file mode 100644 index 00000000..cfff0705 --- /dev/null +++ b/VRCOSC.App/Dolly/DollyManager.cs @@ -0,0 +1,153 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using VRCOSC.App.Dolly.Serialisation; +using VRCOSC.App.OSC.VRChat; +using VRCOSC.App.Profiles; +using VRCOSC.App.Serialisation; +using VRCOSC.App.Utils; + +namespace VRCOSC.App.Dolly; + +public class DollyManager +{ + private static DollyManager? instance; + internal static DollyManager GetInstance() => instance ??= new DollyManager(); + + private VRChatOscClient oscClient => AppManager.GetInstance().VRChatOscClient; + private Storage dollyStorage => AppManager.GetInstance().Storage.GetStorageForDirectory(Path.Join("profiles", ProfileManager.GetInstance().ActiveProfile.Value.ID.ToString(), "dollies")); + private string vrchatDollyDirectoryPath => Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "VRChat", "CameraPaths"); + + public ObservableCollection Dollies { get; } = []; + + private readonly SerialisationManager serialisationManager; + private bool isLoaded; + + public Action? OnPlay; + public Action? OnStop; + + private DollyManager() + { + serialisationManager = new SerialisationManager(); + serialisationManager.RegisterSerialiser(1, new DollyManagerSerialiser(AppManager.GetInstance().Storage, this)); + + Dollies.OnCollectionChanged((newItems, oldItems) => + { + foreach (var newDolly in newItems) + { + newDolly.Name.Subscribe(_ => serialisationManager.Serialise()); + } + + if (!isLoaded) return; + + foreach (var oldDolly in oldItems) + { + var filePath = dollyStorage.GetFullPath($"{oldDolly.Id.ToString()}.json"); + File.Delete(filePath); + } + }, true); + + Dollies.OnCollectionChanged((_, _) => + { + if (!isLoaded) return; + + serialisationManager.Serialise(); + }); + } + + public void Unload() + { + serialisationManager.Serialise(); + isLoaded = false; + } + + public void Load() + { + Dollies.Clear(); + serialisationManager.Deserialise(); + isLoaded = true; + } + + public void Play() + { + oscClient.Send(VRChatOscConstants.ADDRESS_DOLLY_PLAY, true); + } + + public void Stop() + { + oscClient.Send(VRChatOscConstants.ADDRESS_DOLLY_PLAY, false); + } + + public void PlayDelayed(int secondsDelay) + { + oscClient.Send(VRChatOscConstants.ADDRESS_DOLLY_PLAYDELAYED, secondsDelay); + } + + /// + /// Imports the dolly file into VRChat + /// + public async void Import(Dolly dolly) + { + // not stopping causes vrchat to combine the paths and keep playing + Stop(); + await Task.Delay(50); + + var filePath = dollyStorage.GetFullPath($"{dolly.Id.ToString()}.json"); + var contents = await File.ReadAllTextAsync(filePath); + oscClient.Send(VRChatOscConstants.ADDRESS_DOLLY_IMPORT, contents); + } + + /// + /// Exports the current dolly out of VRChat + /// + public async Task Export() + { + var newDolly = new Dolly(); + var destinationFilePath = dollyStorage.GetFullPath($"{newDolly.Id.ToString()}.json"); + var currentDateTime = DateTime.Now; + + oscClient.Send(VRChatOscConstants.ADDRESS_DOLLY_EXPORT, [null]); + await Task.Delay(100); + + var vrchatDollyDirectory = new DirectoryInfo(vrchatDollyDirectoryPath); + var latestFile = vrchatDollyDirectory.GetFiles().Where(f => f.LastWriteTime >= currentDateTime).OrderByDescending(f => f.LastWriteTime).FirstOrDefault(); + if (latestFile is null) return; + + File.Copy(Path.Join(vrchatDollyDirectoryPath, latestFile.Name), destinationFilePath); + + Dollies.Add(newDolly); + } + + /// + /// Imports a dolly file from a file path to be managed + /// + public void ImportFile(string filePath) + { + if (!File.Exists(filePath)) return; + + var newDolly = new Dolly(); + var destinationFilePath = dollyStorage.GetFullPath($"{newDolly.Id.ToString()}.json"); + + File.Copy(filePath, destinationFilePath); + + Dollies.Add(newDolly); + } + + public void HandleDollyEvent(VRChatOscMessage message) + { + if (message.Address == VRChatOscConstants.ADDRESS_DOLLY_PLAY) + { + var playing = (bool)message.Arguments[0]!; + + if (playing) + OnPlay?.Invoke(); + else + OnStop?.Invoke(); + } + } +} \ No newline at end of file diff --git a/VRCOSC.App/Dolly/DollySection.cs b/VRCOSC.App/Dolly/DollySection.cs new file mode 100644 index 00000000..684afba8 --- /dev/null +++ b/VRCOSC.App/Dolly/DollySection.cs @@ -0,0 +1,56 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Numerics; +using Newtonsoft.Json; + +namespace VRCOSC.App.Dolly; + +[JsonObject(MemberSerialization.OptIn)] +public class DollySection +{ + [JsonProperty("Index")] + public int Index { get; set; } + + [JsonProperty("PathIndex")] + public int PathIndex { get; set; } + + [JsonProperty("FocalDistance")] + public float FocalDistance { get; set; } + + [JsonProperty("Aperture")] + public float Aperture { get; set; } + + [JsonProperty("Hue")] + public float Hue { get; set; } + + [JsonProperty("Saturation")] + public float Saturation { get; set; } + + [JsonProperty("Lightness")] + public float Lightness { get; set; } + + [JsonProperty("LookAtMeXOffset")] + public float LookAtMeXOffset { get; set; } + + [JsonProperty("LookAtMeYOffset")] + public float LookAtMeYOffset { get; set; } + + [JsonProperty("Zoom")] + public float Zoom { get; set; } + + [JsonProperty("Speed")] + public float Speed { get; set; } + + [JsonProperty("Duration")] + public float Duration { get; set; } + + [JsonProperty("Position")] + public Vector3 Position { get; set; } + + [JsonProperty("Rotation")] + public Vector3 Rotation { get; set; } + + [JsonProperty("IsLocal")] + public bool IsLocal { get; set; } +} \ No newline at end of file diff --git a/VRCOSC.App/Dolly/Serialisation/DollyManagerSerialiser.cs b/VRCOSC.App/Dolly/Serialisation/DollyManagerSerialiser.cs new file mode 100644 index 00000000..f1f1d5fa --- /dev/null +++ b/VRCOSC.App/Dolly/Serialisation/DollyManagerSerialiser.cs @@ -0,0 +1,29 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System; +using System.Linq; +using VRCOSC.App.Serialisation; +using VRCOSC.App.Utils; + +namespace VRCOSC.App.Dolly.Serialisation; + +internal class DollyManagerSerialiser : ProfiledSerialiser +{ + protected override string FileName => "dollies.json"; + + internal DollyManagerSerialiser(Storage storage, DollyManager reference) + : base(storage, reference) + { + } + + protected override bool ExecuteAfterDeserialisation(SerialisableDollyManager data) + { + Reference.Dollies.AddRange(data.Dollies.Select(serialisableDolly => new Dolly(Guid.Parse(serialisableDolly.Id)) + { + Name = { Value = serialisableDolly.Name } + })); + + return false; + } +} \ No newline at end of file diff --git a/VRCOSC.App/Dolly/Serialisation/SerialisableDollyManager.cs b/VRCOSC.App/Dolly/Serialisation/SerialisableDollyManager.cs new file mode 100644 index 00000000..4a9ebd46 --- /dev/null +++ b/VRCOSC.App/Dolly/Serialisation/SerialisableDollyManager.cs @@ -0,0 +1,49 @@ +// 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.Generic; +using System.Linq; +using Newtonsoft.Json; +using VRCOSC.App.Serialisation; + +namespace VRCOSC.App.Dolly.Serialisation; + +[JsonObject(MemberSerialization.OptIn)] +internal class SerialisableDollyManager : SerialisableVersion +{ + [JsonProperty("dollies")] + public List Dollies = []; + + [JsonConstructor] + public SerialisableDollyManager() + { + } + + public SerialisableDollyManager(DollyManager dollyManager) + { + Version = 1; + + Dollies.AddRange(dollyManager.Dollies.Select(dolly => new SerialisableDolly(dolly))); + } +} + +[JsonObject(MemberSerialization.OptIn)] +internal class SerialisableDolly +{ + [JsonProperty("id")] + public string Id = null!; + + [JsonProperty("name")] + public string Name = null!; + + [JsonConstructor] + public SerialisableDolly() + { + } + + public SerialisableDolly(Dolly dolly) + { + Id = dolly.Id.ToString(); + Name = dolly.Name.Value; + } +} \ No newline at end of file diff --git a/VRCOSC.App/MainApp.xaml b/VRCOSC.App/MainApp.xaml index 31338263..43fa54ba 100644 --- a/VRCOSC.App/MainApp.xaml +++ b/VRCOSC.App/MainApp.xaml @@ -10,6 +10,7 @@ + diff --git a/VRCOSC.App/OSC/VRChat/VRChatOscConstants.cs b/VRCOSC.App/OSC/VRChat/VRChatOscConstants.cs index f2733b36..b094eb9b 100644 --- a/VRCOSC.App/OSC/VRChat/VRChatOscConstants.cs +++ b/VRCOSC.App/OSC/VRChat/VRChatOscConstants.cs @@ -11,7 +11,9 @@ public static class VRChatOscConstants public const string ADDRESS_AVATAR_CHANGE = "/avatar/change"; public const string ADDRESS_CHATBOX_INPUT = "/chatbox/input"; public const string ADDRESS_CHATBOX_TYPING = "/chatbox/typing"; - - public const double UPDATE_FREQUENCY_SECONDS = 20; - public const double UPDATE_DELTA_MILLISECONDS = 1000d / UPDATE_FREQUENCY_SECONDS; -} + public const string ADDRESS_DOLLY_PREFIX = "/dolly/"; + public const string ADDRESS_DOLLY_PLAY = "/dolly/Play"; + public const string ADDRESS_DOLLY_PLAYDELAYED = "/dolly/PlayDelayed"; + public const string ADDRESS_DOLLY_IMPORT = "/dolly/Import"; + public const string ADDRESS_DOLLY_EXPORT = "/dolly/Export"; +} \ No newline at end of file diff --git a/VRCOSC.App/OSC/VRChat/VRChatOscMessage.cs b/VRCOSC.App/OSC/VRChat/VRChatOscMessage.cs index 285188cc..5dac5655 100644 --- a/VRCOSC.App/OSC/VRChat/VRChatOscMessage.cs +++ b/VRCOSC.App/OSC/VRChat/VRChatOscMessage.cs @@ -11,6 +11,7 @@ public record VRChatOscMessage : OSCMessage public bool IsAvatarChangeEvent => Address == VRChatOscConstants.ADDRESS_AVATAR_CHANGE; public bool IsAvatarParameter => Address.StartsWith(VRChatOscConstants.ADDRESS_AVATAR_PARAMETERS_PREFIX); public bool IsChatboxInput => Address == VRChatOscConstants.ADDRESS_CHATBOX_INPUT; + public bool IsDollyEvent => Address.StartsWith(VRChatOscConstants.ADDRESS_DOLLY_PREFIX); private string? parameterName; diff --git a/VRCOSC.App/UI/Core/TextButton.cs b/VRCOSC.App/UI/Core/TextButton.cs new file mode 100644 index 00000000..e8d959c0 --- /dev/null +++ b/VRCOSC.App/UI/Core/TextButton.cs @@ -0,0 +1,20 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Windows; + +// ReSharper disable InconsistentNaming + +namespace VRCOSC.App.UI.Core; + +public class TextButton : VRCOSCButton +{ + public static readonly DependencyProperty TextProperty = + DependencyProperty.Register(nameof(Text), typeof(string), typeof(TextButton), new PropertyMetadata(string.Empty)); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } +} \ No newline at end of file diff --git a/VRCOSC.App/UI/Styles/TextButtonStyle.xaml b/VRCOSC.App/UI/Styles/TextButtonStyle.xaml new file mode 100644 index 00000000..b82a5f62 --- /dev/null +++ b/VRCOSC.App/UI/Styles/TextButtonStyle.xaml @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/Dolly/DollyView.xaml b/VRCOSC.App/UI/Views/Dolly/DollyView.xaml new file mode 100644 index 00000000..033266d3 --- /dev/null +++ b/VRCOSC.App/UI/Views/Dolly/DollyView.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/Dolly/DollyView.xaml.cs b/VRCOSC.App/UI/Views/Dolly/DollyView.xaml.cs new file mode 100644 index 00000000..229bde9e --- /dev/null +++ b/VRCOSC.App/UI/Views/Dolly/DollyView.xaml.cs @@ -0,0 +1,95 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Windows; +using VRCOSC.App.Dolly; +using VRCOSC.App.UI.Core; +using VRCOSC.App.Utils; + +namespace VRCOSC.App.UI.Views.Dolly; + +public partial class DollyView +{ + public DollyManager DollyManager => DollyManager.GetInstance(); + + public Observable Delay { get; } = new(); + public Observable ShowPlay { get; } = new(); + public Observable ShowStop { get; } = new(Visibility.Collapsed); + + public DollyView() + { + InitializeComponent(); + DataContext = this; + + AppManager.GetInstance().State.Subscribe(newState => Dispatcher.Invoke(() => + { + if (newState == AppManagerState.Started) + { + Overlay.FadeOutFromOne(250); + } + else + { + Overlay.FadeIn(250); + } + })); + + DollyManager.OnPlay += () => + { + ShowPlay.Value = Visibility.Collapsed; + ShowStop.Value = Visibility.Visible; + }; + + DollyManager.OnStop += () => + { + ShowPlay.Value = Visibility.Visible; + ShowStop.Value = Visibility.Collapsed; + }; + } + + private void LoadDolly_OnClick(object sender, RoutedEventArgs e) + { + var element = (FrameworkElement)sender; + var dolly = (App.Dolly.Dolly)element.Tag; + + DollyManager.GetInstance().Import(dolly); + } + + private void DeleteDolly_OnClick(object sender, RoutedEventArgs e) + { + var element = (FrameworkElement)sender; + var dolly = (App.Dolly.Dolly)element.Tag; + + var result = MessageBox.Show("Are you sure you want to delete this dolly?", "Dolly Delete Warning", MessageBoxButton.YesNo); + if (result != MessageBoxResult.Yes) return; + + DollyManager.GetInstance().Dollies.Remove(dolly); + } + + private async void ImportVRChat_OnClick(object sender, RoutedEventArgs e) + { + await DollyManager.GetInstance().Export(); + } + + private async void ImportFile_OnClick(object sender, RoutedEventArgs e) + { + var filePath = await Platform.PickFileAsync(".json"); + if (filePath is null) return; + + DollyManager.GetInstance().ImportFile(filePath); + } + + private void Play_OnClick(object sender, RoutedEventArgs e) + { + DollyManager.GetInstance().Play(); + } + + private void PlayDelayed_OnClick(object sender, RoutedEventArgs e) + { + DollyManager.GetInstance().PlayDelayed(Delay.Value); + } + + private void Stop_OnClick(object sender, RoutedEventArgs e) + { + DollyManager.GetInstance().Stop(); + } +} \ No newline at end of file diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml b/VRCOSC.App/UI/Windows/MainWindow.xaml index 6ea97225..c30982ec 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml @@ -96,16 +96,8 @@ - - - - - - - - + + diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs index 3cdb91f5..65c22324 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs @@ -13,6 +13,7 @@ using Semver; using VRCOSC.App.Actions; using VRCOSC.App.ChatBox; +using VRCOSC.App.Dolly; using VRCOSC.App.Modules; using VRCOSC.App.OVR; using VRCOSC.App.Packages; @@ -22,16 +23,14 @@ using VRCOSC.App.Settings; using VRCOSC.App.Startup; using VRCOSC.App.UI.Core; -using VRCOSC.App.UI.Views.AppDebug; using VRCOSC.App.UI.Views.AppSettings; using VRCOSC.App.UI.Views.ChatBox; +using VRCOSC.App.UI.Views.Dolly; using VRCOSC.App.UI.Views.Information; using VRCOSC.App.UI.Views.Modules; using VRCOSC.App.UI.Views.Packages; using VRCOSC.App.UI.Views.Profiles; -using VRCOSC.App.UI.Views.Router; using VRCOSC.App.UI.Views.Run; -using VRCOSC.App.UI.Views.Startup; using VRCOSC.App.Updater; using VRCOSC.App.Utils; using Application = System.Windows.Application; @@ -51,11 +50,9 @@ public partial class MainWindow public PackagesView PackagesView = null!; public ModulesView ModulesView = null!; - public RouterView RouterView = null!; public ChatBoxView ChatBoxView = null!; - public StartupView StartupView = null!; + public DollyView DollyView = null!; public RunView RunView = null!; - public AppDebugView AppDebugView = null!; public ProfilesView ProfilesView = null!; public AppSettingsView AppSettingsView = null!; public InformationView InformationView = null!; @@ -149,17 +146,14 @@ private async void load() PackagesView = new PackagesView(); ModulesView = new ModulesView(); - RouterView = new RouterView(); ChatBoxView = new ChatBoxView(); - StartupView = new StartupView(); + DollyView = new DollyView(); RunView = new RunView(); - AppDebugView = new AppDebugView(); ProfilesView = new ProfilesView(); AppSettingsView = new AppSettingsView(); InformationView = new InformationView(); SettingsManager.GetInstance().GetObservable(VRCOSCSetting.EnableAppDebug).Subscribe(newValue => ShowAppDebug.Value = newValue, true); - SettingsManager.GetInstance().GetObservable(VRCOSCSetting.EnableRouter).Subscribe(newValue => ShowRouter.Value = newValue, true); await PackageManager.GetInstance().Load(); @@ -201,6 +195,7 @@ private async void load() ModuleManager.GetInstance().LoadAllModules(); ChatBoxManager.GetInstance().Load(); RouterManager.GetInstance().Load(); + DollyManager.GetInstance().Load(); StartupManager.GetInstance().Load(); setContent(ModulesView); @@ -215,28 +210,20 @@ private async Task checkForUpdates() { if (velopackUpdater.IsInstalled()) { - // only check for an update if we haven't just updated (to save time checking again) - if (!hasAppVersionChanged()) + await velopackUpdater.CheckForUpdatesAsync(); + + if (velopackUpdater.IsUpdateAvailable()) { - await velopackUpdater.CheckForUpdatesAsync(); + var shouldUpdate = velopackUpdater.PresentUpdate(); - if (velopackUpdater.IsUpdateAvailable()) + if (shouldUpdate) { - var shouldUpdate = velopackUpdater.PresentUpdate(); - - if (shouldUpdate) - { - await ShowLoadingOverlay(new CallbackProgressAction("Updating VRCOSC", () => velopackUpdater.ExecuteUpdateAsync())); - return true; - } + await ShowLoadingOverlay(new CallbackProgressAction("Updating VRCOSC", () => velopackUpdater.ExecuteUpdateAsync())); + return true; } - - Logger.Log("No updates. Proceeding with loading"); - } - else - { - Logger.Log("App has just updated. Skipping update check"); } + + Logger.Log("No updates. Proceeding with loading"); } else { @@ -252,7 +239,7 @@ private async Task checkForUpdates() private int calculateVersionPrecedence() { var installedVersionStr = SettingsManager.GetInstance().GetValue(VRCOSCMetadata.InstalledVersion); - if (string.IsNullOrEmpty(installedVersionStr)) return 0; + if (string.IsNullOrEmpty(installedVersionStr)) return 1; var newVersion = SemVersion.Parse(AppManager.Version, SemVersionStyles.Any); var installedVersion = SemVersion.Parse(installedVersionStr, SemVersionStyles.Any); @@ -462,19 +449,14 @@ private void ModulesButton_OnClick(object sender, RoutedEventArgs e) setContent(ModulesView); } - private void RouterButton_OnClick(object sender, RoutedEventArgs e) - { - setContent(RouterView); - } - private void ChatBoxButton_OnClick(object sender, RoutedEventArgs e) { setContent(ChatBoxView); } - private void StartupButton_OnClick(object sender, RoutedEventArgs e) + private void DollyButton_OnClick(object sender, RoutedEventArgs e) { - setContent(StartupView); + setContent(DollyView); } private void RunButton_OnClick(object sender, RoutedEventArgs e) @@ -482,11 +464,6 @@ private void RunButton_OnClick(object sender, RoutedEventArgs e) setContent(RunView); } - private void DebugButton_OnClick(object sender, RoutedEventArgs e) - { - setContent(AppDebugView); - } - private void ProfilesButton_OnClick(object sender, RoutedEventArgs e) { setContent(ProfilesView); From 134c73a5d18d03cc2ba64334ebee59c64c7b9a97 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 12 Feb 2025 17:44:56 +0000 Subject: [PATCH 73/74] Add support for changing avatars to the SDK --- VRCOSC.App/SDK/Modules/Module.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/VRCOSC.App/SDK/Modules/Module.cs b/VRCOSC.App/SDK/Modules/Module.cs index b99becd7..2fa72adc 100644 --- a/VRCOSC.App/SDK/Modules/Module.cs +++ b/VRCOSC.App/SDK/Modules/Module.cs @@ -815,6 +815,14 @@ protected void SetRuntimeView(Type viewType) RuntimeViewType = viewType; } + /// + /// Changes the user's avatar into the ID that's provided + /// + protected void ChangeAvatar(string avatarId) + { + AppManager.GetInstance().VRChatOscClient.Send($"{VRChatOscConstants.ADDRESS_AVATAR_CHANGE}", avatarId); + } + /// /// Allows you to send any parameter name and value. /// If you want the user to be able to customise the parameter, register a parameter and use From 2004871ce2fe995e59db50af94c464ef5931ce87 Mon Sep 17 00:00:00 2001 From: VolcanicArts Date: Wed, 12 Feb 2025 17:46:45 +0000 Subject: [PATCH 74/74] Move router, startup, and debug, to be settings tabs --- VRCOSC.App/Router/RouterManager.cs | 3 - VRCOSC.App/Settings/SettingsManager.cs | 2 - .../UI/Views/AppDebug/AppDebugView.xaml.cs | 72 --------------- .../AppDebugView.xaml | 4 +- .../UI/Views/AppSettings/AppDebugView.xaml.cs | 87 +++++++++++++++++++ .../UI/Views/AppSettings/AppSettingsView.xaml | 37 ++++---- .../Views/AppSettings/AppSettingsView.xaml.cs | 22 +++-- .../{Router => AppSettings}/RouterView.xaml | 5 +- .../RouterView.xaml.cs | 2 +- .../{Startup => AppSettings}/StartupView.xaml | 4 +- .../StartupView.xaml.cs | 2 +- VRCOSC.App/VRCOSC.App.csproj | 15 ++++ 12 files changed, 143 insertions(+), 112 deletions(-) delete mode 100644 VRCOSC.App/UI/Views/AppDebug/AppDebugView.xaml.cs rename VRCOSC.App/UI/Views/{AppDebug => AppSettings}/AppDebugView.xaml (97%) create mode 100644 VRCOSC.App/UI/Views/AppSettings/AppDebugView.xaml.cs rename VRCOSC.App/UI/Views/{Router => AppSettings}/RouterView.xaml (97%) rename VRCOSC.App/UI/Views/{Router => AppSettings}/RouterView.xaml.cs (96%) rename VRCOSC.App/UI/Views/{Startup => AppSettings}/StartupView.xaml (97%) rename VRCOSC.App/UI/Views/{Startup => AppSettings}/StartupView.xaml.cs (94%) diff --git a/VRCOSC.App/Router/RouterManager.cs b/VRCOSC.App/Router/RouterManager.cs index 43525f00..10f6cb73 100644 --- a/VRCOSC.App/Router/RouterManager.cs +++ b/VRCOSC.App/Router/RouterManager.cs @@ -10,7 +10,6 @@ using VRCOSC.App.OSC.VRChat; using VRCOSC.App.Router.Serialisation; using VRCOSC.App.Serialisation; -using VRCOSC.App.Settings; using VRCOSC.App.Utils; namespace VRCOSC.App.Router; @@ -51,8 +50,6 @@ public void Load() public async Task Start() { - if (!SettingsManager.GetInstance().GetValue(VRCOSCSetting.EnableRouter)) return; - foreach (var route in Routes) { try diff --git a/VRCOSC.App/Settings/SettingsManager.cs b/VRCOSC.App/Settings/SettingsManager.cs index 0a3b6d00..51f0ef89 100644 --- a/VRCOSC.App/Settings/SettingsManager.cs +++ b/VRCOSC.App/Settings/SettingsManager.cs @@ -68,7 +68,6 @@ private void writeDefaults() setDefault(VRCOSCSetting.OutgoingEndpoint, "127.0.0.1:9000"); setDefault(VRCOSCSetting.IncomingEndpoint, "127.0.0.1:9001"); setDefault(VRCOSCSetting.EnableAppDebug, false); - setDefault(VRCOSCSetting.EnableRouter, false); setDefault(VRCOSCSetting.SelectedMicrophoneID, string.Empty); setDefault(VRCOSCSetting.SpeechEnabled, true); setDefault(VRCOSCSetting.SpeechModelPath, string.Empty); @@ -137,7 +136,6 @@ public enum VRCOSCSetting OutgoingEndpoint, IncomingEndpoint, EnableAppDebug, - EnableRouter, SelectedMicrophoneID, SpeechEnabled, SpeechModelPath, diff --git a/VRCOSC.App/UI/Views/AppDebug/AppDebugView.xaml.cs b/VRCOSC.App/UI/Views/AppDebug/AppDebugView.xaml.cs deleted file mode 100644 index 0a0fa454..00000000 --- a/VRCOSC.App/UI/Views/AppDebug/AppDebugView.xaml.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. -// See the LICENSE file in the repository root for full license text. - -using System; -using System.Diagnostics; -using System.Text; -using System.Windows; -using VRCOSC.App.Modules; -using VRCOSC.App.Settings; -using VRCOSC.App.Utils; - -namespace VRCOSC.App.UI.Views.AppDebug; - -public partial class AppDebugView -{ - public Observable Port9000BoundProcess { get; } = new(string.Empty); - - private readonly Repeater repeater; - - public AppDebugView() - { - InitializeComponent(); - DataContext = this; - - repeater = new Repeater($"{nameof(AppDebugView)}-{nameof(executePowerShellCommand)}", () => Port9000BoundProcess.Value = $"{(executePowerShellCommand("Get-Process -Id (Get-NetUDPEndpoint -LocalPort 9000).OwningProcess | Select-Object -ExpandProperty ProcessName") ?? "Nothing").ReplaceLineEndings(string.Empty)}"); - repeater.Start(TimeSpan.FromSeconds(5), true); - } - - private static string? executePowerShellCommand(string command) - { - if (!SettingsManager.GetInstance().GetValue(VRCOSCSetting.EnableAppDebug)) return string.Empty; - - var psi = new ProcessStartInfo - { - FileName = "powershell.exe", - Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"{command}\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using var process = new Process(); - - process.StartInfo = psi; - - var output = new StringBuilder(); - var error = new StringBuilder(); - - process.OutputDataReceived += (_, e) => - { - if (e.Data != null) output.AppendLine(e.Data); - }; - - process.ErrorDataReceived += (_, e) => - { - if (e.Data != null) error.AppendLine(e.Data); - }; - - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - - return error.Length > 0 ? null : output.ToString(); - } - - private async void ReloadModules_OnClick(object sender, RoutedEventArgs e) - { - await ModuleManager.GetInstance().ReloadAllModules(); - } -} \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/AppDebug/AppDebugView.xaml b/VRCOSC.App/UI/Views/AppSettings/AppDebugView.xaml similarity index 97% rename from VRCOSC.App/UI/Views/AppDebug/AppDebugView.xaml rename to VRCOSC.App/UI/Views/AppSettings/AppDebugView.xaml index f59d36ab..5e442758 100644 --- a/VRCOSC.App/UI/Views/AppDebug/AppDebugView.xaml +++ b/VRCOSC.App/UI/Views/AppSettings/AppDebugView.xaml @@ -12,7 +12,7 @@ - + - + \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/AppSettings/AppDebugView.xaml.cs b/VRCOSC.App/UI/Views/AppSettings/AppDebugView.xaml.cs new file mode 100644 index 00000000..b4eea474 --- /dev/null +++ b/VRCOSC.App/UI/Views/AppSettings/AppDebugView.xaml.cs @@ -0,0 +1,87 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System; +using System.Diagnostics; +using System.Text; +using System.Windows; +using VRCOSC.App.Modules; +using VRCOSC.App.Utils; + +namespace VRCOSC.App.UI.Views.AppDebug; + +public partial class AppDebugView +{ + public Observable Port9000BoundProcess { get; } = new(string.Empty); + + private Repeater? repeater; + + public AppDebugView() + { + InitializeComponent(); + DataContext = this; + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + repeater = new Repeater($"{nameof(AppDebugView)}-{nameof(executePowerShellCommand)}", () => Port9000BoundProcess.Value = $"{(executePowerShellCommand("Get-Process -Id (Get-NetUDPEndpoint -LocalPort 9000).OwningProcess | Select-Object -ExpandProperty ProcessName") ?? "Nothing").ReplaceLineEndings(string.Empty)}"); + repeater.Start(TimeSpan.FromSeconds(5), true); + } + + private async void OnUnloaded(object sender, RoutedEventArgs e) + { + await repeater!.StopAsync(); + repeater = null; + } + + private static string? executePowerShellCommand(string command) + { + try + { + var psi = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"{command}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = new Process(); + + process.StartInfo = psi; + + var output = new StringBuilder(); + var error = new StringBuilder(); + + process.OutputDataReceived += (_, e) => + { + if (e.Data != null) output.AppendLine(e.Data); + }; + + process.ErrorDataReceived += (_, e) => + { + if (e.Data != null) error.AppendLine(e.Data); + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + + return error.Length > 0 ? null : output.ToString(); + } + catch (Exception) + { + return null; + } + } + + private async void ReloadModules_OnClick(object sender, RoutedEventArgs e) + { + await ModuleManager.GetInstance().ReloadAllModules(); + } +} \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml index 297ecb7a..a1220968 100644 --- a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml +++ b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:VRCOSC.App.UI.Views.AppSettings" xmlns:core="clr-namespace:VRCOSC.App.UI.Core" + xmlns:appDebug="clr-namespace:VRCOSC.App.UI.Views.AppDebug" mc:Ignorable="d" d:DesignWidth="1366" d:DesignHeight="748" Background="Transparent"> @@ -103,8 +104,14 @@ - - + + + + + + + + @@ -427,14 +434,14 @@ - + - + - - - - - - - + + + + + + + diff --git a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs index 16ccd969..6ebb73a4 100644 --- a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs +++ b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs @@ -117,12 +117,6 @@ public bool EnableAppDebug set => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.EnableAppDebug).Value = value; } - public bool EnableRouter - { - get => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.EnableRouter).Value; - set => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.EnableRouter).Value = value; - } - public string WhisperModelFilePath { get => SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechModelPath).Value; @@ -225,10 +219,12 @@ private void setPage(int pageIndex) OscContainer.Visibility = pageIndex == 1 ? Visibility.Visible : Visibility.Collapsed; AutomationContainer.Visibility = pageIndex == 2 ? Visibility.Visible : Visibility.Collapsed; UpdatesContainer.Visibility = pageIndex == 3 ? Visibility.Visible : Visibility.Collapsed; - AdvancedContainer.Visibility = pageIndex == 4 ? Visibility.Visible : Visibility.Collapsed; + DebugContainer.Visibility = pageIndex == 4 ? Visibility.Visible : Visibility.Collapsed; PackagesContainer.Visibility = pageIndex == 5 ? Visibility.Visible : Visibility.Collapsed; SpeechContainer.Visibility = pageIndex == 6 ? Visibility.Visible : Visibility.Collapsed; OVRContainer.Visibility = pageIndex == 7 ? Visibility.Visible : Visibility.Collapsed; + RouterContainer.Visibility = pageIndex == 8 ? Visibility.Visible : Visibility.Collapsed; + StartupContainer.Visibility = pageIndex == 9 ? Visibility.Visible : Visibility.Collapsed; } private void GeneralTabButton_OnClick(object sender, RoutedEventArgs e) @@ -251,11 +247,21 @@ private void UpdatesTabButton_OnClick(object sender, RoutedEventArgs e) setPage(3); } - private void AdvancedTabButton_OnClick(object sender, RoutedEventArgs e) + private void DebugTabButton_OnClick(object sender, RoutedEventArgs e) { setPage(4); } + private void RouterTabButton_OnClick(object sender, RoutedEventArgs e) + { + setPage(8); + } + + private void StartupTabButton_OnClick(object sender, RoutedEventArgs e) + { + setPage(9); + } + private void PackagesTabButton_OnClick(object sender, RoutedEventArgs e) { setPage(5); diff --git a/VRCOSC.App/UI/Views/Router/RouterView.xaml b/VRCOSC.App/UI/Views/AppSettings/RouterView.xaml similarity index 97% rename from VRCOSC.App/UI/Views/Router/RouterView.xaml rename to VRCOSC.App/UI/Views/AppSettings/RouterView.xaml index a98eaf1e..4fc0d317 100644 --- a/VRCOSC.App/UI/Views/Router/RouterView.xaml +++ b/VRCOSC.App/UI/Views/AppSettings/RouterView.xaml @@ -1,11 +1,10 @@ - @@ -14,7 +13,7 @@ Colour2="{StaticResource CBackground4}" /> - + - + diff --git a/VRCOSC.App/UI/Views/Startup/StartupView.xaml.cs b/VRCOSC.App/UI/Views/AppSettings/StartupView.xaml.cs similarity index 94% rename from VRCOSC.App/UI/Views/Startup/StartupView.xaml.cs rename to VRCOSC.App/UI/Views/AppSettings/StartupView.xaml.cs index 06e5924d..8016e291 100644 --- a/VRCOSC.App/UI/Views/Startup/StartupView.xaml.cs +++ b/VRCOSC.App/UI/Views/AppSettings/StartupView.xaml.cs @@ -5,7 +5,7 @@ using VRCOSC.App.Startup; using VRCOSC.App.UI.Core; -namespace VRCOSC.App.UI.Views.Startup; +namespace VRCOSC.App.UI.Views.AppSettings; public partial class StartupView { diff --git a/VRCOSC.App/VRCOSC.App.csproj b/VRCOSC.App/VRCOSC.App.csproj index 61ac7c26..48a98f66 100644 --- a/VRCOSC.App/VRCOSC.App.csproj +++ b/VRCOSC.App/VRCOSC.App.csproj @@ -225,6 +225,21 @@ Wpf Designer + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + \ No newline at end of file