diff --git a/VRCOSC.Desktop/VRCOSC.Desktop.csproj b/VRCOSC.Desktop/VRCOSC.Desktop.csproj index 151ee9c2..9e62a202 100644 --- a/VRCOSC.Desktop/VRCOSC.Desktop.csproj +++ b/VRCOSC.Desktop/VRCOSC.Desktop.csproj @@ -6,12 +6,12 @@ game.ico app.manifest 0.0.0 - 2023.516.0 + 2023.531.0 VRCOSC VolcanicArts VolcanicArts enable - 2023.516.0 + 2023.531.0 diff --git a/VRCOSC.Desktop/VRCOSCGameDesktop.cs b/VRCOSC.Desktop/VRCOSCGameDesktop.cs index 9ef622ce..aa191c21 100644 --- a/VRCOSC.Desktop/VRCOSCGameDesktop.cs +++ b/VRCOSC.Desktop/VRCOSCGameDesktop.cs @@ -4,12 +4,10 @@ using VRCOSC.Desktop.Updater; using VRCOSC.Game; using VRCOSC.Game.Graphics.Updater; -using VRCOSC.Modules; namespace VRCOSC.Desktop; public partial class VRCOSCGameDesktop : VRCOSCGame { - protected override IVRCOSCSecrets GetSecrets() => new VRCOSCModuleSecrets(); protected override VRCOSCUpdateManager CreateUpdateManager() => new SquirrelUpdateManager(); } diff --git a/VRCOSC.Game.Tests/VRCOSC.Game.Tests.csproj b/VRCOSC.Game.Tests/VRCOSC.Game.Tests.csproj index 348067dd..5a63d763 100644 --- a/VRCOSC.Game.Tests/VRCOSC.Game.Tests.csproj +++ b/VRCOSC.Game.Tests/VRCOSC.Game.Tests.csproj @@ -1,4 +1,4 @@ - + WinExe net6.0-windows10.0.22621.0 @@ -10,7 +10,7 @@ - + diff --git a/VRCOSC.Game/ChatBox/Clips/Clip.cs b/VRCOSC.Game/ChatBox/Clips/Clip.cs index c7917b95..6b1c66d0 100644 --- a/VRCOSC.Game/ChatBox/Clips/Clip.cs +++ b/VRCOSC.Game/ChatBox/Clips/Clip.cs @@ -228,8 +228,12 @@ private string formatText(IProvidesFormat formatter) if (!chatBoxManager.ModuleEnabledCache[clipVariable.Module]) return; chatBoxManager.VariableValues.TryGetValue((clipVariable.Module, clipVariable.Lookup), out var variableValue); - returnText = returnText.Replace(clipVariable.DisplayableFormat, variableValue ?? string.Empty); + + chatBoxManager.VariableValues.Where(pair => pair.Key.Item2.StartsWith($"{clipVariable.Lookup}_")).ForEach(pair => + { + returnText = returnText.Replace(clipVariable.DisplayableFormatWithSuffix(pair.Key.Item2.Split('_').Last()), pair.Value); + }); }); return returnText; @@ -268,8 +272,6 @@ private void removeStatesOfRemovedModules(NotifyCollectionChangedEventArgs e) private void addStatesOfAddedModules(NotifyCollectionChangedEventArgs e) { - var checkDefaultState = !States.Any() && e.NewItems!.Count == 1; - foreach (string moduleName in e.NewItems!) { var currentStateCopy = States.Select(clipState => clipState.Copy()).ToList(); @@ -287,12 +289,6 @@ private void addStatesOfAddedModules(NotifyCollectionChangedEventArgs e) States.AddRange(localCurrentStatesCopy); States.Add(new ClipState(newStateMetadata)); } - - if (checkDefaultState) - { - var defaultState = GetStateFor(moduleName, @"default"); - if (defaultState is not null) defaultState.Enabled.Value = true; - } } } diff --git a/VRCOSC.Game/ChatBox/Clips/ClipVariable.cs b/VRCOSC.Game/ChatBox/Clips/ClipVariable.cs index f0e6f71e..3a482e22 100644 --- a/VRCOSC.Game/ChatBox/Clips/ClipVariable.cs +++ b/VRCOSC.Game/ChatBox/Clips/ClipVariable.cs @@ -17,4 +17,5 @@ public class ClipVariableMetadata public required string Format { get; init; } public string DisplayableFormat => $"{variable_start_char}{Module.Replace("module", string.Empty)}.{Format}{variable_end_char}"; + public string DisplayableFormatWithSuffix(string suffix) => $"{variable_start_char}{Module.Replace("module", string.Empty)}.{Format}_{suffix}{variable_end_char}"; } diff --git a/VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs b/VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs index 142bccf3..88674d8e 100644 --- a/VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs +++ b/VRCOSC.Game/ChatBox/Serialisation/V1/TimelineSerialiser.cs @@ -2,9 +2,11 @@ // See the LICENSE file in the repository root for full license text. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using osu.Framework.Platform; +using VRCOSC.Game.ChatBox.Clips; using VRCOSC.Game.ChatBox.Serialisation.V1.Structures; using VRCOSC.Game.Graphics.Notifications; using VRCOSC.Game.Managers; @@ -25,8 +27,6 @@ public TimelineSerialiser(Storage storage, NotificationContainer notification, C protected override void ExecuteAfterDeserialisation(ChatBoxManager chatBoxManager, SerialisableTimeline data) { - chatBoxManager.Clips.Clear(); - data.Clips.ForEach(clip => { clip.AssociatedModules.ToImmutableList().ForEach(moduleName => @@ -54,6 +54,8 @@ protected override void ExecuteAfterDeserialisation(ChatBoxManager chatBoxManage }); }); + var createdClips = new List(); + data.Clips.ForEach(clip => { var newClip = chatBoxManager.CreateClip(); @@ -85,9 +87,10 @@ protected override void ExecuteAfterDeserialisation(ChatBoxManager chatBoxManage eventData.Length.Value = clipEvent.Length; }); - chatBoxManager.Clips.Add(newClip); + createdClips.Add(newClip); }); + chatBoxManager.Clips.ReplaceItems(createdClips); chatBoxManager.SetTimelineLength(TimeSpan.FromTicks(data.Ticks)); } } diff --git a/VRCOSC.Game/Extensions.cs b/VRCOSC.Game/Extensions.cs index 22a367a0..5777c566 100644 --- a/VRCOSC.Game/Extensions.cs +++ b/VRCOSC.Game/Extensions.cs @@ -2,6 +2,9 @@ // See the LICENSE file in the repository root for full license text. using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; namespace VRCOSC.Game; @@ -66,3 +69,17 @@ public static class Colour4Extensions colour.A ); } + +public static class BindableListExtensions +{ + public static void ReplaceItems(this BindableList source, IEnumerable items) => source.ReplaceRange(0, source.Count, items); +} + +public static class AssemblyExtensions +{ + public static T? GetAssemblyAttribute(this System.Reflection.Assembly ass) where T : Attribute + { + var attributes = ass.GetCustomAttributes(typeof(T), false); + return attributes.Length == 0 ? null : attributes.OfType().SingleOrDefault(); + } +} diff --git a/VRCOSC.Game/Github/GitHubProvider.cs b/VRCOSC.Game/Github/GitHubProvider.cs new file mode 100644 index 00000000..a90087d9 --- /dev/null +++ b/VRCOSC.Game/Github/GitHubProvider.cs @@ -0,0 +1,26 @@ +// 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.Threading.Tasks; +using Octokit; + +namespace VRCOSC.Game.Github; + +public class GitHubProvider +{ + private readonly GitHubClient client; + + public GitHubProvider(string appName) + { + client = new GitHubClient(new ProductHeaderValue(appName)); + } + + public Task GetLatestReleaseFor(Uri repoUrl) + { + var userName = repoUrl.Segments[^2].TrimEnd(new[] { '/' }); + var repoName = repoUrl.Segments[^1].TrimEnd(new[] { '/' }); + + return client.Repository.Release.GetLatest(userName, repoName); + } +} diff --git a/VRCOSC.Game/Graphics/About/AboutHeader.cs b/VRCOSC.Game/Graphics/About/AboutHeader.cs index 1bd2b600..d6b3b8e3 100644 --- a/VRCOSC.Game/Graphics/About/AboutHeader.cs +++ b/VRCOSC.Game/Graphics/About/AboutHeader.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Platform; using VRCOSC.Game.Config; using VRCOSC.Game.Graphics.Screen; @@ -10,12 +11,15 @@ namespace VRCOSC.Game.Graphics.About; public partial class AboutHeader : BaseHeader { + [Resolved] + private GameHost host { get; set; } = null!; + [Resolved] private VRCOSCConfigManager configManager { get; set; } = null!; private Bindable versionBindable = null!; - protected override string Title => $"VRCOSC {versionBindable.Value}"; + protected override string Title => $"{host.Name} {versionBindable.Value}"; protected override string SubTitle => "Copyright VolcanicArts 2023. See license file in repository root for more information"; diff --git a/VRCOSC.Game/Graphics/About/AboutScreen.cs b/VRCOSC.Game/Graphics/About/AboutScreen.cs index 36ced99d..9e97507d 100644 --- a/VRCOSC.Game/Graphics/About/AboutScreen.cs +++ b/VRCOSC.Game/Graphics/About/AboutScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Platform; using osuTK; using VRCOSC.Game.Graphics.Screen; +using VRCOSC.Game.Graphics.Themes; using VRCOSC.Game.Graphics.UI.Button; namespace VRCOSC.Game.Graphics.About; @@ -34,7 +35,7 @@ public sealed partial class AboutScreen : BaseScreen Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Both, Size = new Vector2(0.5f, 0.9f), - Direction = FillDirection.Full, + Direction = FillDirection.Vertical, Spacing = new Vector2(5) } } @@ -45,19 +46,40 @@ private void load() { RelativeSizeAxes = Axes.Both; - buttonFlow.AddRange(new[] + buttonFlow.AddRange(new Drawable[] { - new AboutButton + new FillFlowContainer { - Icon = FontAwesome.Brands.Github, - BackgroundColour = Colour4.FromHex("272b33"), - Action = () => host.OpenUrlExternally("https://github.com/VolcanicArts/VRCOSC") + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new AboutButton + { + Icon = FontAwesome.Brands.Github, + BackgroundColour = Colour4.FromHex("272b33"), + Action = () => host.OpenUrlExternally("https://github.com/VolcanicArts/VRCOSC") + }, + new AboutButton + { + Icon = FontAwesome.Brands.Discord, + BackgroundColour = Colour4.FromHex("7289DA"), + Action = () => host.OpenUrlExternally("https://discord.gg/vj4brHyvT5") + } + } }, - new AboutButton + new TextButton { - Icon = FontAwesome.Brands.Discord, - BackgroundColour = Colour4.FromHex("7289DA"), - Action = () => host.OpenUrlExternally("https://discord.gg/vj4brHyvT5") + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(150, 50), + BackgroundColour = ThemeManager.Current[ThemeAttribute.Action], + CornerRadius = 5, + Text = "Donate", + Action = () => host.OpenUrlExternally("https://ko-fi.com/volcanicarts") } }); } @@ -81,7 +103,6 @@ private void load() Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Size = new Vector2(0.9f), - Masking = true, CornerRadius = 10, Icon = Icon, BackgroundColour = BackgroundColour, diff --git a/VRCOSC.Game/Graphics/ChatBox/ChatBoxScreen.cs b/VRCOSC.Game/Graphics/ChatBox/ChatBoxScreen.cs index 6f4d150b..507b13b8 100644 --- a/VRCOSC.Game/Graphics/ChatBox/ChatBoxScreen.cs +++ b/VRCOSC.Game/Graphics/ChatBox/ChatBoxScreen.cs @@ -5,11 +5,16 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osuTK; using VRCOSC.Game.Graphics.ChatBox.SelectedClip; using VRCOSC.Game.Graphics.ChatBox.Timeline; using VRCOSC.Game.Graphics.ChatBox.Timeline.Menu.Clip; using VRCOSC.Game.Graphics.ChatBox.Timeline.Menu.Layer; using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Button; +using osu.Framework.Platform; +using VRCOSC.Game.Managers; +using VRCOSC.Game.Processes; namespace VRCOSC.Game.Graphics.ChatBox; @@ -63,12 +68,33 @@ private void load() null, new Drawable[] { - new TimelineLengthContainer + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = 300, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Spacing = new Vector2(5, 0), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ImportButton(), + new ExportButton() + } + }, + new TimelineLengthContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 300, + } + } } }, null, @@ -88,4 +114,53 @@ private void load() clipMenu }; } + + private partial class ImportButton : TextButton + { + [Resolved] + private ChatBoxManager chatBoxManager { get; set; } = null!; + + public ImportButton() + { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + Size = new Vector2(110, 30); + Text = "Import Config"; + FontSize = 18; + CornerRadius = 5; + BackgroundColour = ThemeManager.Current[ThemeAttribute.Action]; + BorderThickness = 2; + } + + protected override void LoadComplete() + { + Action += () => WinForms.OpenFileDialog(@"chatbox.json|*.json", fileName => Schedule(() => chatBoxManager.Import(fileName))); + } + } + + private partial class ExportButton : TextButton + { + [Resolved] + private GameHost host { get; set; } = null!; + + [Resolved] + private Storage storage { get; set; } = null!; + + public ExportButton() + { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + Size = new Vector2(110, 30); + Text = "Export Config"; + FontSize = 18; + CornerRadius = 5; + BackgroundColour = ThemeManager.Current[ThemeAttribute.Action]; + BorderThickness = 2; + } + + protected override void LoadComplete() + { + Action += () => host.PresentFileExternally(storage.GetFullPath(@"chatbox.json")); + } + } } diff --git a/VRCOSC.Game/Graphics/ChatBox/Metadata/MetadataToggle.cs b/VRCOSC.Game/Graphics/ChatBox/Metadata/MetadataToggle.cs index e9826ef8..87e66484 100644 --- a/VRCOSC.Game/Graphics/ChatBox/Metadata/MetadataToggle.cs +++ b/VRCOSC.Game/Graphics/ChatBox/Metadata/MetadataToggle.cs @@ -70,10 +70,7 @@ private void load() Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, State = State, - Masking = true, - CornerRadius = 5, BorderThickness = 2, - BorderColour = ThemeManager.Current[ThemeAttribute.Border], ShouldAnimate = false } } diff --git a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableAssociatedModule.cs b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableAssociatedModule.cs index 14603b67..f0e05c71 100644 --- a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableAssociatedModule.cs +++ b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableAssociatedModule.cs @@ -74,7 +74,6 @@ private void load() State = State.GetBoundCopy(), BorderThickness = 2, ShouldAnimate = false, - BorderColour = ThemeManager.Current[ThemeAttribute.Border] } } } diff --git a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableEvent.cs b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableEvent.cs index 3213d63a..6f25a096 100644 --- a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableEvent.cs +++ b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableEvent.cs @@ -70,7 +70,6 @@ private void load() RelativeSizeAxes = Axes.Both, State = ClipEvent.Enabled.GetBoundCopy(), BorderThickness = 2, - BorderColour = ThemeManager.Current[ThemeAttribute.Border], ShouldAnimate = false } }, @@ -117,7 +116,8 @@ private void load() Masking = true, CornerRadius = 5, BorderThickness = 2, - BorderColour = ThemeManager.Current[ThemeAttribute.Border] + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + UnicodeSupport = true } }, null, diff --git a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableState.cs b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableState.cs index 5e2b2472..bf0a117c 100644 --- a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableState.cs +++ b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/DrawableState.cs @@ -83,7 +83,6 @@ private void load() RelativeSizeAxes = Axes.Both, State = ClipState.Enabled.GetBoundCopy(), BorderThickness = 2, - BorderColour = ThemeManager.Current[ThemeAttribute.Border], ShouldAnimate = false } }, @@ -114,7 +113,8 @@ private void load() Masking = true, CornerRadius = 5, BorderThickness = 2, - BorderColour = ThemeManager.Current[ThemeAttribute.Border] + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + UnicodeSupport = true } } } diff --git a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipEditorWrapper.cs b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipEditorWrapper.cs index d3f40ead..390c252f 100644 --- a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipEditorWrapper.cs +++ b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipEditorWrapper.cs @@ -86,13 +86,13 @@ private void load() RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { - new Dimension(GridSizeMode.Relative, 0.15f), + new Dimension(GridSizeMode.Relative, 0.15f, 0, 200), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(GridSizeMode.Relative, 0.15f), + new Dimension(GridSizeMode.Relative, 0.15f, 0, 200), new Dimension(GridSizeMode.Absolute, 5), new Dimension(), new Dimension(GridSizeMode.Absolute, 5), - new Dimension(GridSizeMode.Relative, 0.2f), + new Dimension(GridSizeMode.Relative, 0.2f, 0, 300), }, Content = new[] { diff --git a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipModuleSelector.cs b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipModuleSelector.cs index 9591f50e..acf2ae66 100644 --- a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipModuleSelector.cs +++ b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipModuleSelector.cs @@ -99,7 +99,7 @@ private void load() moduleFlow.Clear(); - foreach (var module in gameManager.ModuleManager.Where(module => module.GetType().IsSubclassOf(typeof(ChatBoxModule)))) + foreach (var module in gameManager.ModuleManager.Modules.Where(module => module.GetType().IsSubclassOf(typeof(ChatBoxModule)))) { DrawableAssociatedModule drawableAssociatedModule; diff --git a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipStateEditorContainer.cs b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipStateEditorContainer.cs index 287403da..bd4d3865 100644 --- a/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipStateEditorContainer.cs +++ b/VRCOSC.Game/Graphics/ChatBox/SelectedClip/SelectedClipStateEditorContainer.cs @@ -197,6 +197,7 @@ private void load() { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Font = FrameworkFont.Regular.With(size: 20), Text = "Show relevant states only (Based on enabled modules)", Colour = ThemeManager.Current[ThemeAttribute.SubText] } diff --git a/VRCOSC.Game/Graphics/ChatBox/Timeline/DrawableClip.cs b/VRCOSC.Game/Graphics/ChatBox/Timeline/DrawableClip.cs index 7fd14a7b..99415797 100644 --- a/VRCOSC.Game/Graphics/ChatBox/Timeline/DrawableClip.cs +++ b/VRCOSC.Game/Graphics/ChatBox/Timeline/DrawableClip.cs @@ -57,28 +57,36 @@ private void load() Colour = ThemeManager.Current[ThemeAttribute.Dark], RelativeSizeAxes = Axes.Both }, - new StartResizeDetector(Clip) - { - RelativeSizeAxes = Axes.Y, - Width = 15 - }, - new EndResizeDetector(Clip) - { - RelativeSizeAxes = Axes.Y, - Width = 15 - }, new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10), + Padding = new MarginPadding(2), Children = new Drawable[] { - drawName = new SpriteText + new StartResizeDetector(Clip) + { + RelativeSizeAxes = Axes.Y, + Width = 12 + }, + new EndResizeDetector(Clip) + { + RelativeSizeAxes = Axes.Y, + Width = 12 + }, + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = FrameworkFont.Regular.With(size: 20), - Colour = ThemeManager.Current[ThemeAttribute.Text] + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + drawName = new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = FrameworkFont.Regular.With(size: 20), + Colour = ThemeManager.Current[ThemeAttribute.Text] + } + } } } } diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/AttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/AttributeCard.cs index d161a420..19a8933f 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/AttributeCard.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/AttributeCard.cs @@ -1,7 +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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -9,22 +8,21 @@ using osuTK; using VRCOSC.Game.Graphics.Themes; using VRCOSC.Game.Graphics.UI.Button; -using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes; -public abstract partial class AttributeCard : Container +public abstract partial class AttributeCard : Container where T : ModuleAttribute { private readonly VRCOSCButton resetToDefault; protected override FillFlowContainer Content { get; } - public readonly ModuleAttribute AttributeData; - public bool Enable { get; set; } = true; + protected readonly T AttributeData; - protected override bool ShouldBeConsideredForInput(Drawable child) => Enable; + protected override bool ShouldBeConsideredForInput(Drawable child) => AttributeData.Enabled; - protected AttributeCard(ModuleAttribute attributeData) + protected AttributeCard(T attributeData) { AttributeData = attributeData; @@ -49,9 +47,8 @@ protected AttributeCard(ModuleAttribute attributeData) Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Action = SetDefault, + Action = setDefault, BorderThickness = 2, - BorderColour = ThemeManager.Current[ThemeAttribute.Border], BackgroundColour = ThemeManager.Current[ThemeAttribute.Action], Icon = FontAwesome.Solid.Undo, IconPadding = 7, @@ -116,50 +113,30 @@ protected AttributeCard(ModuleAttribute attributeData) } }; - textFlow.AddText(AttributeData.Metadata.DisplayName, t => + textFlow.AddText(AttributeData.Name, t => { t.Font = FrameworkFont.Regular.With(size: 25); t.Colour = ThemeManager.Current[ThemeAttribute.Text]; }); - textFlow.AddParagraph(AttributeData.Metadata.Description, t => + textFlow.AddParagraph(AttributeData.Description, t => { t.Font = FrameworkFont.Regular.With(size: 20); t.Colour = ThemeManager.Current[ThemeAttribute.SubText]; }); } - protected override void LoadComplete() + protected override void Update() { - AttributeData.Attribute.BindValueChanged(onAttributeUpdate, true); + resetToDefault.FadeTo(AttributeData.IsDefault() ? 0 : 1, 200, Easing.OutQuart); + this.FadeTo(AttributeData.Enabled ? 1 : 0.5f, 150, Easing.OutQuart); } - protected virtual void SetDefault() + private void setDefault() { - AttributeData.Attribute.SetDefault(); + AttributeData.SetDefault(); + OnSetDefault(); } - protected void UpdateResetToDefault(bool show) - { - resetToDefault.FadeTo(show ? 1 : 0, 200, Easing.OutQuart); - } - - protected void UpdateAttribute(object value) - { - //Specifically check for equal values here to stop memory allocations from setting the value - if (value == AttributeData.Attribute.Value) return; - - AttributeData.Attribute.Value = value; - } - - private void onAttributeUpdate(ValueChangedEvent e) - { - UpdateResetToDefault(!AttributeData.Attribute.IsDefault); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - AttributeData.Attribute.ValueChanged -= onAttributeUpdate; - } + protected virtual void OnSetDefault() { } } diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/AttributeCardList.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/AttributeCardList.cs new file mode 100644 index 00000000..41cf6a71 --- /dev/null +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/AttributeCardList.cs @@ -0,0 +1,161 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Collections.Specialized; +using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osuTK; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Button; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes; + +public abstract partial class AttributeCardList : AttributeCard where TAttribute : ModuleAttributeList +{ + private FillFlowContainer listFlow = null!; + + protected AttributeCardList(TAttribute attributeData) + : base(attributeData) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Content.Add(listFlow = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5), + AutoSizeEasing = Easing.OutQuint, + AutoSizeDuration = 150, + LayoutEasing = Easing.OutQuint, + LayoutDuration = 150 + }); + } + + protected override void LoadComplete() + { + AttributeData.Attribute.CollectionChanged += attributeOnCollectionChanged; + addAdditionIcon(); + AttributeData.Attribute.ForEach(OnInstanceAdd); + } + + private void attributeOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems is not null) + { + foreach (TInstance newInstance in e.NewItems) + { + OnInstanceAdd(newInstance); + } + } + } + + protected override void OnSetDefault() + { + listFlow.RemoveAll(d => d.GetType() == typeof(GridContainer), true); + AttributeData.Attribute.ForEach(OnInstanceAdd); + } + + private void addInstance() + { + AttributeData.Attribute.Add(CreateInstance()); + } + + protected void AddToFlow(Drawable drawable) + { + GridContainer gridInstance; + + var position = listFlow[^1].Position; + + listFlow.Add(gridInstance = new GridContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Position = position, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(GridSizeMode.Absolute, 30) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + } + }); + + gridInstance.Content = new[] + { + new[] + { + drawable, + null, + new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Scale = new Vector2(0.9f), + CornerRadius = 5, + BackgroundColour = ThemeManager.Current[ThemeAttribute.Failure], + Icon = FontAwesome.Solid.Get(0xf00d), + IconPadding = 4, + IconShadow = true, + Action = () => + { + AttributeData.Attribute.RemoveAt(listFlow.IndexOf(gridInstance) - 1); + gridInstance.RemoveAndDisposeImmediately(); + } + } + } + }; + } + + private void addAdditionIcon() + { + var iconWrapper = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Width = 0.8f, + Child = new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Icon = FontAwesome.Solid.Plus, + BackgroundColour = ThemeManager.Current[ThemeAttribute.Success], + Circular = true, + IconShadow = true, + IconPadding = 6, + Action = addInstance + } + }; + + listFlow.Add(iconWrapper); + listFlow.SetLayoutPosition(iconWrapper, float.MaxValue); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + AttributeData.Attribute.CollectionChanged -= attributeOnCollectionChanged; + } + + protected abstract void OnInstanceAdd(TInstance instance); + protected abstract TInstance CreateInstance(); +} diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Dropdown/DropdownAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Dropdown/DropdownAttributeCard.cs index edbfbdd8..6e4d2108 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Dropdown/DropdownAttributeCard.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Dropdown/DropdownAttributeCard.cs @@ -6,15 +6,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using VRCOSC.Game.Graphics.UI; -using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Dropdown; -public sealed partial class DropdownAttributeCard : AttributeCard where T : Enum +public sealed partial class DropdownAttributeCard : AttributeCard> where T : Enum { - private VRCOSCDropdown dropdown = null!; - - public DropdownAttributeCard(ModuleAttribute attributeData) + public DropdownAttributeCard(ModuleEnumAttribute attributeData) : base(attributeData) { } @@ -22,25 +20,13 @@ public DropdownAttributeCard(ModuleAttribute attributeData) [BackgroundDependencyLoader] private void load() { - Add(dropdown = new VRCOSCDropdown + Add(new VRCOSCDropdown { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, Items = Enum.GetValues(typeof(T)).Cast(), - Current = { Value = (T)AttributeData.Attribute.Value } + Current = AttributeData.Attribute.GetBoundCopy() }); } - - protected override void LoadComplete() - { - base.LoadComplete(); - dropdown.Current.ValueChanged += e => UpdateAttribute(e.NewValue); - } - - protected override void SetDefault() - { - base.SetDefault(); - dropdown.Current.Value = (T)AttributeData.Attribute.Value; - } } diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/FloatSliderAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/FloatSliderAttributeCard.cs index 14c7cb52..bc8cbb2b 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/FloatSliderAttributeCard.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/FloatSliderAttributeCard.cs @@ -2,22 +2,16 @@ // See the LICENSE file in the repository root for full license text. using osu.Framework.Bindables; -using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Slider; -public sealed partial class FloatSliderAttributeCard : SliderAttributeCard +public sealed partial class FloatSliderAttributeCard : SliderAttributeCard { - public FloatSliderAttributeCard(ModuleAttributeWithBounds attributeData) + public FloatSliderAttributeCard(ModuleFloatRangeAttribute attributeData) : base(attributeData) { } - protected override Bindable CreateCurrent() => new BindableNumber - { - MinValue = (float)AttributeDataWithBounds.MinValue, - MaxValue = (float)AttributeDataWithBounds.MaxValue, - Precision = 0.01f, - Value = (float)AttributeData.Attribute.Value - }; + protected override BindableNumber CreateCurrent() => (BindableNumber)AttributeData.Attribute; } diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/IntSliderAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/IntSliderAttributeCard.cs index b2eafe79..30e70f4c 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/IntSliderAttributeCard.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/IntSliderAttributeCard.cs @@ -2,22 +2,16 @@ // See the LICENSE file in the repository root for full license text. using osu.Framework.Bindables; -using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Slider; -public sealed partial class IntSliderAttributeCard : SliderAttributeCard +public sealed partial class IntSliderAttributeCard : SliderAttributeCard { - public IntSliderAttributeCard(ModuleAttributeWithBounds attributeData) + public IntSliderAttributeCard(ModuleIntRangeAttribute attributeData) : base(attributeData) { } - protected override Bindable CreateCurrent() => new BindableNumber - { - MinValue = (int)AttributeDataWithBounds.MinValue, - MaxValue = (int)AttributeDataWithBounds.MaxValue, - Precision = 1, - Value = (int)AttributeData.Attribute.Value - }; + protected override BindableNumber CreateCurrent() => (BindableNumber)AttributeData.Attribute; } diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/SliderAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/SliderAttributeCard.cs index 218bf15b..b6dee0ad 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/SliderAttributeCard.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Slider/SliderAttributeCard.cs @@ -6,46 +6,29 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using VRCOSC.Game.Graphics.UI; -using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Slider; -public abstract partial class SliderAttributeCard : AttributeCard where T : struct, IComparable, IConvertible, IEquatable +public abstract partial class SliderAttributeCard : AttributeCard where TValue : struct, IComparable, IConvertible, IEquatable where TAttribute : ModuleAttribute { - protected ModuleAttributeWithBounds AttributeDataWithBounds; - - private VRCOSCSlider slider = null!; - - protected SliderAttributeCard(ModuleAttributeWithBounds attributeData) + protected SliderAttributeCard(TAttribute attributeData) : base(attributeData) { - AttributeDataWithBounds = attributeData; } [BackgroundDependencyLoader] private void load() { - Add(slider = new VRCOSCSlider + Add(new VRCOSCSlider { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, Height = 30, - Current = CreateCurrent() + RoudedCurrent = CreateCurrent().GetBoundCopy() }); } - protected override void LoadComplete() - { - base.LoadComplete(); - slider.Current.ValueChanged += e => UpdateAttribute(e.NewValue); - } - - protected override void SetDefault() - { - base.SetDefault(); - slider.Current.Value = (T)AttributeData.Attribute.Value; - } - - protected abstract Bindable CreateCurrent(); + protected abstract BindableNumber CreateCurrent(); } diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/TextAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/FloatTextAttributeCard.cs similarity index 54% rename from VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/TextAttributeCard.cs rename to VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/FloatTextAttributeCard.cs index e8088318..c780ffe6 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/TextAttributeCard.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/FloatTextAttributeCard.cs @@ -1,19 +1,18 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. +using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Graphics; using VRCOSC.Game.Graphics.Themes; using VRCOSC.Game.Graphics.UI.Text; -using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Text; -public partial class TextAttributeCard : AttributeCard where TTextBox : ValidationTextBox, new() +public partial class FloatTextAttributeCard : AttributeCard { - private TTextBox textBox = null!; - - public TextAttributeCard(ModuleAttribute attributeData) + public FloatTextAttributeCard(ModuleFloatAttribute attributeData) : base(attributeData) { } @@ -21,7 +20,7 @@ public TextAttributeCard(ModuleAttribute attributeData) [BackgroundDependencyLoader] private void load() { - Add(textBox = new TTextBox + Add(new FloatTextBox { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -31,19 +30,8 @@ private void load() CornerRadius = 5, BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, - Text = AttributeData.Attribute.Value.ToString() + Text = AttributeData.Attribute.Value.ToString(CultureInfo.CurrentCulture), + ValidCurrent = AttributeData.Attribute.GetBoundCopy() }); } - - protected override void LoadComplete() - { - base.LoadComplete(); - textBox.OnValidEntry += entry => UpdateAttribute(entry!); - } - - protected override void SetDefault() - { - base.SetDefault(); - textBox.Current.Value = AttributeData.Attribute.Value.ToString(); - } } diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/IntTextAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/IntTextAttributeCard.cs new file mode 100644 index 00000000..0d79c1f9 --- /dev/null +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/IntTextAttributeCard.cs @@ -0,0 +1,36 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Text; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Text; + +public partial class IntTextAttributeCard : AttributeCard +{ + public IntTextAttributeCard(ModuleIntAttribute attributeData) + : base(attributeData) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Add(new IntTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + Text = AttributeData.Attribute.Value.ToString(), + ValidCurrent = AttributeData.Attribute.GetBoundCopy() + }); + } +} diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/MutableKeyValuePairAttributeCardList.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/MutableKeyValuePairAttributeCardList.cs new file mode 100644 index 00000000..ed663111 --- /dev/null +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/MutableKeyValuePairAttributeCardList.cs @@ -0,0 +1,74 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Text; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Text; + +public partial class MutableKeyValuePairAttributeCardList : AttributeCardList +{ + public MutableKeyValuePairAttributeCardList(MutableKeyValuePairListAttribute attributeData) + : base(attributeData) + { + } + + protected override void OnInstanceAdd(MutableKeyValuePair instance) + { + AddToFlow(new GridContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.25f), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable?[] + { + new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + ValidCurrent = instance.Key.GetBoundCopy(), + PlaceholderText = AttributeData.KeyPlaceholder + }, + null, + new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + ValidCurrent = instance.Value.GetBoundCopy(), + PlaceholderText = AttributeData.ValuePlaceholder + } + } + } + }); + } + + protected override MutableKeyValuePair CreateInstance() => new(); +} diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextAttributeCard.cs new file mode 100644 index 00000000..dc49d23e --- /dev/null +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextAttributeCard.cs @@ -0,0 +1,36 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Text; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Text; + +public partial class StringTextAttributeCard : AttributeCard +{ + public StringTextAttributeCard(ModuleStringAttribute attributeData) + : base(attributeData) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Add(new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + Text = AttributeData.Attribute.Value, + ValidCurrent = AttributeData.Attribute.GetBoundCopy() + }); + } +} diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextAttributeCardList.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextAttributeCardList.cs new file mode 100644 index 00000000..fb6209f1 --- /dev/null +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextAttributeCardList.cs @@ -0,0 +1,37 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Text; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Text; + +public partial class StringTextAttributeCardList : AttributeCardList> +{ + public StringTextAttributeCardList(ModuleStringListAttribute attributeData) + : base(attributeData) + { + } + + protected override void OnInstanceAdd(Bindable instance) + { + AddToFlow(new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + Text = instance.Value, + ValidCurrent = instance.GetBoundCopy() + }); + } + + protected override Bindable CreateInstance() => new(string.Empty); +} diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/ButtonStringAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextWithButtonAttributeCard.cs similarity index 57% rename from VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/ButtonStringAttributeCard.cs rename to VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextWithButtonAttributeCard.cs index b527824b..a1849300 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/ButtonStringAttributeCard.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Text/StringTextWithButtonAttributeCard.cs @@ -7,23 +7,34 @@ using VRCOSC.Game.Graphics.Themes; using VRCOSC.Game.Graphics.UI.Button; using VRCOSC.Game.Graphics.UI.Text; -using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Text; -public sealed partial class ButtonStringAttributeCard : TextAttributeCard +public partial class StringTextWithButtonAttributeCard : AttributeCard { - private readonly ModuleAttributeWithButton attributeWithButton; - - public ButtonStringAttributeCard(ModuleAttributeWithButton attributeData) + public StringTextWithButtonAttributeCard(ModuleStringWithButtonAttribute attributeData) : base(attributeData) { - attributeWithButton = attributeData; } [BackgroundDependencyLoader] private void load() { + Add(new StringTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = 30, + Masking = true, + CornerRadius = 5, + BorderColour = ThemeManager.Current[ThemeAttribute.Border], + BorderThickness = 2, + Text = AttributeData.Attribute.Value, + ValidCurrent = AttributeData.Attribute.GetBoundCopy() + }); + Add(new Container { Anchor = Anchor.TopCentre, @@ -36,11 +47,10 @@ private void load() Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Text = attributeWithButton.ButtonText, - Masking = true, + Text = AttributeData.ButtonText, CornerRadius = 5, FontSize = 22, - Action = attributeWithButton.ButtonAction, + Action = AttributeData.ButtonCallback, BackgroundColour = ThemeManager.Current[ThemeAttribute.Action] } }); diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Toggle/BoolAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Toggle/BoolAttributeCard.cs new file mode 100644 index 00000000..67bf7a7f --- /dev/null +++ b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Toggle/BoolAttributeCard.cs @@ -0,0 +1,33 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osuTK; +using VRCOSC.Game.Graphics.UI.Button; +using VRCOSC.Game.Modules.Attributes; + +namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Toggle; + +public sealed partial class BoolAttributeCard : AttributeCard +{ + public BoolAttributeCard(ModuleBoolAttribute attributeData) + : base(attributeData) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Add(new ToggleButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(35), + CornerRadius = 10, + BorderThickness = 2, + ShouldAnimate = false, + State = AttributeData.Attribute.GetBoundCopy() + }); + } +} diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Toggle/ToggleAttributeCard.cs b/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Toggle/ToggleAttributeCard.cs deleted file mode 100644 index 2ba122c3..00000000 --- a/VRCOSC.Game/Graphics/ModuleAttributes/Attributes/Toggle/ToggleAttributeCard.cs +++ /dev/null @@ -1,49 +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 osu.Framework.Allocation; -using osu.Framework.Graphics; -using osuTK; -using VRCOSC.Game.Graphics.Themes; -using VRCOSC.Game.Graphics.UI.Button; -using VRCOSC.Game.Modules; - -namespace VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Toggle; - -public sealed partial class ToggleAttributeCard : AttributeCard -{ - private ToggleButton toggleButton = null!; - - public ToggleAttributeCard(ModuleAttribute attributeData) - : base(attributeData) - { - } - - [BackgroundDependencyLoader] - private void load() - { - Add(toggleButton = new ToggleButton - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(35), - CornerRadius = 10, - BorderColour = ThemeManager.Current[ThemeAttribute.Border], - BorderThickness = 2, - ShouldAnimate = false, - State = { Value = (bool)AttributeData.Attribute.Value } - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - toggleButton.State.ValueChanged += e => UpdateAttribute(e.NewValue); - } - - protected override void SetDefault() - { - base.SetDefault(); - toggleButton.State.Value = (bool)AttributeData.Attribute.Value; - } -} diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/ModuleAttributeFlow.cs b/VRCOSC.Game/Graphics/ModuleAttributes/ModuleAttributeFlow.cs index b2f6ef78..f4fb2c72 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/ModuleAttributeFlow.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/ModuleAttributeFlow.cs @@ -1,23 +1,16 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osuTK; -using VRCOSC.Game.Graphics.ModuleAttributes.Attributes; -using VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Dropdown; -using VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Slider; -using VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Text; -using VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Toggle; using VRCOSC.Game.Graphics.Themes; -using VRCOSC.Game.Graphics.UI.Text; -using VRCOSC.Game.Modules; +using VRCOSC.Game.Graphics.UI; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleAttributes; @@ -26,7 +19,7 @@ public partial class ModuleAttributeFlow : Container private readonly string attributeName; public readonly BindableList AttributeList = new(); - private FillFlowContainer attributeFlow = null!; + private FillFlowContainer attributeFlow = null!; private TextFlowContainer noAttributesContainer = null!; public ModuleAttributeFlow(string attributeName) @@ -68,16 +61,15 @@ private void load() Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(2), - Child = new BasicScrollContainer + Child = new VRCOSCScrollContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, ClampExtension = 0, - ScrollbarVisible = false, ScrollContent = { - Child = attributeFlow = new FillFlowContainer + Child = attributeFlow = new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -103,78 +95,12 @@ protected override void LoadComplete() attributeFlow.Clear(); - foreach (ModuleAttribute newAttribute in e.NewItems) + foreach (ModuleAttribute moduleAttribute in e.NewItems) { - attributeFlow.Add(generateCard(newAttribute)); + attributeFlow.Add(moduleAttribute.GetAssociatedCard()); } noAttributesContainer.Alpha = attributeFlow.Any() ? 0 : 1; - - checkShouldDisplay(); }, true); } - - private void checkShouldDisplay() - { - attributeFlow.ForEach(card => - { - card.Enable = card.AttributeData.Enabled; - card.FadeTo(card.AttributeData.Enabled ? 1 : 0.25f, 250, Easing.OutQuad); - }); - } - - private AttributeCard generateCard(ModuleAttribute attributeData) - { - var value = attributeData.Attribute.Value; - - if (value.GetType().IsSubclassOf(typeof(Enum))) - { - attributeData.Attribute.BindValueChanged(_ => checkShouldDisplay()); - Type instanceType = typeof(DropdownAttributeCard<>).MakeGenericType(value.GetType()); - return (Activator.CreateInstance(instanceType, attributeData) as AttributeCard)!; - } - - switch (attributeData) - { - case ModuleAttributeWithButton attributeSingleWithButton: - switch (value) - { - case string: - return new ButtonStringAttributeCard(attributeSingleWithButton); - - default: - throw new ArgumentOutOfRangeException(nameof(attributeSingleWithButton), "Cannot generate button with non-text counterpart"); - } - - case ModuleAttributeWithBounds attributeDataWithBounds: - switch (value) - { - case int: - return new IntSliderAttributeCard(attributeDataWithBounds); - - case float: - return new FloatSliderAttributeCard(attributeDataWithBounds); - - default: - throw new ArgumentOutOfRangeException(nameof(attributeDataWithBounds), "Cannot have bounds for a non-numeric value"); - } - - default: - switch (value) - { - case string: - return new TextAttributeCard(attributeData); - - case int: - return new TextAttributeCard(attributeData); - - case bool: - attributeData.Attribute.BindValueChanged(_ => checkShouldDisplay()); - return new ToggleAttributeCard(attributeData); - - default: - throw new ArgumentOutOfRangeException(nameof(attributeData), $"Type {value.GetType()} is not supported in the {nameof(ModuleAttributeFlow)}"); - } - } - } } diff --git a/VRCOSC.Game/Graphics/ModuleAttributes/ModuleAttributesScreen.cs b/VRCOSC.Game/Graphics/ModuleAttributes/ModuleAttributesScreen.cs index 618886be..57a58a13 100644 --- a/VRCOSC.Game/Graphics/ModuleAttributes/ModuleAttributesScreen.cs +++ b/VRCOSC.Game/Graphics/ModuleAttributes/ModuleAttributesScreen.cs @@ -36,7 +36,7 @@ public partial class ModuleAttributesScreen : BaseScreen { new Dimension(), new Dimension(GridSizeMode.Absolute, 5), - new Dimension() + new Dimension(GridSizeMode.Relative, 0.35f) }, Content = new[] { diff --git a/VRCOSC.Game/Graphics/ModuleInfo/DrawableInfoCard.cs b/VRCOSC.Game/Graphics/ModuleInfo/DrawableInfoCard.cs new file mode 100644 index 00000000..7fb29e93 --- /dev/null +++ b/VRCOSC.Game/Graphics/ModuleInfo/DrawableInfoCard.cs @@ -0,0 +1,116 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osuTK; +using VRCOSC.Game.Graphics.Themes; + +namespace VRCOSC.Game.Graphics.ModuleInfo; + +public partial class DrawableInfoCard : Container +{ + private readonly string infoString; + + public DrawableInfoCard(string infoString) + { + this.infoString = infoString; + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + BorderThickness = 2; + BorderColour = ThemeManager.Current[ThemeAttribute.Border]; + CornerRadius = 5; + Width = 0.75f; + Masking = true; + + Children = new Drawable[] + { + new Box + { + Colour = ThemeManager.Current[ThemeAttribute.Darker], + RelativeSizeAxes = Axes.Both + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(5), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 40), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension() + }, + Content = new[] + { + new Drawable?[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(ThemeManager.Current[ThemeAttribute.Pending].Darken(0.25f), ThemeManager.Current[ThemeAttribute.Pending]) + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(VRCOSCGraphicsContants.ONE_OVER_GOLDEN_RATIO), + Icon = FontAwesome.Solid.ExclamationTriangle, + Shadow = true + } + } + }, + null, + new TextFlowContainer(t => + { + t.Font = FrameworkFont.Regular.With(size: 20); + t.Colour = ThemeManager.Current[ThemeAttribute.Text]; + }) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextAnchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + AutoSizeEasing = Easing.OutQuint, + AutoSizeDuration = 150, + Text = infoString, + Masking = true + } + } + } + } + } + } + }; + } +} diff --git a/VRCOSC.Game/Graphics/ModuleInfo/DrawableParameterAttribute.cs b/VRCOSC.Game/Graphics/ModuleInfo/DrawableParameterAttribute.cs index 87bf6813..d08c0141 100644 --- a/VRCOSC.Game/Graphics/ModuleInfo/DrawableParameterAttribute.cs +++ b/VRCOSC.Game/Graphics/ModuleInfo/DrawableParameterAttribute.cs @@ -8,14 +8,15 @@ using osu.Framework.Graphics.Shapes; using VRCOSC.Game.Graphics.Themes; using VRCOSC.Game.Modules; +using VRCOSC.Game.Modules.Attributes; namespace VRCOSC.Game.Graphics.ModuleInfo; public partial class DrawableParameterAttribute : Container { - private readonly ParameterAttribute parameterAttribute; + private readonly ModuleParameter parameterAttribute; - public DrawableParameterAttribute(ParameterAttribute parameterAttribute) + public DrawableParameterAttribute(ModuleParameter parameterAttribute) { this.parameterAttribute = parameterAttribute; } @@ -49,13 +50,15 @@ private void load() } }; - textFlow.AddText($"{parameterAttribute.Metadata.DisplayName} - {parameterAttribute.Metadata.Description}", t => + textFlow.AddText($"{parameterAttribute.Name} - {parameterAttribute.Description}", t => { t.Font = FrameworkFont.Regular.With(size: 20); t.Colour = ThemeManager.Current[ThemeAttribute.Text]; }); - textFlow.AddParagraph($"\nName: {parameterAttribute.Name}", t => + textFlow.NewParagraph(); + + textFlow.AddParagraph($"Name: {parameterAttribute.ParameterName}", t => { t.Font = FrameworkFont.Regular.With(size: 17); t.Colour = ThemeManager.Current[ThemeAttribute.SubText]; diff --git a/VRCOSC.Game/Graphics/ModuleInfo/ModuleInfoScreen.cs b/VRCOSC.Game/Graphics/ModuleInfo/ModuleInfoScreen.cs index 022eb406..285b4283 100644 --- a/VRCOSC.Game/Graphics/ModuleInfo/ModuleInfoScreen.cs +++ b/VRCOSC.Game/Graphics/ModuleInfo/ModuleInfoScreen.cs @@ -1,19 +1,21 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; using VRCOSC.Game.Graphics.Screen; +using VRCOSC.Game.Graphics.UI; using VRCOSC.Game.Modules; namespace VRCOSC.Game.Graphics.ModuleInfo; public partial class ModuleInfoScreen : BaseScreen { + private FillFlowContainer infoFlow = null!; private FillFlowContainer parameterAttributeFlow = null!; [Resolved(name: "InfoModule")] @@ -21,26 +23,50 @@ public partial class ModuleInfoScreen : BaseScreen protected override BaseHeader CreateHeader() => new ModuleInfoHeader(); - protected override Drawable CreateBody() => new BasicScrollContainer + protected override Drawable CreateBody() => new VRCOSCScrollContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, ClampExtension = 0, - ScrollbarVisible = false, ScrollContent = { - Child = parameterAttributeFlow = new FillFlowContainer + Child = new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(5), - Spacing = new Vector2(5, 5), - Direction = FillDirection.Full, + Spacing = new Vector2(0, 10), + Direction = FillDirection.Vertical, LayoutEasing = Easing.OutQuad, - LayoutDuration = 150 + LayoutDuration = 150, + Children = new Drawable[] + { + infoFlow = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5), + Direction = FillDirection.Vertical, + LayoutEasing = Easing.OutQuad, + LayoutDuration = 150 + }, + parameterAttributeFlow = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5), + Direction = FillDirection.Vertical, + LayoutEasing = Easing.OutQuad, + LayoutDuration = 150 + } + } } } }; @@ -51,12 +77,14 @@ protected override void LoadComplete() { if (e.NewValue is null) return; + infoFlow.Clear(); parameterAttributeFlow.Clear(); - e.NewValue.Parameters.Values.ForEach(parameterAttribute => - { - parameterAttributeFlow.Add(new DrawableParameterAttribute(parameterAttribute)); - }); + infoFlow.AddRange(e.NewValue.Info.Select(infoString => new DrawableInfoCard(infoString))); + parameterAttributeFlow.AddRange(e.NewValue.Parameters.Values.Select(parameterAttribute => new DrawableParameterAttribute(parameterAttribute))); + + infoFlow.Alpha = infoFlow.Any() ? 1 : 0; + parameterAttributeFlow.Alpha = parameterAttributeFlow.Any() ? 1 : 0; }, true); } } diff --git a/VRCOSC.Game/Graphics/ModuleListing/DrawableModuleAssembly.cs b/VRCOSC.Game/Graphics/ModuleListing/DrawableModuleAssembly.cs new file mode 100644 index 00000000..75870a9f --- /dev/null +++ b/VRCOSC.Game/Graphics/ModuleListing/DrawableModuleAssembly.cs @@ -0,0 +1,61 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osuTK; +using VRCOSC.Game.Modules; + +namespace VRCOSC.Game.Graphics.ModuleListing; + +public partial class DrawableModuleAssembly : Container +{ + private readonly ModuleCollection moduleCollection; + private FillFlowContainer moduleFlow = null!; + + public DrawableModuleAssembly(ModuleCollection moduleCollection) + { + this.moduleCollection = moduleCollection; + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Children = new Drawable[] + { + moduleFlow = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5), + LayoutEasing = Easing.OutQuad, + LayoutDuration = 150, + Children = new Drawable[] + { + new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = FrameworkFont.Regular.With(size: 30), + Text = moduleCollection.Title + } + } + } + }; + } + + protected override void LoadComplete() + { + moduleFlow.AddRange(moduleCollection.Modules.Select(module => new ModuleCard(module))); + } +} diff --git a/VRCOSC.Game/Graphics/ModuleListing/ModuleCard.cs b/VRCOSC.Game/Graphics/ModuleListing/ModuleCard.cs index 0faf8295..3cad8b7d 100644 --- a/VRCOSC.Game/Graphics/ModuleListing/ModuleCard.cs +++ b/VRCOSC.Game/Graphics/ModuleListing/ModuleCard.cs @@ -137,7 +137,6 @@ public ModuleCard(Module module) RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Question, IconPadding = 5, - CornerRadius = 5, Action = () => infoModule.Value = Module, BackgroundColour = ThemeManager.Current[ThemeAttribute.Light] } @@ -155,7 +154,6 @@ public ModuleCard(Module module) RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Get(0xF013), IconPadding = 5, - CornerRadius = 5, Action = () => editingModule.Value = Module, BackgroundColour = ThemeManager.Current[ThemeAttribute.Light] } diff --git a/VRCOSC.Game/Graphics/ModuleListing/ModulesHeader.cs b/VRCOSC.Game/Graphics/ModuleListing/ModulesHeader.cs index cecb651f..84666738 100644 --- a/VRCOSC.Game/Graphics/ModuleListing/ModulesHeader.cs +++ b/VRCOSC.Game/Graphics/ModuleListing/ModulesHeader.cs @@ -1,12 +1,42 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osuTK; using VRCOSC.Game.Graphics.Screen; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Button; namespace VRCOSC.Game.Graphics.ModuleListing; public sealed partial class ModulesHeader : BaseHeader { + [Resolved] + private ModulesScreen modulesScreen { get; set; } = null!; + protected override string Title => "Modules"; - protected override string SubTitle => "Select and edit modules settings/parameters"; + protected override string SubTitle => "Select modules and edit settings/parameters"; + + // protected override Drawable CreateRightShoulder() => new Container + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // RelativeSizeAxes = Axes.Both, + // Child = new IconButton + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // RelativeSizeAxes = Axes.Both, + // FillMode = FillMode.Fit, + // Size = new Vector2(0.8f), + // Icon = FontAwesome.Solid.Download, + // IconShadow = true, + // CornerRadius = 10, + // BackgroundColour = ThemeManager.Current[ThemeAttribute.Action], + // Action = () => modulesScreen.ShowRepoListing() + // } + // }; } diff --git a/VRCOSC.Game/Graphics/ModuleListing/ModulesScreen.cs b/VRCOSC.Game/Graphics/ModuleListing/ModulesScreen.cs index 4c0c067d..d0c58a0c 100644 --- a/VRCOSC.Game/Graphics/ModuleListing/ModulesScreen.cs +++ b/VRCOSC.Game/Graphics/ModuleListing/ModulesScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; @@ -8,6 +9,7 @@ using osuTK; using VRCOSC.Game.Graphics.ModuleAttributes; using VRCOSC.Game.Graphics.ModuleInfo; +using VRCOSC.Game.Graphics.RepoListing; using VRCOSC.Game.Graphics.Screen; using VRCOSC.Game.Graphics.UI; using VRCOSC.Game.Managers; @@ -20,7 +22,8 @@ public sealed partial class ModulesScreen : BaseScreen [Resolved] private GameManager gameManager { get; set; } = null!; - private FillFlowContainer moduleCardFlow = null!; + private FillFlowContainer moduleFlow = null!; + private RepoListingPopover repoListing = null!; [BackgroundDependencyLoader] private void load() @@ -30,10 +33,16 @@ private void load() AddRange(new Drawable[] { new ModuleAttributesPopover(), - new ModuleInfoPopover() + new ModuleInfoPopover(), + repoListing = new RepoListingPopover() }); } + public void ShowRepoListing() + { + repoListing.Show(); + } + protected override BaseHeader CreateHeader() => new ModulesHeader(); protected override Drawable CreateBody() => new VRCOSCScrollContainer @@ -42,18 +51,17 @@ private void load() Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, ClampExtension = 0, - ScrollbarOverlapsContent = false, ScrollContent = { - Child = moduleCardFlow = new FillFlowContainer + Child = moduleFlow = new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(5), - Direction = FillDirection.Full, - Spacing = new Vector2(0, 5), + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), LayoutEasing = Easing.OutQuad, LayoutDuration = 150 } @@ -62,6 +70,17 @@ private void load() protected override void LoadComplete() { - gameManager.ModuleManager.ForEach(module => moduleCardFlow.Add(new ModuleCard(module))); + gameManager.ModuleManager.ModuleCollections.Values.Select(collection => new DrawableModuleAssembly(collection)).ForEach(drawableModuleAssembly => + { + moduleFlow.Add(drawableModuleAssembly); + + moduleFlow.Add(new LineSeparator + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }); + }); + + moduleFlow.Remove(moduleFlow.Last(), true); } } diff --git a/VRCOSC.Game/Graphics/RepoListing/DrawableRepoListing.cs b/VRCOSC.Game/Graphics/RepoListing/DrawableRepoListing.cs new file mode 100644 index 00000000..50cd240c --- /dev/null +++ b/VRCOSC.Game/Graphics/RepoListing/DrawableRepoListing.cs @@ -0,0 +1,58 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using VRCOSC.Game.Github; +using VRCOSC.Game.Graphics.Themes; + +namespace VRCOSC.Game.Graphics.RepoListing; + +public partial class DrawableRepoListing : Container +{ + [Resolved] + private GitHubProvider gitHubProvider { get; set; } = null!; + + private readonly Uri repoUrl; + + public DrawableRepoListing(Uri repoUrl) + { + this.repoUrl = repoUrl; + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Masking = true; + BorderThickness = 2; + BorderColour = ThemeManager.Current[ThemeAttribute.Border]; + CornerRadius = 5; + + Children = new Drawable[] + { + new Box + { + Colour = ThemeManager.Current[ThemeAttribute.Light], + RelativeSizeAxes = Axes.Both + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(7) + } + }; + } + + protected override async void LoadComplete() + { + var repoListing = await gitHubProvider.GetLatestReleaseFor(repoUrl); + } +} diff --git a/VRCOSC.Game/Graphics/RepoListing/RepoListingFlowContainer.cs b/VRCOSC.Game/Graphics/RepoListing/RepoListingFlowContainer.cs new file mode 100644 index 00000000..6dc7d51c --- /dev/null +++ b/VRCOSC.Game/Graphics/RepoListing/RepoListingFlowContainer.cs @@ -0,0 +1,70 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; +using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI; + +namespace VRCOSC.Game.Graphics.RepoListing; + +public partial class RepoListingFlowContainer : Container +{ + private FillFlowContainer repoFlow = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + Masking = true; + BorderThickness = 2; + BorderColour = ThemeManager.Current[ThemeAttribute.Border]; + CornerRadius = 10; + + Children = new Drawable[] + { + new Box + { + Colour = ThemeManager.Current[ThemeAttribute.Darker], + RelativeSizeAxes = Axes.Both + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(2), + Child = new VRCOSCScrollContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + ClampExtension = 0, + ScrollContent = + { + Child = repoFlow = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + LayoutDuration = 150, + LayoutEasing = Easing.OutQuint, + AutoSizeDuration = 150, + AutoSizeEasing = Easing.OutQuint, + Padding = new MarginPadding(5), + Spacing = new Vector2(0, 5) + } + } + } + } + }; + } + + protected override void LoadComplete() + { + repoFlow.Add(new DrawableRepoListing(new Uri("https://github.com/VolcanicArts/VRCOSC"))); + } +} diff --git a/VRCOSC.Game/Graphics/RepoListing/RepoListingHeader.cs b/VRCOSC.Game/Graphics/RepoListing/RepoListingHeader.cs new file mode 100644 index 00000000..35822614 --- /dev/null +++ b/VRCOSC.Game/Graphics/RepoListing/RepoListingHeader.cs @@ -0,0 +1,12 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using VRCOSC.Game.Graphics.Screen; + +namespace VRCOSC.Game.Graphics.RepoListing; + +public partial class RepoListingHeader : BaseHeader +{ + protected override string Title => "Module Repositories"; + protected override string SubTitle => "Here you can add repositories that publish module DLLs to be automatically updated"; +} diff --git a/VRCOSC.Game/Graphics/RepoListing/RepoListingPopover.cs b/VRCOSC.Game/Graphics/RepoListing/RepoListingPopover.cs new file mode 100644 index 00000000..0a0f5f6d --- /dev/null +++ b/VRCOSC.Game/Graphics/RepoListing/RepoListingPopover.cs @@ -0,0 +1,19 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; + +namespace VRCOSC.Game.Graphics.RepoListing; + +public partial class RepoListingPopover : PopoverScreen +{ + [BackgroundDependencyLoader] + private void load() + { + Child = new RepoListingScreen + { + RelativeSizeAxes = Axes.Both + }; + } +} diff --git a/VRCOSC.Game/Graphics/RepoListing/RepoListingScreen.cs b/VRCOSC.Game/Graphics/RepoListing/RepoListingScreen.cs new file mode 100644 index 00000000..db0509a0 --- /dev/null +++ b/VRCOSC.Game/Graphics/RepoListing/RepoListingScreen.cs @@ -0,0 +1,44 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using VRCOSC.Game.Graphics.ModuleInfo; +using VRCOSC.Game.Graphics.Screen; + +namespace VRCOSC.Game.Graphics.RepoListing; + +public partial class RepoListingScreen : BaseScreen +{ + protected override BaseHeader CreateHeader() => new RepoListingHeader(); + + protected override Drawable CreateBody() => new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Child = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + new DrawableInfoCard("VRCOSC is not responsible for any files downloaded unless specified otherwise. Exercise caution"), + }, + null, + new Drawable[] + { + new RepoListingFlowContainer() + } + } + } + }; +} diff --git a/VRCOSC.Game/Graphics/Run/ParameterEntry.cs b/VRCOSC.Game/Graphics/Run/ParameterEntry.cs index 439f6fb4..8e16d891 100644 --- a/VRCOSC.Game/Graphics/Run/ParameterEntry.cs +++ b/VRCOSC.Game/Graphics/Run/ParameterEntry.cs @@ -56,7 +56,7 @@ private void load() { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = FrameworkFont.Regular.With(size: 17), + Font = new FontUsage("ArialUnicode", size: 18), Colour = ThemeManager.Current[ThemeAttribute.SubText] } } diff --git a/VRCOSC.Game/Graphics/Run/RunScreenFooter.cs b/VRCOSC.Game/Graphics/Run/RunScreenFooter.cs index b3c033d0..48f3db9f 100644 --- a/VRCOSC.Game/Graphics/Run/RunScreenFooter.cs +++ b/VRCOSC.Game/Graphics/Run/RunScreenFooter.cs @@ -66,7 +66,6 @@ private void load() RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Play, IconShadow = true, - Masking = true, CornerRadius = 10, BackgroundColour = ThemeManager.Current[ThemeAttribute.Success], Action = gameManager.Start @@ -97,7 +96,6 @@ private void load() RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Stop, IconShadow = true, - Masking = true, CornerRadius = 10, BackgroundColour = ThemeManager.Current[ThemeAttribute.Failure], Action = gameManager.Stop @@ -117,7 +115,6 @@ private void load() RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Redo, IconShadow = true, - Masking = true, CornerRadius = 10, BackgroundColour = ThemeManager.Current[ThemeAttribute.Action], Action = gameManager.Restart diff --git a/VRCOSC.Game/Graphics/Run/TerminalContainer.cs b/VRCOSC.Game/Graphics/Run/TerminalContainer.cs index 74de2f6f..0c59f816 100644 --- a/VRCOSC.Game/Graphics/Run/TerminalContainer.cs +++ b/VRCOSC.Game/Graphics/Run/TerminalContainer.cs @@ -7,8 +7,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Logging; +using osu.Framework.Platform; +using osuTK; using VRCOSC.Game.Graphics.Themes; +using VRCOSC.Game.Graphics.UI.Button; using VRCOSC.Game.Managers; namespace VRCOSC.Game.Graphics.Run; @@ -23,6 +27,12 @@ public sealed partial class TerminalContainer : Container private readonly DrawablePool terminalEntryPool = new(terminal_entry_count); + [Resolved] + private GameHost host { get; set; } = null!; + + [Resolved] + private Storage storage { get; set; } = null!; + [Resolved] private GameManager gameManager { get; set; } = null!; @@ -39,25 +49,45 @@ public TerminalContainer() { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(2), - Child = terminalScroll = new BasicScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - ClampExtension = 0, - ScrollContent = + Children = new Drawable[]{ + terminalScroll = new BasicScrollContainer { - Child = Content = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + ClampExtension = 0, + ScrollContent = { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding + Child = Content = new FillFlowContainer { - Horizontal = 3 + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding + { + Horizontal = 3 + } } } + }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Size = new Vector2(40), + Padding = new MarginPadding(3), + Child = new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.ExternalLinkAlt, + IconShadow = true, + IconPadding = 6, + BackgroundColour = ThemeManager.Current[ThemeAttribute.Action], + Action = () => host.OpenFileExternally(storage.GetStorageForDirectory("logs").GetFullPath("terminal.log")) + } } } } diff --git a/VRCOSC.Game/Graphics/Screen/BaseHeader.cs b/VRCOSC.Game/Graphics/Screen/BaseHeader.cs index 0815707b..377678ed 100644 --- a/VRCOSC.Game/Graphics/Screen/BaseHeader.cs +++ b/VRCOSC.Game/Graphics/Screen/BaseHeader.cs @@ -71,7 +71,9 @@ private void load() Bottom = 5, Horizontal = 10 }, - TextAnchor = Anchor.TopCentre + TextAnchor = Anchor.TopCentre, + AutoSizeEasing = Easing.OutQuint, + AutoSizeDuration = 150 } } }, diff --git a/VRCOSC.Game/Graphics/Screen/BaseScreen.cs b/VRCOSC.Game/Graphics/Screen/BaseScreen.cs index f33af385..da636861 100644 --- a/VRCOSC.Game/Graphics/Screen/BaseScreen.cs +++ b/VRCOSC.Game/Graphics/Screen/BaseScreen.cs @@ -84,5 +84,5 @@ private void load() } protected abstract BaseHeader? CreateHeader(); - protected abstract Drawable? CreateBody(); + protected abstract Drawable CreateBody(); } diff --git a/VRCOSC.Game/Graphics/Settings/Cards/ToggleSettingCard.cs b/VRCOSC.Game/Graphics/Settings/Cards/ToggleSettingCard.cs index 3abbbbc2..f6c91fca 100644 --- a/VRCOSC.Game/Graphics/Settings/Cards/ToggleSettingCard.cs +++ b/VRCOSC.Game/Graphics/Settings/Cards/ToggleSettingCard.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osuTK; -using VRCOSC.Game.Graphics.Themes; using VRCOSC.Game.Graphics.UI.Button; namespace VRCOSC.Game.Graphics.Settings.Cards; @@ -27,8 +26,6 @@ private void load() Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Size = new Vector2(25), - CornerRadius = 5, - BorderColour = ThemeManager.Current[ThemeAttribute.Border], BorderThickness = 2, ShouldAnimate = false, State = { Value = SettingBindable.Value } diff --git a/VRCOSC.Game/Graphics/TabBar/DrawableTab.cs b/VRCOSC.Game/Graphics/TabBar/DrawableTab.cs index d333d384..ccb1a8a7 100644 --- a/VRCOSC.Game/Graphics/TabBar/DrawableTab.cs +++ b/VRCOSC.Game/Graphics/TabBar/DrawableTab.cs @@ -10,7 +10,6 @@ using osu.Framework.Input.Events; using osuTK; using VRCOSC.Game.Graphics.Themes; -using VRCOSC.Game.Managers; namespace VRCOSC.Game.Graphics.TabBar; @@ -24,28 +23,20 @@ public sealed partial class DrawableTab : ClickableContainer private Box background = null!; private SelectedIndicator indicator = null!; - private TabPopover popover = null!; private SpriteIcon spriteIcon = null!; public Tab Tab { get; init; } - public IconUsage Icon { get; init; } [Resolved] private Bindable selectedTab { get; set; } = null!; - [Resolved] - private GameManager gameManager { get; set; } = null!; - - public DrawableTab() + [BackgroundDependencyLoader] + private void load() { RelativeSizeAxes = Axes.Both; FillMode = FillMode.Fit; - } - [BackgroundDependencyLoader] - private void load() - { Children = new Drawable[] { background = new Box @@ -66,18 +57,6 @@ private void load() indicator = new SelectedIndicator { RelativeSizeAxes = Axes.Both - }, - popover = new TabPopover - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreLeft, - PopoverAnchor = Anchor.CentreLeft, - Child = new SpriteText - { - Text = Tab.ToString(), - Colour = ThemeManager.Current[ThemeAttribute.Text], - Font = FrameworkFont.Regular.With(size: 20) - } } }; } @@ -107,7 +86,6 @@ protected override void LoadComplete() protected override bool OnHover(HoverEvent e) { background.FadeColour(hover_colour, onhover_duration, Easing.InOutSine); - popover.Show(); if (selectedTab.Value == Tab) return base.OnHover(e); @@ -120,7 +98,6 @@ protected override void OnHoverLost(HoverLostEvent e) base.OnHoverLost(e); background.FadeColour(default_colour, onhoverlost_duration, Easing.InOutSine); - popover.Hide(); if (selectedTab.Value == Tab) return; diff --git a/VRCOSC.Game/Graphics/TabBar/TabPopover.cs b/VRCOSC.Game/Graphics/TabBar/TabPopover.cs deleted file mode 100644 index 9e579d40..00000000 --- a/VRCOSC.Game/Graphics/TabBar/TabPopover.cs +++ /dev/null @@ -1,56 +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 osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osuTK; -using VRCOSC.Game.Graphics.Themes; - -namespace VRCOSC.Game.Graphics.TabBar; - -public sealed partial class TabPopover : Popover -{ - public TabPopover() - { - Background.Colour = ThemeManager.Current[ThemeAttribute.Mid]; - Content.Padding = new MarginPadding(10); - RelativePositionAxes = Axes.X; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Body.Masking = true; - Body.CornerRadius = 5; - } - - protected override Drawable CreateArrow() => new EquilateralTriangle - { - Colour = ThemeManager.Current[ThemeAttribute.Mid], - Origin = Anchor.TopCentre, - Scale = new Vector2(1.05f) - }; - - protected override void AnchorUpdated(Anchor anchor) - { - base.AnchorUpdated(anchor); - - bool isCenteredAnchor = anchor.HasFlagFast(Anchor.x1) || anchor.HasFlagFast(Anchor.y1); - Body.Margin = new MarginPadding(isCenteredAnchor ? 10 : 3); - Arrow.Size = new Vector2(isCenteredAnchor ? 12 : 15); - } - - protected override void PopIn() - { - this.FadeIn(200, Easing.OutQuart); - this.MoveToX(0, 200, Easing.OutQuart); - } - - protected override void PopOut() - { - this.FadeOut(200, Easing.OutQuart); - this.MoveToX(0.25f, 200, Easing.OutQuart); - } -} diff --git a/VRCOSC.Game/Graphics/UI/Button/BasicButton.cs b/VRCOSC.Game/Graphics/UI/Button/BasicButton.cs index 1ae43829..58b647e4 100644 --- a/VRCOSC.Game/Graphics/UI/Button/BasicButton.cs +++ b/VRCOSC.Game/Graphics/UI/Button/BasicButton.cs @@ -12,82 +12,46 @@ namespace VRCOSC.Game.Graphics.UI.Button; public partial class BasicButton : VRCOSCButton { - private Colour4 backgroundColourStateOff = ThemeManager.Current[ThemeAttribute.Failure]; - private Colour4 backgroundColourStateOn = ThemeManager.Current[ThemeAttribute.Success]; + private Drawable backgroundBox = null!; - protected Drawable BackgroundBox = null!; - - public BindableBool State = new(); - public bool Stateful { get; init; } + public Bindable State = new(); + public bool Stateful { get; set; } public Colour4 BackgroundColour { - get => backgroundColourStateOff; - set - { - backgroundColourStateOn = value; - backgroundColourStateOff = value; - updateBackgroundColour(); - } - } - - public Colour4 BackgroundColourStateOn - { - get => backgroundColourStateOn; + get => BackgroundColourStateOff; set { - backgroundColourStateOn = value; - updateBackgroundColour(); + BackgroundColourStateOn = value; + BackgroundColourStateOff = value; } } - public Colour4 BackgroundColourStateOff - { - get => backgroundColourStateOff; - set - { - backgroundColourStateOff = value; - updateBackgroundColour(); - } - } + public Colour4 BackgroundColourStateOn { get; set; } = ThemeManager.Current[ThemeAttribute.Success]; + public Colour4 BackgroundColourStateOff { get; set; } = ThemeManager.Current[ThemeAttribute.Failure]; [BackgroundDependencyLoader] private void load() { - Child = BackgroundBox = CreateBackground(); - - State.BindValueChanged(_ => updateBackgroundColour(), true); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - updateBackgroundColour(); + Add(backgroundBox = new Box + { + RelativeSizeAxes = Axes.Both + }); } - private void updateBackgroundColour() + protected override void Update() { - if (!IsLoaded) return; + base.Update(); if (Stateful) - BackgroundBox.Colour = State.Value ? backgroundColourStateOn : backgroundColourStateOff; + backgroundBox.Colour = State.Value ? BackgroundColourStateOn : BackgroundColourStateOff; else - BackgroundBox.Colour = backgroundColourStateOff; + backgroundBox.Colour = BackgroundColourStateOff; } protected override bool OnClick(ClickEvent e) { - if (Enabled.Value) State.Toggle(); + if (Enabled.Value) State.Value = !State.Value; return base.OnClick(e); } - - public virtual Drawable CreateBackground() - { - return new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both - }; - } } diff --git a/VRCOSC.Game/Graphics/UI/Button/IconButton.cs b/VRCOSC.Game/Graphics/UI/Button/IconButton.cs index 6ef4d801..77c5894e 100644 --- a/VRCOSC.Game/Graphics/UI/Button/IconButton.cs +++ b/VRCOSC.Game/Graphics/UI/Button/IconButton.cs @@ -11,43 +11,23 @@ namespace VRCOSC.Game.Graphics.UI.Button; public partial class IconButton : BasicButton { - private IconUsage iconStateOff = FontAwesome.Solid.PowerOff; - private IconUsage iconStateOn = FontAwesome.Solid.PowerOff; private int iconPadding = 8; private SpriteIcon spriteIcon = null!; private Container? wrapper; - public IconUsage Icon + public IconUsage? Icon { - get => iconStateOff; + get => IconStateOff; set { - iconStateOff = value; - iconStateOn = value; - updateIcon(); + IconStateOff = value; + IconStateOn = value; } } - public IconUsage IconStateOff - { - get => iconStateOff; - set - { - iconStateOff = value; - updateIcon(); - } - } - - public IconUsage IconStateOn - { - get => iconStateOn; - set - { - iconStateOn = value; - updateIcon(); - } - } + public IconUsage? IconStateOff { get; set; } = FontAwesome.Solid.PowerOff; + public IconUsage? IconStateOn { get; set; } = FontAwesome.Solid.PowerOff; public int IconPadding { @@ -63,44 +43,50 @@ public int IconPadding public bool IconShadow { get; init; } + public IconButton() + { + CornerRadius = 5; + } + [BackgroundDependencyLoader] private void load() { Add(wrapper = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, Padding = new MarginPadding(IconPadding), - Child = spriteIcon = createSpriteIcon() + Child = spriteIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Shadow = IconShadow, + Colour = ThemeManager.Current[ThemeAttribute.Text] + } }); - - State.BindValueChanged(_ => updateIcon(), true); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - updateIcon(); } - private void updateIcon() + protected override void Update() { - if (!IsLoaded) return; + base.Update(); if (Stateful) - spriteIcon.Icon = State.Value ? iconStateOn : iconStateOff; + setIcon(State.Value ? IconStateOn : IconStateOff); else - spriteIcon.Icon = iconStateOff; + setIcon(IconStateOn); } - private SpriteIcon createSpriteIcon() => new() + private void setIcon(IconUsage? iconUsage) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Shadow = IconShadow, - Colour = ThemeManager.Current[ThemeAttribute.Text] - }; + if (iconUsage is null) + { + spriteIcon.FadeTo(0, 150, Easing.OutQuint); + } + else + { + spriteIcon.Icon = iconUsage.Value; + spriteIcon.FadeTo(1, 150, Easing.OutQuint); + } + } } diff --git a/VRCOSC.Game/Graphics/UI/Button/TextButton.cs b/VRCOSC.Game/Graphics/UI/Button/TextButton.cs index d75aed67..43c77f29 100644 --- a/VRCOSC.Game/Graphics/UI/Button/TextButton.cs +++ b/VRCOSC.Game/Graphics/UI/Button/TextButton.cs @@ -10,33 +10,21 @@ namespace VRCOSC.Game.Graphics.UI.Button; public partial class TextButton : BasicButton { - private string text = string.Empty; - - private SpriteText? spriteText; - - public string Text - { - get => text; - set - { - text = value; - if (spriteText is not null) spriteText.Text = text; - } - } - + public string Text { get; init; } = string.Empty; public float FontSize { get; init; } = 30; [BackgroundDependencyLoader] private void load() { - Add(spriteText = new SpriteText + Add(new SpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = FrameworkFont.Regular.With(size: FontSize), Colour = ThemeManager.Current[ThemeAttribute.Text], Text = Text, - Shadow = true + Shadow = true, + ShadowColour = ThemeManager.Current[ThemeAttribute.Darker] }); } } diff --git a/VRCOSC.Game/Graphics/UI/Button/ToggleButton.cs b/VRCOSC.Game/Graphics/UI/Button/ToggleButton.cs index e05c9217..a6aaa30a 100644 --- a/VRCOSC.Game/Graphics/UI/Button/ToggleButton.cs +++ b/VRCOSC.Game/Graphics/UI/Button/ToggleButton.cs @@ -2,64 +2,20 @@ // See the LICENSE file in the repository root for full license text. using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using VRCOSC.Game.Graphics.Themes; namespace VRCOSC.Game.Graphics.UI.Button; -public sealed partial class ToggleButton : VRCOSCButton +public sealed partial class ToggleButton : IconButton { - public Bindable State { get; init; } = new(); - - public ToggleButton() - { - Masking = true; - CornerRadius = 5; - } - [BackgroundDependencyLoader] private void load() { - SpriteIcon icon; - - Children = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Colour = ThemeManager.Current[ThemeAttribute.Mid] - }, - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(5), - Child = icon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Icon = FontAwesome.Solid.Check, - Colour = ThemeManager.Current[ThemeAttribute.Text], - Alpha = State.Value ? 1 : 0 - } - } - }; - - State.BindValueChanged(e => icon.FadeTo(e.NewValue ? 1 : 0, 200, Easing.OutQuart)); - } - - protected override bool OnClick(ClickEvent e) - { - State.Value = !State.Value; - return base.OnClick(e); + Stateful = true; + IconStateOn = FontAwesome.Solid.Check; + IconStateOff = null; + IconPadding = 5; + BackgroundColour = ThemeManager.Current[ThemeAttribute.Mid]; } } diff --git a/VRCOSC.Game/Graphics/UI/Button/VRCOSCButton.cs b/VRCOSC.Game/Graphics/UI/Button/VRCOSCButton.cs index 595df98e..15f90450 100644 --- a/VRCOSC.Game/Graphics/UI/Button/VRCOSCButton.cs +++ b/VRCOSC.Game/Graphics/UI/Button/VRCOSCButton.cs @@ -3,29 +3,49 @@ using System; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osuTK; using osuTK.Input; +using VRCOSC.Game.Graphics.Themes; namespace VRCOSC.Game.Graphics.UI.Button; -public partial class VRCOSCButton : osu.Framework.Graphics.UserInterface.Button +public partial class VRCOSCButton : ClickableContainer { private const float alpha_enabled = 1.0f; private const float alpha_disabled = 0.5f; + private static readonly Vector2 hover_offset = new(0, -2); public bool ShouldAnimate { get; init; } = true; public bool Circular { get; init; } - public Vector2 HoverOffset { get; init; } = new(0, -2); + + protected override Container Content { get; } public override bool HandlePositionalInput => Enabled.Value; + public new float CornerRadius + { + set => Content.CornerRadius = value; + } + + public new float BorderThickness + { + set => Content.BorderThickness = value; + } + protected VRCOSCButton() { - Masking = true; + InternalChild = Content = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + EdgeEffect = VRCOSCEdgeEffects.NoShadow, + BorderColour = ThemeManager.Current[ThemeAttribute.Border] + }; + Enabled.Value = true; - Enabled.BindValueChanged(_ => this.FadeTo(Enabled.Value ? alpha_enabled : alpha_disabled, 500, Easing.OutCirc), true); - EdgeEffect = VRCOSCEdgeEffects.NoShadow; + Enabled.BindValueChanged(_ => Content.FadeTo(Enabled.Value ? alpha_enabled : alpha_disabled, 500, Easing.OutCirc), true); } protected override void UpdateAfterAutoSize() @@ -38,8 +58,8 @@ protected override bool OnHover(HoverEvent e) { if (ShouldAnimate) { - this.TransformTo(nameof(EdgeEffect), VRCOSCEdgeEffects.HoverShadow, 100, Easing.OutCirc); - this.MoveTo(HoverOffset, 100, Easing.OutCirc); + Content.TransformTo(nameof(EdgeEffect), VRCOSCEdgeEffects.HoverShadow, 100, Easing.OutCirc); + Content.MoveTo(hover_offset, 100, Easing.OutCirc); } return true; @@ -49,8 +69,8 @@ protected override void OnHoverLost(HoverLostEvent e) { if (ShouldAnimate) { - this.TransformTo(nameof(EdgeEffect), VRCOSCEdgeEffects.NoShadow, 100, Easing.OutCirc); - this.MoveTo(Vector2.Zero, 100, Easing.OutCirc); + Content.TransformTo(nameof(EdgeEffect), VRCOSCEdgeEffects.NoShadow, 100, Easing.OutCirc); + Content.MoveTo(Vector2.Zero, 100, Easing.OutCirc); } } @@ -58,8 +78,8 @@ protected override bool OnClick(ClickEvent e) { if (ShouldAnimate && IsHovered) { - this.TransformTo(nameof(EdgeEffect), VRCOSCEdgeEffects.HoverShadow, 100, Easing.OutCirc); - this.MoveTo(HoverOffset, 100, Easing.OutCirc); + Content.TransformTo(nameof(EdgeEffect), VRCOSCEdgeEffects.HoverShadow, 100, Easing.OutCirc); + Content.MoveTo(hover_offset, 100, Easing.OutCirc); } return base.OnClick(e); @@ -71,8 +91,8 @@ protected override bool OnMouseDown(MouseDownEvent e) if (ShouldAnimate) { - this.TransformTo(nameof(EdgeEffect), VRCOSCEdgeEffects.NoShadow, 100, Easing.OutCirc); - this.MoveTo(Vector2.Zero, 100, Easing.OutCirc); + Content.TransformTo(nameof(EdgeEffect), VRCOSCEdgeEffects.NoShadow, 100, Easing.OutCirc); + Content.MoveTo(Vector2.Zero, 100, Easing.OutCirc); } return true; diff --git a/VRCOSC.Game/Graphics/UI/Text/FloatTextBox.cs b/VRCOSC.Game/Graphics/UI/Text/FloatTextBox.cs new file mode 100644 index 00000000..d6997a4f --- /dev/null +++ b/VRCOSC.Game/Graphics/UI/Text/FloatTextBox.cs @@ -0,0 +1,16 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +namespace VRCOSC.Game.Graphics.UI.Text; + +public partial class FloatTextBox : ValidationTextBox +{ + public FloatTextBox() + { + EmptyIsValid = false; + } + + protected override bool IsTextValid(string text) => float.TryParse(text, out _); + + protected override float GetConvertedText() => float.Parse(Current.Value); +} diff --git a/VRCOSC.Game/Graphics/UI/Text/IPTextBox.cs b/VRCOSC.Game/Graphics/UI/Text/IPTextBox.cs deleted file mode 100644 index 41de7daa..00000000 --- a/VRCOSC.Game/Graphics/UI/Text/IPTextBox.cs +++ /dev/null @@ -1,15 +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.Text.RegularExpressions; - -namespace VRCOSC.Game.Graphics.UI.Text; - -public partial class IPTextBox : ValidationTextBox -{ - private readonly Regex regex = new(@"^(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})$"); - - protected override bool IsTextValid(string text) => regex.IsMatch(text); - - protected override string GetConvertedText() => Current.Value; -} diff --git a/VRCOSC.Game/Graphics/UI/Text/IntTextBox.cs b/VRCOSC.Game/Graphics/UI/Text/IntTextBox.cs index d7a8da7f..4249c078 100644 --- a/VRCOSC.Game/Graphics/UI/Text/IntTextBox.cs +++ b/VRCOSC.Game/Graphics/UI/Text/IntTextBox.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; - namespace VRCOSC.Game.Graphics.UI.Text; public partial class IntTextBox : ValidationTextBox @@ -12,18 +10,7 @@ public IntTextBox() EmptyIsValid = false; } - protected override bool IsTextValid(string text) - { - try - { - _ = int.Parse(text); - return true; - } - catch (Exception) - { - return false; - } - } + protected override bool IsTextValid(string text) => int.TryParse(text, out _); protected override int GetConvertedText() => int.Parse(Current.Value); } diff --git a/VRCOSC.Game/Graphics/UI/Text/PortTextBox.cs b/VRCOSC.Game/Graphics/UI/Text/PortTextBox.cs deleted file mode 100644 index b3028573..00000000 --- a/VRCOSC.Game/Graphics/UI/Text/PortTextBox.cs +++ /dev/null @@ -1,21 +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.Text.RegularExpressions; - -namespace VRCOSC.Game.Graphics.UI.Text; - -public partial class PortTextBox : ValidationTextBox -{ - private readonly Regex regex = new(@"^([0-9]{1,5})$"); - - protected override bool IsTextValid(string text) - { - if (!regex.IsMatch(text)) return false; - - var port = int.Parse(text); - return port is >= 0 and <= 65535; - } - - protected override int GetConvertedText() => int.Parse(Current.Value); -} diff --git a/VRCOSC.Game/Graphics/UI/Text/ValidationTextBox.cs b/VRCOSC.Game/Graphics/UI/Text/ValidationTextBox.cs index c1c46829..5b091149 100644 --- a/VRCOSC.Game/Graphics/UI/Text/ValidationTextBox.cs +++ b/VRCOSC.Game/Graphics/UI/Text/ValidationTextBox.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,6 +14,7 @@ namespace VRCOSC.Game.Graphics.UI.Text; public abstract partial class ValidationTextBox : VRCOSCTextBox { public bool EmptyIsValid { get; init; } = true; + public Bindable? ValidCurrent { get; init; } private InvalidIcon invalidIcon = null!; @@ -29,12 +31,16 @@ private void load() FillMode = FillMode.Fit }); + ValidCurrent?.BindValueChanged(e => Current.Value = e.NewValue?.ToString(), true); + Current.BindValueChanged(e => { if (validate(e.NewValue)) { invalidIcon.Hide(); - OnValidEntry?.Invoke(GetConvertedText()); + var value = GetConvertedText(); + OnValidEntry?.Invoke(value); + if (ValidCurrent is not null) ValidCurrent.Value = value; } else { @@ -76,7 +82,7 @@ private void load() RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.ExclamationCircle, Colour = Colour4.Red - }, + } } }; } diff --git a/VRCOSC.Game/Graphics/UI/VRCOSCScrollContainer.cs b/VRCOSC.Game/Graphics/UI/VRCOSCScrollContainer.cs index 98b1b7da..4c768ea9 100644 --- a/VRCOSC.Game/Graphics/UI/VRCOSCScrollContainer.cs +++ b/VRCOSC.Game/Graphics/UI/VRCOSCScrollContainer.cs @@ -23,6 +23,7 @@ public partial class VRCOSCScrollContainer : ScrollContainer protected VRCOSCScrollContainer(Direction scrollDirection = Direction.Vertical) : base(scrollDirection) { + ScrollbarOverlapsContent = false; } protected override ScrollbarContainer CreateScrollbar(Direction direction) => new VRCOSCScrollbar(direction); diff --git a/VRCOSC.Game/Graphics/UI/VRCOSCSlider.cs b/VRCOSC.Game/Graphics/UI/VRCOSCSlider.cs index 4cca65fa..29a29b6b 100644 --- a/VRCOSC.Game/Graphics/UI/VRCOSCSlider.cs +++ b/VRCOSC.Game/Graphics/UI/VRCOSCSlider.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -13,10 +14,12 @@ namespace VRCOSC.Game.Graphics.UI; public sealed partial class VRCOSCSlider : BasicSliderBar where T : struct, IComparable, IConvertible, IEquatable { + public required BindableNumber RoudedCurrent { get; init; } + public VRCOSCSlider() { BackgroundColour = ThemeManager.Current[ThemeAttribute.Dark]; - SelectionColour = ThemeManager.Current[ThemeAttribute.Lighter]; + SelectionColour = ThemeManager.Current[ThemeAttribute.Light]; Masking = true; CornerRadius = 5; BorderThickness = 2; @@ -26,6 +29,8 @@ public VRCOSCSlider() [BackgroundDependencyLoader] private void load() { + Current = RoudedCurrent.GetUnboundCopy(); + SpriteText valueText; Add(new Container @@ -62,17 +67,15 @@ private void load() } }); - Current.BindValueChanged(_ => valueText.Text = getCurrentValue().ToString()!, true); + Current.BindValueChanged(_ => + { + valueText.Text = getCurrentValue().ToString()!; + RoudedCurrent.Value = getCurrentValue(); + }, true); } - private T roundValue() - { - // bit excessive, but it keeps the float as 2 decimal places. Might refactor into multiple slider types - return (T)Convert.ChangeType(MathF.Round(Convert.ToSingle(Current.Value), 2), typeof(T)); - } + private T getCurrentValue() => typeof(T) == typeof(float) ? roundValue() : Current.Value; - private T getCurrentValue() - { - return typeof(T) == typeof(float) ? roundValue() : Current.Value; - } + // bit excessive, but it keeps the float as 2 decimal places. Might refactor into multiple slider types + private T roundValue() => (T)Convert.ChangeType(MathF.Round(Convert.ToSingle(Current.Value), 2), typeof(T)); } diff --git a/VRCOSC.Game/Graphics/UI/VRCOSCTextBox.cs b/VRCOSC.Game/Graphics/UI/VRCOSCTextBox.cs index 42a57a86..d0f37869 100644 --- a/VRCOSC.Game/Graphics/UI/VRCOSCTextBox.cs +++ b/VRCOSC.Game/Graphics/UI/VRCOSCTextBox.cs @@ -15,6 +15,8 @@ public partial class VRCOSCTextBox : BasicTextBox [Resolved] private GameHost host { get; set; } = null!; + public bool UnicodeSupport { get; init; } + public VRCOSCTextBox() { BackgroundFocused = ThemeManager.Current[ThemeAttribute.Dark]; @@ -42,16 +44,21 @@ protected override SpriteText CreatePlaceholder() return base.CreatePlaceholder().With(t => t.Colour = ThemeManager.Current[ThemeAttribute.Text].Opacity(0.5f)); } - protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer + protected override Drawable GetDrawableCharacter(char c) { - AutoSizeAxes = Axes.Both, - Child = new SpriteText + var font = UnicodeSupport ? new FontUsage(@"ArialUnicode", size: CalculatedTextSize) : FrameworkFont.Condensed.With(size: CalculatedTextSize); + + return new FallingDownContainer { - Text = c.ToString(), - Font = FrameworkFont.Condensed.With(size: CalculatedTextSize), - Colour = ThemeManager.Current[ThemeAttribute.Text] - } - }; + AutoSizeAxes = Axes.Both, + Child = new SpriteText + { + Text = c.ToString(), + Font = font, + Colour = ThemeManager.Current[ThemeAttribute.Text] + } + }; + } protected override void Dispose(bool isDisposing) { diff --git a/VRCOSC.Game/Managers/ChatBoxManager.cs b/VRCOSC.Game/Managers/ChatBoxManager.cs index d3ae760c..772d04e2 100644 --- a/VRCOSC.Game/Managers/ChatBoxManager.cs +++ b/VRCOSC.Game/Managers/ChatBoxManager.cs @@ -17,7 +17,7 @@ namespace VRCOSC.Game.Managers; -public class ChatBoxManager : ICanSerialise +public class ChatBoxManager { private bool sendEnabled; @@ -69,16 +69,31 @@ public void Load(Storage storage, GameManager gameManager, NotificationContainer serialisationManager = new SerialisationManager(); serialisationManager.RegisterSerialiser(1, new TimelineSerialiser(storage, notification, this)); - TimelineLength.Value = TimeSpan.FromMinutes(1); - Clips.AddRange(DefaultTimeline.GenerateDefaultTimeline(this)); - + setDefaults(); Deserialise(); + bindAttributes(); + } + private void setDefaults() + { + TimelineLength.Value = TimeSpan.FromMinutes(1); + Clips.ReplaceItems(DefaultTimeline.GenerateDefaultTimeline(this)); + } + + private void bindAttributes() + { TimelineLength.BindValueChanged(_ => Serialise()); Clips.BindCollectionChanged((_, _) => Serialise()); Clips.ForEach(clip => clip.Load()); } + public void Import(string filePath) + { + SelectedClip.Value = null; + serialisationManager.Deserialise(filePath); + bindAttributes(); + } + public void Deserialise() { serialisationManager.Deserialise(); @@ -247,9 +262,10 @@ public void RegisterVariable(string module, string lookup, string name, string f VariableMetadata[module][lookup] = variableMetadata; } - public void SetVariable(string module, string lookup, string? value) + public void SetVariable(string module, string lookup, string? value, string suffix) { - VariableValues[(module, lookup)] = value; + var finalLookup = string.IsNullOrEmpty(suffix) ? lookup : $"{lookup}_{suffix}"; + VariableValues[(module, finalLookup)] = value; } public void RegisterState(string module, string lookup, string name, string defaultFormat) diff --git a/VRCOSC.Game/Managers/GameManager.cs b/VRCOSC.Game/Managers/GameManager.cs index 691f65c0..b496cad5 100644 --- a/VRCOSC.Game/Managers/GameManager.cs +++ b/VRCOSC.Game/Managers/GameManager.cs @@ -21,7 +21,6 @@ using VRCOSC.Game.Graphics.Notifications; using VRCOSC.Game.Modules; using VRCOSC.Game.Modules.Avatar; -using VRCOSC.Game.Modules.Sources; using VRCOSC.Game.OpenVR; using VRCOSC.Game.OpenVR.Metadata; using VRCOSC.Game.OSC; @@ -58,9 +57,6 @@ public partial class GameManager : Component [Resolved] private GameHost host { get; set; } = null!; - [Resolved] - private IVRCOSCSecrets secrets { get; set; } = null!; - [Resolved] private ChatBoxManager chatBoxManager { get; set; } = null!; @@ -106,10 +102,8 @@ private void load() private void setupModules() { ModuleManager = new ModuleManager(); - ModuleManager.AddSource(new InternalModuleSource()); - ModuleManager.AddSource(new ExternalModuleSource(storage)); - ModuleManager.InjectModuleDependencies(host, this, secrets, new Scheduler(() => ThreadSafety.IsUpdateThread, Clock)); - ModuleManager.Load(storage, notifications); + ModuleManager.InjectModuleDependencies(host, this, new Scheduler(() => ThreadSafety.IsUpdateThread, Clock), storage, notifications); + ModuleManager.Load(); } protected override void Update() @@ -148,7 +142,7 @@ protected override void LoadComplete() editingModule.BindValueChanged(e => { - if (e.NewValue is null && e.OldValue is not null) ModuleManager.Serialise(); + if (e.NewValue is null && e.OldValue is not null) e.OldValue.Serialise(); }, true); } @@ -213,7 +207,7 @@ public void Start() } var moduleEnabled = new Dictionary(); - ModuleManager.ForEach(module => moduleEnabled.Add(module.SerialisedName, module.Enabled.Value)); + ModuleManager.Modules.ForEach(module => moduleEnabled.Add(module.SerialisedName, module.Enabled.Value)); AvatarConfig = null; diff --git a/VRCOSC.Game/Managers/ModuleManager.cs b/VRCOSC.Game/Managers/ModuleManager.cs index 33e6c0b0..b981bc99 100644 --- a/VRCOSC.Game/Managers/ModuleManager.cs +++ b/VRCOSC.Game/Managers/ModuleManager.cs @@ -4,119 +4,131 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; -using osu.Framework.Lists; +using System.Reflection; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Platform; using osu.Framework.Threading; using VRCOSC.Game.Graphics.Notifications; using VRCOSC.Game.Modules; using VRCOSC.Game.Modules.Serialisation.Legacy; -using VRCOSC.Game.Modules.Serialisation.V1; -using VRCOSC.Game.Modules.Sources; using VRCOSC.Game.OSC.VRChat; using VRCOSC.Game.Serialisation; +using Module = VRCOSC.Game.Modules.Module; namespace VRCOSC.Game.Managers; -public sealed class ModuleManager : IEnumerable, ICanSerialise +public sealed class ModuleManager : IEnumerable { - private static TerminalLogger terminal => new("ModuleManager"); + private static TerminalLogger terminal => new("VRCOSC"); - private readonly List sources = new(); - private readonly SortedList modules = new(); + public readonly Dictionary ModuleCollections = new(); - public Action? OnModuleEnabledChanged; + // TODO - Can be made private when migration is complete + public IReadOnlyList Modules => ModuleCollections.Values.SelectMany(collection => collection.Modules).ToList(); - public void AddSource(IModuleSource source) => sources.Add(source); - public bool RemoveSource(IModuleSource source) => sources.Remove(source); + public Action? OnModuleEnabledChanged; private GameHost host = null!; private GameManager gameManager = null!; - private IVRCOSCSecrets secrets = null!; private Scheduler scheduler = null!; + private Storage storage = null!; + private NotificationContainer notification = null!; private SerialisationManager serialisationManager = null!; private readonly List runningModulesCache = new(); - public void InjectModuleDependencies(GameHost host, GameManager gameManager, IVRCOSCSecrets secrets, Scheduler scheduler) + public void InjectModuleDependencies(GameHost host, GameManager gameManager, Scheduler scheduler, Storage storage, NotificationContainer notification) { this.host = host; this.gameManager = gameManager; - this.secrets = secrets; this.scheduler = scheduler; + this.storage = storage; + this.notification = notification; } - public void Load(Storage storage, NotificationContainer notification) + public void Load() { serialisationManager = new SerialisationManager(); - serialisationManager.RegisterSerialiser(1, new ModuleSerialiser(storage, notification, this)); + serialisationManager.RegisterSerialiser(1, new LegacyModuleManagerSerialiser(storage, notification, this)); - modules.Clear(); + loadModules(); - sources.ForEach(source => + // TODO - Remove when migration has completed + if (storage.Exists("modules.json")) { - foreach (var type in source.Load()) + serialisationManager.Deserialise(); + storage.Delete("modules.json"); + + foreach (var module in Modules) { - var module = (Module)Activator.CreateInstance(type)!; - module.InjectDependencies(host, gameManager, secrets, scheduler); - module.Load(); - modules.Add(module); + module.Serialise(); } - }); + } + } + + private void loadModules() + { + ModuleCollections.Clear(); - deserialiseProxy(storage); + loadInternalModules(); + loadExternalModules(); - foreach (var module in this) + foreach (var module in Modules) { module.Enabled.BindValueChanged(_ => { OnModuleEnabledChanged?.Invoke(); - Serialise(); + module.Serialise(); }); } } - // Handles migration from LegacyModuleSerialiser - private void deserialiseProxy(Storage storage) + private void loadInternalModules() { - if (!storage.Exists("modules.json")) - { - var legacySerialisation = new LegacyModuleSerialiser(storage); - - foreach (var module in this) - { - legacySerialisation.Deserialise(module); - } + var dllPath = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.AllDirectories).FirstOrDefault(fileName => fileName.Contains("VRCOSC.Modules"))!; + loadModulesFromAssembly(Assembly.Load(File.ReadAllBytes(dllPath))); + } - if (serialisationManager.Serialise()) - { - storage.DeleteDirectory("modules"); - } - } - else - { - Deserialise(); - } + private void loadExternalModules() + { + var moduleDirectoryPath = storage.GetStorageForDirectory("assemblies").GetFullPath(string.Empty, true); + Directory.GetFiles(moduleDirectoryPath, "*.dll", SearchOption.AllDirectories).ForEach(dllPath => loadModulesFromAssembly(Assembly.Load(File.ReadAllBytes(dllPath)))); } - public void Deserialise() + private void loadModulesFromAssembly(Assembly assembly) { - serialisationManager.Deserialise(); + assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(Module)) && !type.IsAbstract).ForEach(type => registerModule(assembly, type)); } - public void Serialise() + private void registerModule(Assembly assembly, Type type) { - serialisationManager.Serialise(); + try + { + var module = (Module)Activator.CreateInstance(type)!; + module.InjectDependencies(host, gameManager, scheduler, storage, notification); + module.Load(); + + var assemblyLookup = assembly.GetName().Name!.ToLowerInvariant(); + + ModuleCollections.TryAdd(assemblyLookup, new ModuleCollection(assembly)); + ModuleCollections[assemblyLookup].Modules.Add(module); + } + catch (Exception) + { + notification.Notify(new ExceptionNotification($"{type.Name} could not be loaded. It may require an update")); + } } public void Start() { - if (modules.All(module => !module.Enabled.Value)) + if (Modules.All(module => !module.Enabled.Value)) terminal.Log("You have no modules selected!\nSelect some modules to begin using VRCOSC"); runningModulesCache.Clear(); - foreach (var module in modules.Where(module => module.Enabled.Value)) + foreach (var module in Modules.Where(module => module.Enabled.Value)) { module.Start(); runningModulesCache.Add(module); @@ -154,10 +166,12 @@ public void PlayerUpdate() } } - public IEnumerable GetEnabledModuleNames() => modules.Where(module => module.Enabled.Value).Select(module => module.SerialisedName); + public Module? GetModule(string serialisedName) => Modules.SingleOrDefault(module => module.SerialisedName == serialisedName); + + public IEnumerable GetEnabledModuleNames() => Modules.Where(module => module.Enabled.Value).Select(module => module.SerialisedName); - public string GetModuleName(string serialisedName) => modules.Single(module => module.SerialisedName == serialisedName).Title; + public string GetModuleName(string serialisedName) => Modules.Single(module => module.SerialisedName == serialisedName).Title; - public IEnumerator GetEnumerator() => modules.GetEnumerator(); + public IEnumerator GetEnumerator() => ModuleCollections.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/VRCOSC.Game/VRCOSCSecretsKeys.cs b/VRCOSC.Game/Managers/RepoManager.cs similarity index 64% rename from VRCOSC.Game/VRCOSCSecretsKeys.cs rename to VRCOSC.Game/Managers/RepoManager.cs index 80637006..3f50ba34 100644 --- a/VRCOSC.Game/VRCOSCSecretsKeys.cs +++ b/VRCOSC.Game/Managers/RepoManager.cs @@ -1,10 +1,8 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. -namespace VRCOSC.Game; +namespace VRCOSC.Game.Managers; -public enum VRCOSCSecretsKeys +public class RepoManager { - Hyperate, - Weather } diff --git a/VRCOSC.Game/Managers/RouterManager.cs b/VRCOSC.Game/Managers/RouterManager.cs index 7629b7de..08de0f09 100644 --- a/VRCOSC.Game/Managers/RouterManager.cs +++ b/VRCOSC.Game/Managers/RouterManager.cs @@ -11,7 +11,7 @@ namespace VRCOSC.Game.Managers; -public class RouterManager : ICanSerialise +public class RouterManager { public BindableList Store = new(); diff --git a/VRCOSC.Game/Managers/StartupManager.cs b/VRCOSC.Game/Managers/StartupManager.cs index 889e001f..2f0ccaa2 100644 --- a/VRCOSC.Game/Managers/StartupManager.cs +++ b/VRCOSC.Game/Managers/StartupManager.cs @@ -15,7 +15,7 @@ namespace VRCOSC.Game.Managers; -public class StartupManager : ICanSerialise +public class StartupManager { private readonly TerminalLogger logger = new("VRCOSC"); private readonly SerialisationManager serialisationManager; diff --git a/VRCOSC.Game/Modules/Attributes/ModuleAttribute.cs b/VRCOSC.Game/Modules/Attributes/ModuleAttribute.cs new file mode 100644 index 00000000..6cc189a3 --- /dev/null +++ b/VRCOSC.Game/Modules/Attributes/ModuleAttribute.cs @@ -0,0 +1,187 @@ +// 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.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Dropdown; +using VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Slider; +using VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Text; +using VRCOSC.Game.Graphics.ModuleAttributes.Attributes.Toggle; +using VRCOSC.Game.OSC.VRChat; + +namespace VRCOSC.Game.Modules.Attributes; + +public abstract class ModuleAttribute +{ + public required string Name { internal get; init; } + public required string Description { internal get; init; } + public Func? DependsOn { internal get; init; } + + public bool IsValueType() => GetValueType() == typeof(TCheckType) || GetValueType().IsSubclassOf(typeof(TCheckType)); + public abstract Type GetValueType(); + + public abstract object GetSerialisableValue(); + public abstract void Setup(); + public abstract void SetDefault(); + public abstract void DeserialiseValue(object value); + public abstract bool IsDefault(); + public abstract Drawable GetAssociatedCard(); + + public bool Enabled => DependsOn?.Invoke() ?? true; +} + +public abstract class ModuleAttribute : ModuleAttribute +{ + public Bindable Attribute = null!; + + public required T Default { internal get; init; } + + public virtual T Value => throw new NotImplementedException(); + + protected abstract Bindable CreateBindable(); + + public override Type GetValueType() => typeof(T); + + public override object GetSerialisableValue() => Attribute.Value!; + public override void Setup() => Attribute = CreateBindable(); + public override void SetDefault() => Attribute.SetDefault(); + public override void DeserialiseValue(object value) => Attribute.Value = (T)value; + public override bool IsDefault() => Attribute.IsDefault; +} + +public class ModuleBoolAttribute : ModuleAttribute +{ + public override bool Value => Attribute.Value; + protected override Bindable CreateBindable() => new(Default); + public override Drawable GetAssociatedCard() => new BoolAttributeCard(this); +} + +public class ModuleIntAttribute : ModuleAttribute +{ + public override int Value => Attribute.Value; + protected override BindableNumber CreateBindable() => new(Default); + public override Drawable GetAssociatedCard() => new IntTextAttributeCard(this); + public override void DeserialiseValue(object value) => Attribute.Value = Convert.ToInt32(value); +} + +public class ModuleFloatAttribute : ModuleAttribute +{ + public override float Value => Attribute.Value; + protected override BindableNumber CreateBindable() => new(Default); + public override Drawable GetAssociatedCard() => new FloatTextAttributeCard(this); + public override void DeserialiseValue(object value) => Attribute.Value = Convert.ToSingle(value); +} + +public class ModuleStringAttribute : ModuleAttribute +{ + public override string Value => Attribute.Value; + protected override Bindable CreateBindable() => new(Default); + public override Drawable GetAssociatedCard() => new StringTextAttributeCard(this); +} + +public class ModuleStringWithButtonAttribute : ModuleStringAttribute +{ + public required string ButtonText { internal get; init; } + public required Action ButtonCallback { internal get; init; } + public override Drawable GetAssociatedCard() => new StringTextWithButtonAttributeCard(this); +} + +public class ModuleEnumAttribute : ModuleAttribute where T : Enum +{ + public override T Value => Attribute.Value; + protected override Bindable CreateBindable() => new(Default); + public override Drawable GetAssociatedCard() => new DropdownAttributeCard(this); + public override void DeserialiseValue(object value) => Attribute.Value = (T)Enum.ToObject(typeof(T), Convert.ToInt32(value)); +} + +public class ModuleIntRangeAttribute : ModuleIntAttribute +{ + public required int Min { internal get; init; } + public required int Max { internal get; init; } + + protected override BindableNumber CreateBindable() + { + var baseBindable = base.CreateBindable(); + baseBindable.MinValue = Min; + baseBindable.MaxValue = Max; + return baseBindable; + } + + public override Drawable GetAssociatedCard() => new IntSliderAttributeCard(this); +} + +public class ModuleFloatRangeAttribute : ModuleFloatAttribute +{ + public required float Min { internal get; init; } + public required float Max { internal get; init; } + + protected override BindableNumber CreateBindable() + { + var baseBindable = base.CreateBindable(); + baseBindable.MinValue = Min; + baseBindable.MaxValue = Max; + return baseBindable; + } + + public override Drawable GetAssociatedCard() => new FloatSliderAttributeCard(this); +} + +public abstract class ModuleAttributeList : ModuleAttribute +{ + public BindableList Attribute = null!; + public required List Default { get; init; } + + public override Type GetValueType() => typeof(T); + public override object GetSerialisableValue() => Attribute.ToList(); + public override void Setup() => Attribute = CreateBindableList(); + + protected abstract BindableList CreateBindableList(); + protected abstract IEnumerable GetClonedDefaults(); + protected abstract IEnumerable JArrayToType(JArray array); + + public override void SetDefault() => Attribute.ReplaceItems(GetClonedDefaults()); + public override void DeserialiseValue(object value) => Attribute.ReplaceItems(JArrayToType((JArray)value)); +} + +public abstract class ModuleAttributePrimitiveList : ModuleAttributeList> +{ + public override Type GetValueType() => typeof(T); + public override object GetSerialisableValue() => Attribute.ToList(); + public override void Setup() => Attribute = CreateBindableList(); + protected override IEnumerable> JArrayToType(JArray array) => array.Select(value => new Bindable(value.Value()!)).ToList(); +} + +public class ModuleStringListAttribute : ModuleAttributePrimitiveList +{ + public override Drawable GetAssociatedCard() => new StringTextAttributeCardList(this); + public override bool IsDefault() => Attribute.Count == Default.Count && !Attribute.Where((t, i) => !t.Value.Equals(Default.ElementAt(i).Value)).Any(); + + protected override BindableList> CreateBindableList() => new(Default); + protected override IEnumerable> GetClonedDefaults() => Default.Select(defaultValue => defaultValue.GetUnboundCopy()).ToList(); +} + +public class MutableKeyValuePairListAttribute : ModuleAttributeList +{ + public required string KeyPlaceholder { internal get; init; } + public required string ValuePlaceholder { internal get; init; } + + public override Drawable GetAssociatedCard() => new MutableKeyValuePairAttributeCardList(this); + public override bool IsDefault() => Attribute.Count == Default.Count && !Attribute.Where((t, i) => !t.Equals(Default.ElementAt(i))).Any(); + + protected override BindableList CreateBindableList() => new(Default); + protected override IEnumerable JArrayToType(JArray array) => array.Select(value => new MutableKeyValuePair(value.ToObject()!)).ToList(); + protected override IEnumerable GetClonedDefaults() => Default.Select(defaultValue => new MutableKeyValuePair(defaultValue)).ToList(); +} + +public class ModuleParameter : ModuleStringAttribute +{ + public required ParameterMode Mode { internal get; init; } + public required Type ExpectedType = null!; + + public string ParameterName => Attribute.Value; + public string FormattedAddress => $"{VRChatOscConstants.ADDRESS_AVATAR_PARAMETERS_PREFIX}/{ParameterName}"; +} diff --git a/VRCOSC.Game/Modules/Attributes/MutableKeyValuePair.cs b/VRCOSC.Game/Modules/Attributes/MutableKeyValuePair.cs new file mode 100644 index 00000000..a095a489 --- /dev/null +++ b/VRCOSC.Game/Modules/Attributes/MutableKeyValuePair.cs @@ -0,0 +1,37 @@ +// 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 Newtonsoft.Json; +using osu.Framework.Bindables; + +namespace VRCOSC.Game.Modules.Attributes; + +public class MutableKeyValuePair : IEquatable +{ + [JsonProperty("key")] + public Bindable Key = new(string.Empty); + + [JsonProperty("value")] + public Bindable Value = new(string.Empty); + + [JsonConstructor] + public MutableKeyValuePair() + { + } + + public MutableKeyValuePair(MutableKeyValuePair other) + { + Key.Value = other.Key.Value; + Value.Value = other.Value.Value; + } + + public bool Equals(MutableKeyValuePair? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Key.Value.Equals(other.Key.Value) + && Value.Value.Equals(other.Value.Value); + } +} diff --git a/VRCOSC.Game/Modules/ChatBox/ChatBoxModule.cs b/VRCOSC.Game/Modules/ChatBox/ChatBoxModule.cs index 1c95e008..f2a1a6fb 100644 --- a/VRCOSC.Game/Modules/ChatBox/ChatBoxModule.cs +++ b/VRCOSC.Game/Modules/ChatBox/ChatBoxModule.cs @@ -11,7 +11,7 @@ public abstract class ChatBoxModule : Module protected void CreateState(Enum lookup, string name, string defaultFormat) => ChatBoxManager.RegisterState(SerialisedName, lookup.ToLookup(), name, defaultFormat); protected void CreateEvent(Enum lookup, string name, string defaultFormat, int defaultLength) => ChatBoxManager.RegisterEvent(SerialisedName, lookup.ToLookup(), name, defaultFormat, defaultLength); - protected void SetVariableValue(Enum lookup, string? value) => ChatBoxManager.SetVariable(SerialisedName, lookup.ToLookup(), value); + protected void SetVariableValue(Enum lookup, string? value, string suffix = "") => ChatBoxManager.SetVariable(SerialisedName, lookup.ToLookup(), value, suffix); protected string GetVariableFormat(Enum lookup) => ChatBoxManager.VariableMetadata[SerialisedName][lookup.ToLookup()].DisplayableFormat; protected void SetAllVariableValues(string? value) where T : Enum => setAllVariableValues(typeof(T), value); diff --git a/VRCOSC.Game/Modules/Module.cs b/VRCOSC.Game/Modules/Module.cs index e435ba35..0a13e956 100644 --- a/VRCOSC.Game/Modules/Module.cs +++ b/VRCOSC.Game/Modules/Module.cs @@ -10,10 +10,14 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Platform; using osu.Framework.Threading; +using VRCOSC.Game.Graphics.Notifications; using VRCOSC.Game.Managers; +using VRCOSC.Game.Modules.Attributes; using VRCOSC.Game.Modules.Avatar; +using VRCOSC.Game.Modules.Serialisation.V1; using VRCOSC.Game.OpenVR; using VRCOSC.Game.OSC.VRChat; +using VRCOSC.Game.Serialisation; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable InconsistentNaming @@ -24,7 +28,6 @@ public abstract class Module : IComparable { private GameHost Host = null!; private GameManager GameManager = null!; - protected IVRCOSCSecrets Secrets { get; private set; } = null!; private Scheduler Scheduler = null!; private TerminalLogger Terminal = null!; @@ -38,14 +41,14 @@ public abstract class Module : IComparable internal readonly BindableBool Enabled = new(); internal readonly Dictionary Settings = new(); - internal readonly Dictionary Parameters = new(); - internal readonly Dictionary ParametersLookup = new(); + internal readonly Dictionary Parameters = new(); public virtual string Title => string.Empty; public virtual string Description => string.Empty; public virtual string Author => string.Empty; public virtual string Prefab => string.Empty; public virtual ModuleType Type => ModuleType.General; + public virtual IEnumerable Info => new List(); protected virtual TimeSpan DeltaUpdate => TimeSpan.MaxValue; protected virtual bool ShouldUpdateImmediately => true; @@ -60,15 +63,16 @@ public abstract class Module : IComparable protected bool IsStopping => State.Value == ModuleState.Stopping; protected bool HasStopped => State.Value == ModuleState.Stopped; - internal bool HasSettings => Settings.Any(); - internal bool HasParameters => Parameters.Any(); + private SerialisationManager serialisationManager = null!; - public void InjectDependencies(GameHost host, GameManager gameManager, IVRCOSCSecrets secrets, Scheduler scheduler) + public void InjectDependencies(GameHost host, GameManager gameManager, Scheduler scheduler, Storage storage, NotificationContainer notifications) { Host = host; GameManager = gameManager; - Secrets = secrets; Scheduler = scheduler; + + serialisationManager = new SerialisationManager(); + serialisationManager.RegisterSerialiser(1, new ModuleSerialiser(storage, notifications, this)); } public void Load() @@ -76,47 +80,142 @@ public void Load() Terminal = new TerminalLogger(Title); CreateAttributes(); + Settings.Values.ForEach(setting => setting.Setup()); + Parameters.Values.ForEach(parameter => parameter.Setup()); - Parameters.ForEach(pair => ParametersLookup.Add(pair.Key.ToLookup(), pair.Key)); State.ValueChanged += _ => Log(State.Value.ToString()); + + Deserialise(); + } + + #region Serialisation + + public void Serialise() + { + serialisationManager.Serialise(); } + public void Deserialise() + { + serialisationManager.Deserialise(); + } + + #endregion + #region Attributes protected virtual void CreateAttributes() { } + protected void CreateSetting(Enum lookup, ModuleAttribute attribute) => Settings.Add(lookup.ToLookup(), attribute); + protected void CreateSetting(Enum lookup, string displayName, string description, bool defaultValue, Func? dependsOn = null) - => addSingleSetting(lookup, displayName, description, defaultValue, dependsOn); + => Settings.Add(lookup.ToLookup(), new ModuleBoolAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn + }); protected void CreateSetting(Enum lookup, string displayName, string description, int defaultValue, Func? dependsOn = null) - => addSingleSetting(lookup, displayName, description, defaultValue, dependsOn); + => Settings.Add(lookup.ToLookup(), new ModuleIntAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn + }); + + protected void CreateSetting(Enum lookup, string displayName, string description, float defaultValue, Func? dependsOn = null) + => Settings.Add(lookup.ToLookup(), new ModuleFloatAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn + }); protected void CreateSetting(Enum lookup, string displayName, string description, string defaultValue, Func? dependsOn = null) - => addSingleSetting(lookup, displayName, description, defaultValue, dependsOn); - - protected void CreateSetting(Enum lookup, string displayName, string description, Enum defaultValue, Func? dependsOn = null) - => addSingleSetting(lookup, displayName, description, defaultValue, dependsOn); + => Settings.Add(lookup.ToLookup(), new ModuleStringAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn + }); + + protected void CreateSetting(Enum lookup, string displayName, string description, string defaultValue, string buttonText, Action buttonCallback, Func? dependsOn = null) + => Settings.Add(lookup.ToLookup(), new ModuleStringWithButtonAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn, + ButtonText = buttonText, + ButtonCallback = buttonCallback + }); + + protected void CreateSetting(Enum lookup, string displayName, string description, T defaultValue, Func? dependsOn = null) where T : Enum + => Settings.Add(lookup.ToLookup(), new ModuleEnumAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn + }); + + protected void CreateSetting(Enum lookup, string displayName, string description, IEnumerable defaultValue, Func? dependsOn = null) + => Settings.Add(lookup.ToLookup(), new ModuleStringListAttribute + { + Name = displayName, + Description = description, + Default = defaultValue.Select(v => new Bindable(v)).ToList(), + DependsOn = dependsOn + }); + + protected void CreateSetting(Enum lookup, string displayName, string description, List defaultValue, string keyPlaceholder, string valuePlaceholder, Func? dependsOn = null) + => Settings.Add(lookup.ToLookup(), new MutableKeyValuePairListAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn, + KeyPlaceholder = keyPlaceholder, + ValuePlaceholder = valuePlaceholder + }); protected void CreateSetting(Enum lookup, string displayName, string description, int defaultValue, int minValue, int maxValue, Func? dependsOn = null) - => addRangedSetting(lookup, displayName, description, defaultValue, minValue, maxValue, dependsOn); + => Settings.Add(lookup.ToLookup(), new ModuleIntRangeAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn, + Min = minValue, + Max = maxValue + }); protected void CreateSetting(Enum lookup, string displayName, string description, float defaultValue, float minValue, float maxValue, Func? dependsOn = null) - => addRangedSetting(lookup, displayName, description, defaultValue, minValue, maxValue, dependsOn); - - protected void CreateSetting(Enum lookup, string displayName, string description, string defaultValue, string buttonText, Action buttonAction, Func? dependsOn = null) - => addTextAndButtonSetting(lookup, displayName, description, defaultValue, buttonText, buttonAction, dependsOn); + => Settings.Add(lookup.ToLookup(), new ModuleFloatRangeAttribute + { + Name = displayName, + Description = description, + Default = defaultValue, + DependsOn = dependsOn, + Min = minValue, + Max = maxValue + }); protected void CreateParameter(Enum lookup, ParameterMode mode, string parameterName, string displayName, string description) - => Parameters.Add(lookup, new ParameterAttribute(mode, new ModuleAttributeMetadata(displayName, description), parameterName, typeof(T), null)); - - private void addSingleSetting(Enum lookup, string displayName, string description, object defaultValue, Func? dependsOn) - => Settings.Add(lookup.ToLookup(), new ModuleAttribute(new ModuleAttributeMetadata(displayName, description), defaultValue, dependsOn)); - - private void addRangedSetting(Enum lookup, string displayName, string description, T defaultValue, T minValue, T maxValue, Func? dependsOn) where T : struct - => Settings.Add(lookup.ToLookup(), new ModuleAttributeWithBounds(new ModuleAttributeMetadata(displayName, description), defaultValue, minValue, maxValue, dependsOn)); - - private void addTextAndButtonSetting(Enum lookup, string displayName, string description, string defaultValue, string buttonText, Action buttonAction, Func? dependsOn) - => Settings.Add(lookup.ToLookup(), new ModuleAttributeWithButton(new ModuleAttributeMetadata(displayName, description), defaultValue, buttonText, buttonAction, dependsOn)); + => Parameters.Add(lookup, new ModuleParameter + { + Name = displayName, + Description = description, + Default = parameterName, + DependsOn = null, + Mode = mode, + ExpectedType = typeof(T) + }); public bool DoesSettingExist(string lookup, [NotNullWhen(returnValue: true)] out ModuleAttribute? attribute) { @@ -130,17 +229,17 @@ public bool DoesSettingExist(string lookup, [NotNullWhen(returnValue: true)] out return false; } - public bool DoesParameterExist(string lookup, [NotNullWhen(returnValue: true)] out ModuleAttribute? key) + public bool DoesParameterExist(string lookup, [NotNullWhen(returnValue: true)] out ModuleAttribute? attribute) { foreach (var (lookupToCheck, _) in Parameters) { if (lookupToCheck.ToLookup() != lookup) continue; - key = Parameters[lookupToCheck]; + attribute = Parameters[lookupToCheck]; return true; } - key = null; + attribute = null; return false; } @@ -184,14 +283,36 @@ protected virtual void OnPlayerUpdate() { } #region Settings + protected List GetSettingList(Enum lookup) + { + var setting = Settings[lookup.ToLookup()]; + + if (setting.GetType().IsSubclassOf(typeof(ModuleAttributePrimitiveList<>))) + { + if (setting is ModuleAttributeList> settingList) + { + return settingList.Attribute.Select(bindable => bindable.Value).ToList(); + } + } + else + { + if (setting is ModuleAttributeList settingList) + { + return settingList.Attribute.ToList(); + } + } + + throw new InvalidCastException($"Setting with lookup '{lookup}' is not of type '{nameof(List)}'"); + } + protected T GetSetting(Enum lookup) { - object? value = Settings[lookup.ToLookup()].Attribute.Value; + var setting = Settings[lookup.ToLookup()]; - if (value is not T valueCast) + if (!setting.IsValueType()) throw new InvalidCastException($"Setting with lookup '{lookup}' is not of type '{nameof(T)}'"); - return valueCast; + return ((ModuleAttribute)setting).Value; } #endregion @@ -229,11 +350,13 @@ internal void OnParameterReceived(VRChatOscData data) if (!data.IsAvatarParameter) return; + OnAnyParameterReceived(data); + Enum lookup; try { - lookup = Parameters.Single(pair => pair.Value.Name == data.ParameterName).Key; + lookup = Parameters.Single(pair => pair.Value.ParameterName == data.ParameterName).Key; } catch (InvalidOperationException) { @@ -244,7 +367,7 @@ internal void OnParameterReceived(VRChatOscData data) if (!parameterData.Mode.HasFlagFast(ParameterMode.Read)) return; - if (data.ParameterValue.GetType() != parameterData.ExpectedType) + if (!data.IsValueType(parameterData.ExpectedType)) { Log($@"Cannot accept input parameter. `{lookup}` expects type `{parameterData.ExpectedType}` but received type `{data.ParameterValue.GetType()}`"); return; @@ -266,6 +389,7 @@ internal void OnParameterReceived(VRChatOscData data) } } + protected virtual void OnAnyParameterReceived(VRChatOscData data) { } protected virtual void OnBoolParameterReceived(Enum key, bool value) { } protected virtual void OnIntParameterReceived(Enum key, int value) { } protected virtual void OnFloatParameterReceived(Enum key, float value) { } diff --git a/VRCOSC.Game/Modules/ModuleAttribute.cs b/VRCOSC.Game/Modules/ModuleAttribute.cs deleted file mode 100644 index ef4052c5..00000000 --- a/VRCOSC.Game/Modules/ModuleAttribute.cs +++ /dev/null @@ -1,86 +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 osu.Framework.Bindables; -using VRCOSC.Game.OSC.VRChat; - -namespace VRCOSC.Game.Modules; - -public class ModuleAttribute -{ - public readonly ModuleAttributeMetadata Metadata; - public readonly Bindable Attribute; - public readonly Type Type; - private readonly Func? dependsOn; - - public ModuleAttribute(ModuleAttributeMetadata metadata, object defaultValue, Func? dependsOn) - { - Metadata = metadata; - Type = defaultValue.GetType(); - - Attribute = new Bindable - { - Value = defaultValue, - Default = defaultValue - }; - - this.dependsOn = dependsOn; - } - - public bool Enabled => dependsOn?.Invoke() ?? true; -} - -public sealed class ParameterAttribute : ModuleAttribute -{ - public readonly ParameterMode Mode; - public readonly Type ExpectedType; - - public string Name => (string)Attribute.Value; - public string FormattedAddress => $"{VRChatOscConstants.ADDRESS_AVATAR_PARAMETERS_PREFIX}/{Attribute.Value}"; - - public ParameterAttribute(ParameterMode mode, ModuleAttributeMetadata metadata, string defaultName, Type expectedType, Func? dependsOn) - : base(metadata, defaultName, dependsOn) - { - Mode = mode; - ExpectedType = expectedType; - } -} - -public sealed class ModuleAttributeWithButton : ModuleAttribute -{ - public readonly Action ButtonAction; - public readonly string ButtonText; - - public ModuleAttributeWithButton(ModuleAttributeMetadata metadata, object defaultValue, string buttonText, Action buttonAction, Func? dependsOn) - : base(metadata, defaultValue, dependsOn) - { - ButtonText = buttonText; - ButtonAction = buttonAction; - } -} - -public sealed class ModuleAttributeWithBounds : ModuleAttribute -{ - public readonly object MinValue; - public readonly object MaxValue; - - public ModuleAttributeWithBounds(ModuleAttributeMetadata metadata, object defaultValue, object minValue, object maxValue, Func? dependsOn) - : base(metadata, defaultValue, dependsOn) - { - MinValue = minValue; - MaxValue = maxValue; - } -} - -public sealed class ModuleAttributeMetadata -{ - public readonly string DisplayName; - public readonly string Description; - - public ModuleAttributeMetadata(string displayName, string description) - { - DisplayName = displayName; - Description = description; - } -} diff --git a/VRCOSC.Game/Modules/ModuleCollection.cs b/VRCOSC.Game/Modules/ModuleCollection.cs new file mode 100644 index 00000000..66fb4988 --- /dev/null +++ b/VRCOSC.Game/Modules/ModuleCollection.cs @@ -0,0 +1,25 @@ +// 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; +using System.Collections.Generic; +using System.Reflection; +using osu.Framework.Lists; + +namespace VRCOSC.Game.Modules; + +public class ModuleCollection : IEnumerable +{ + public readonly Assembly Assembly; + public readonly SortedList Modules = new(); + + public string Title => Assembly.GetCustomAttribute()?.Product ?? "UNKNOWN"; + + public ModuleCollection(Assembly assembly) + { + Assembly = assembly; + } + + public IEnumerator GetEnumerator() => Modules.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/VRCOSC.Game/Modules/Serialisation/Legacy/LegacyModuleManagerSerialiser.cs b/VRCOSC.Game/Modules/Serialisation/Legacy/LegacyModuleManagerSerialiser.cs new file mode 100644 index 00000000..1ee9b1f9 --- /dev/null +++ b/VRCOSC.Game/Modules/Serialisation/Legacy/LegacyModuleManagerSerialiser.cs @@ -0,0 +1,54 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Platform; +using VRCOSC.Game.Graphics.Notifications; +using VRCOSC.Game.Managers; +using VRCOSC.Game.Modules.Serialisation.Legacy.Models; +using VRCOSC.Game.Serialisation; + +namespace VRCOSC.Game.Modules.Serialisation.Legacy; + +public class LegacyModuleManagerSerialiser : Serialiser +{ + protected override string FileName => "modules.json"; + + public LegacyModuleManagerSerialiser(Storage storage, NotificationContainer notification, ModuleManager reference) + : base(storage, notification, reference) + { + } + + protected override LegacySerialisableModuleManager GetSerialisableData(ModuleManager moduleManager) => new(moduleManager); + + protected override void ExecuteAfterDeserialisation(ModuleManager moduleManager, LegacySerialisableModuleManager data) + { + data.Modules.ForEach(modulePair => + { + var (moduleName, moduleData) = modulePair; + + var module = moduleManager.GetModule(moduleName); + if (module is null) return; + + module.Enabled.Value = moduleData.Enabled; + + moduleData.Settings.ForEach(settingPair => + { + var (settingKey, settingValue) = settingPair; + + if (!module.DoesSettingExist(settingKey, out var setting)) return; + + setting.DeserialiseValue(settingValue); + }); + + moduleData.Parameters.ForEach(parameterPair => + { + var (parameterKey, parameterValue) = parameterPair; + + if (!module.DoesParameterExist(parameterKey, out var parameter)) return; + + parameter.DeserialiseValue(parameterValue); + }); + }); + } +} diff --git a/VRCOSC.Game/Modules/Serialisation/Legacy/LegacyModuleSerialiser.cs b/VRCOSC.Game/Modules/Serialisation/Legacy/LegacyModuleSerialiser.cs deleted file mode 100644 index a9ac0c9e..00000000 --- a/VRCOSC.Game/Modules/Serialisation/Legacy/LegacyModuleSerialiser.cs +++ /dev/null @@ -1,229 +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.IO; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Logging; -using osu.Framework.Platform; - -namespace VRCOSC.Game.Modules.Serialisation.Legacy; - -public class LegacyModuleSerialiser -{ - private const string directory_name = "modules"; - private readonly Storage storage; - - public LegacyModuleSerialiser(Storage storage) - { - this.storage = storage.GetStorageForDirectory(directory_name); - } - - public void Deserialise(Module module) - { - using (var stream = storage.GetStream(module.FileName)) - { - if (stream is not null) - { - using var reader = new StreamReader(stream); - - while (reader.ReadLine() is { } line) - { - switch (line) - { - case "#InternalSettings": - performInternalSettingsLoad(reader, module); - break; - - case "#Settings": - performSettingsLoad(reader, module); - break; - - case "#Parameters": - performParametersLoad(reader, module); - break; - } - } - } - } - - Serialise(module); - } - - private static void performInternalSettingsLoad(TextReader reader, Module module) - { - while (reader.ReadLine() is { } line) - { - if (line.Equals("#End")) break; - - var lineSplit = line.Split(new[] { '=' }, 2); - var lookup = lineSplit[0]; - var value = lineSplit[1]; - - switch (lookup) - { - case "enabled": - module.Enabled.Value = bool.Parse(value); - break; - } - } - } - - private static void performSettingsLoad(TextReader reader, Module module) - { - while (reader.ReadLine() is { } line) - { - if (line.Equals("#End")) break; - - var lineSplitLookupValue = line.Split(new[] { '=' }, 2); - var lookupType = lineSplitLookupValue[0].Split(new[] { ':' }, 2); - var value = lineSplitLookupValue[1]; - - var lookupStr = lookupType[0]; - var typeStr = lookupType[1]; - - var lookup = lookupStr; - if (lookupStr.Contains('#')) lookup = lookupStr.Split(new[] { '#' }, 2)[0]; - - if (!module.Settings.ContainsKey(lookup)) continue; - - var setting = module.Settings[lookup]; - - var readableTypeName = setting.Attribute.Value.GetType().ToReadableName().ToLowerInvariant(); - if (!readableTypeName.Equals(typeStr)) continue; - - switch (typeStr) - { - case "enum": - var typeAndValue = value.Split(new[] { '#' }, 2); - var enumName = typeAndValue[0].Split('+')[1]; - var enumType = enumNameToType(enumName); - if (enumType is not null) setting.Attribute.Value = Enum.ToObject(enumType, int.Parse(typeAndValue[1])); - break; - - case "string": - setting.Attribute.Value = value; - break; - - case "int": - setting.Attribute.Value = int.Parse(value); - break; - - case "float": - setting.Attribute.Value = float.Parse(value); - break; - - case "bool": - setting.Attribute.Value = bool.Parse(value); - break; - - default: - Logger.Log($"Unknown type found in file: {typeStr}"); - break; - } - } - } - - private static void performParametersLoad(TextReader reader, Module module) - { - while (reader.ReadLine() is { } line) - { - if (line.Equals("#End")) break; - - var lineSplit = line.Split(new[] { '=' }, 2); - var lookup = lineSplit[0]; - var value = lineSplit[1]; - - if (!module.ParametersLookup.ContainsKey(lookup)) continue; - - var parameter = module.Parameters[module.ParametersLookup[lookup]]; - parameter.Attribute.Value = value; - } - } - - private static Type? enumNameToType(string enumName) - { - Type? returnType = null; - - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - try - { - assembly.GetTypes().ForEach(type => - { - if (!type.IsSubclassOf(typeof(Enum))) return; - - if (type.Name.Equals(enumName, StringComparison.Ordinal)) returnType = type; - }); - } - catch { } - } - - return returnType; - } - - public void Serialise(Module module) - { - using var stream = storage.CreateFileSafely(module.FileName); - using var writer = new StreamWriter(stream); - - performInternalSettingsSave(writer, module); - performSettingsSave(writer, module); - performParametersSave(writer, module); - } - - private static void performInternalSettingsSave(TextWriter writer, Module module) - { - writer.WriteLine(@"#InternalSettings"); - writer.WriteLine(@"{0}={1}", "enabled", module.Enabled.Value.ToString()); - writer.WriteLine(@"#End"); - } - - private static void performSettingsSave(TextWriter writer, Module module) - { - var areAllDefault = module.Settings.All(pair => pair.Value.Attribute.IsDefault); - if (areAllDefault) return; - - writer.WriteLine(@"#Settings"); - - foreach (var (lookup, moduleAttributeData) in module.Settings) - { - if (moduleAttributeData.Attribute.IsDefault) continue; - - var value = moduleAttributeData.Attribute.Value; - var valueType = value.GetType(); - var readableTypeName = valueType.ToReadableName().ToLowerInvariant(); - - if (valueType.IsSubclassOf(typeof(Enum))) - { - var enumClass = valueType.FullName; - writer.WriteLine(@"{0}:{1}={2}#{3}", lookup, readableTypeName, enumClass, (int)value); - } - else - { - writer.WriteLine(@"{0}:{1}={2}", lookup, readableTypeName, value); - } - } - - writer.WriteLine(@"#End"); - } - - private static void performParametersSave(TextWriter writer, Module module) - { - var areAllDefault = module.Parameters.All(pair => pair.Value.Attribute.IsDefault); - if (areAllDefault) return; - - writer.WriteLine(@"#Parameters"); - - foreach (var (lookup, parameterAttribute) in module.Parameters) - { - if (parameterAttribute.Attribute.IsDefault) continue; - - var value = parameterAttribute.Attribute.Value; - writer.WriteLine(@"{0}={1}", lookup.ToLookup(), value); - } - - writer.WriteLine(@"#End"); - } -} diff --git a/VRCOSC.Game/Modules/Serialisation/Legacy/Models/LegacySerialisableModule.cs b/VRCOSC.Game/Modules/Serialisation/Legacy/Models/LegacySerialisableModule.cs new file mode 100644 index 00000000..f9abc7d5 --- /dev/null +++ b/VRCOSC.Game/Modules/Serialisation/Legacy/Models/LegacySerialisableModule.cs @@ -0,0 +1,44 @@ +// 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; +using osu.Framework.Extensions.IEnumerableExtensions; + +namespace VRCOSC.Game.Modules.Serialisation.Legacy.Models; + +public class LegacySerialisableModule +{ + [JsonProperty("enabled")] + public bool Enabled; + + [JsonProperty("settings")] + public Dictionary Settings = new(); + + [JsonProperty("parameters")] + public Dictionary Parameters = new(); + + [JsonConstructor] + public LegacySerialisableModule() + { + } + + public LegacySerialisableModule(Module module) + { + Enabled = module.Enabled.Value; + + module.Settings.ForEach(pair => + { + if (pair.Value.IsDefault()) return; + + Settings.Add(pair.Key, pair.Value.GetSerialisableValue()); + }); + + module.Parameters.ForEach(pair => + { + if (pair.Value.IsDefault()) return; + + Parameters.Add(pair.Key.ToLookup(), pair.Value.GetSerialisableValue()); + }); + } +} diff --git a/VRCOSC.Game/Modules/Serialisation/V1/Models/SerialisableModuleManager.cs b/VRCOSC.Game/Modules/Serialisation/Legacy/Models/LegacySerialisableModuleManager.cs similarity index 51% rename from VRCOSC.Game/Modules/Serialisation/V1/Models/SerialisableModuleManager.cs rename to VRCOSC.Game/Modules/Serialisation/Legacy/Models/LegacySerialisableModuleManager.cs index f1112b26..c7fc5ae8 100644 --- a/VRCOSC.Game/Modules/Serialisation/V1/Models/SerialisableModuleManager.cs +++ b/VRCOSC.Game/Modules/Serialisation/Legacy/Models/LegacySerialisableModuleManager.cs @@ -6,24 +6,24 @@ using osu.Framework.Extensions.IEnumerableExtensions; using VRCOSC.Game.Managers; -namespace VRCOSC.Game.Modules.Serialisation.V1.Models; +namespace VRCOSC.Game.Modules.Serialisation.Legacy.Models; -public class SerialisableModuleManager +public class LegacySerialisableModuleManager { [JsonProperty("version")] public int Version; [JsonProperty("modules")] - public Dictionary Modules = new(); + public Dictionary Modules = new(); [JsonConstructor] - public SerialisableModuleManager() + public LegacySerialisableModuleManager() { } - public SerialisableModuleManager(ModuleManager moduleManager) + public LegacySerialisableModuleManager(ModuleManager moduleManager) { Version = 1; - moduleManager.ForEach(module => Modules.Add(module.SerialisedName, new SerialisableModule(module))); + moduleManager.Modules.ForEach(module => Modules.Add(module.SerialisedName, new LegacySerialisableModule(module))); } } diff --git a/VRCOSC.Game/Modules/Serialisation/V1/Models/SerialisableModule.cs b/VRCOSC.Game/Modules/Serialisation/V1/Models/SerialisableModule.cs index 1c466253..052fa2f1 100644 --- a/VRCOSC.Game/Modules/Serialisation/V1/Models/SerialisableModule.cs +++ b/VRCOSC.Game/Modules/Serialisation/V1/Models/SerialisableModule.cs @@ -9,6 +9,9 @@ namespace VRCOSC.Game.Modules.Serialisation.V1.Models; public class SerialisableModule { + [JsonProperty("version")] + public int Version; + [JsonProperty("enabled")] public bool Enabled; @@ -25,20 +28,22 @@ public SerialisableModule() public SerialisableModule(Module module) { + Version = 1; + Enabled = module.Enabled.Value; module.Settings.ForEach(pair => { - if (pair.Value.Attribute.IsDefault) return; + if (pair.Value.IsDefault()) return; - Settings.Add(pair.Key, pair.Value.Attribute.Value); + Settings.Add(pair.Key, pair.Value.GetSerialisableValue()); }); module.Parameters.ForEach(pair => { - if (pair.Value.Attribute.IsDefault) return; + if (pair.Value.IsDefault()) return; - Parameters.Add(pair.Key.ToLookup(), pair.Value.Attribute.Value); + Parameters.Add(pair.Key.ToLookup(), pair.Value.GetSerialisableValue()); }); } } diff --git a/VRCOSC.Game/Modules/Serialisation/V1/ModuleSerialiser.cs b/VRCOSC.Game/Modules/Serialisation/V1/ModuleSerialiser.cs index e202ee1c..11b51a57 100644 --- a/VRCOSC.Game/Modules/Serialisation/V1/ModuleSerialiser.cs +++ b/VRCOSC.Game/Modules/Serialisation/V1/ModuleSerialiser.cs @@ -1,62 +1,45 @@ // 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 osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Platform; using VRCOSC.Game.Graphics.Notifications; -using VRCOSC.Game.Managers; using VRCOSC.Game.Modules.Serialisation.V1.Models; using VRCOSC.Game.Serialisation; namespace VRCOSC.Game.Modules.Serialisation.V1; -public class ModuleSerialiser : Serialiser +public class ModuleSerialiser : Serialiser { - protected override string FileName => "modules.json"; + private readonly Module moduleReference; - public ModuleSerialiser(Storage storage, NotificationContainer notification, ModuleManager reference) + protected override string Directory => "modules"; + protected override string FileName => $"{moduleReference.SerialisedName}.json"; + + public ModuleSerialiser(Storage storage, NotificationContainer notification, Module reference) : base(storage, notification, reference) { + moduleReference = reference; } - protected override SerialisableModuleManager GetSerialisableData(ModuleManager moduleManager) => new(moduleManager); + protected override SerialisableModule GetSerialisableData(Module reference) => new(reference); - protected override void ExecuteAfterDeserialisation(ModuleManager moduleManager, SerialisableModuleManager data) + protected override void ExecuteAfterDeserialisation(Module module, SerialisableModule data) { - data.Modules.ForEach(modulePair => - { - var (moduleName, moduleData) = modulePair; - - var module = moduleManager.SingleOrDefault(module => module.SerialisedName == moduleName); - if (module is null) return; - - module.Enabled.Value = moduleData.Enabled; - - moduleData.Settings.ForEach(settingPair => - { - var (settingKey, settingValue) = settingPair; + module.Enabled.Value = data.Enabled; - if (!module.DoesSettingExist(settingKey, out var setting)) return; - - if (setting.Type.IsEnum) - { - setting.Attribute.Value = Enum.ToObject(setting.Type, settingValue); - return; - } - - setting.Attribute.Value = Convert.ChangeType(settingValue, setting.Type); - }); + data.Settings.ForEach(settingPair => + { + var (settingKey, settingValue) = settingPair; - moduleData.Parameters.ForEach(parameterPair => - { - var (parameterKey, parameterValue) = parameterPair; + if (module.DoesSettingExist(settingKey, out var setting)) setting.DeserialiseValue(settingValue); + }); - if (!module.DoesParameterExist(parameterKey, out var parameter)) return; + data.Parameters.ForEach(parameterPair => + { + var (parameterKey, parameterValue) = parameterPair; - parameter.Attribute.Value = Convert.ChangeType(parameterValue, parameter.Type); - }); + if (module.DoesParameterExist(parameterKey, out var parameter)) parameter.DeserialiseValue(parameterValue); }); } } diff --git a/VRCOSC.Game/Modules/Sources/DLLModuleSource.cs b/VRCOSC.Game/Modules/Sources/DLLModuleSource.cs deleted file mode 100644 index 1a7a75b8..00000000 --- a/VRCOSC.Game/Modules/Sources/DLLModuleSource.cs +++ /dev/null @@ -1,19 +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.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace VRCOSC.Game.Modules.Sources; - -public abstract class DLLModuleSource : IModuleSource -{ - protected IEnumerable LoadModulesFromDLL(string dllPath) - { - return Assembly.LoadFile(dllPath).GetTypes().Where(type => type.IsSubclassOf(typeof(Module)) && !type.IsAbstract); - } - - public abstract IEnumerable Load(); -} diff --git a/VRCOSC.Game/Modules/Sources/ExternalModuleSource.cs b/VRCOSC.Game/Modules/Sources/ExternalModuleSource.cs deleted file mode 100644 index 4b5cf831..00000000 --- a/VRCOSC.Game/Modules/Sources/ExternalModuleSource.cs +++ /dev/null @@ -1,31 +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.Collections.Generic; -using System.IO; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Platform; - -namespace VRCOSC.Game.Modules.Sources; - -public class ExternalModuleSource : DLLModuleSource -{ - private const string custom_directory = "custom"; - - private readonly Storage storage; - - public ExternalModuleSource(Storage storage) - { - this.storage = storage; - } - - public override IEnumerable Load() - { - var moduleDirectoryPath = storage.GetStorageForDirectory(custom_directory).GetFullPath(string.Empty, true); - - var moduleList = new List(); - Directory.GetFiles(moduleDirectoryPath, "*.dll", SearchOption.AllDirectories).ForEach(dllPath => moduleList.AddRange(LoadModulesFromDLL(dllPath))); - return moduleList; - } -} diff --git a/VRCOSC.Game/Modules/Sources/IModuleSource.cs b/VRCOSC.Game/Modules/Sources/IModuleSource.cs deleted file mode 100644 index e48c8618..00000000 --- a/VRCOSC.Game/Modules/Sources/IModuleSource.cs +++ /dev/null @@ -1,12 +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.Collections.Generic; - -namespace VRCOSC.Game.Modules.Sources; - -public interface IModuleSource -{ - public IEnumerable Load(); -} diff --git a/VRCOSC.Game/Modules/Sources/InternalModuleSource.cs b/VRCOSC.Game/Modules/Sources/InternalModuleSource.cs deleted file mode 100644 index 954b0686..00000000 --- a/VRCOSC.Game/Modules/Sources/InternalModuleSource.cs +++ /dev/null @@ -1,26 +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.Collections.Generic; -using System.IO; -using System.Linq; -using osu.Framework.Logging; - -namespace VRCOSC.Game.Modules.Sources; - -public class InternalModuleSource : DLLModuleSource -{ - public override IEnumerable Load() - { - var dllPath = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.AllDirectories).FirstOrDefault(fileName => fileName.Contains("VRCOSC.Modules")); - - if (string.IsNullOrEmpty(dllPath)) - { - Logger.Log("Could not find internal module assembly"); - return new List(); - } - - return LoadModulesFromDLL(dllPath); - } -} diff --git a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs index 4f52bde6..e54a8c9f 100644 --- a/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs +++ b/VRCOSC.Game/OSC/VRChat/VRChatOscClient.cs @@ -22,7 +22,7 @@ public VRChatOscClient() if (!data.Values.Any()) return; - OnParameterReceived?.Invoke(new VRChatOscData(data)); + OnParameterReceived?.Invoke(data); }; } diff --git a/VRCOSC.Game/OSC/VRChat/VRChatOscData.cs b/VRCOSC.Game/OSC/VRChat/VRChatOscData.cs index a86e6611..a2667807 100644 --- a/VRCOSC.Game/OSC/VRChat/VRChatOscData.cs +++ b/VRCOSC.Game/OSC/VRChat/VRChatOscData.cs @@ -1,6 +1,7 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. +using System; using VRCOSC.Game.OSC.Client; namespace VRCOSC.Game.OSC.VRChat; @@ -13,6 +14,10 @@ public class VRChatOscData : OscData public string ParameterName => Address.Remove(0, VRChatOscConstants.ADDRESS_AVATAR_PARAMETERS_PREFIX.Length + 1); public object ParameterValue => Values[0]; + public bool IsValueType(Type type) => ParameterValue.GetType() == type; + public bool IsValueType() => IsValueType(typeof(T)); + public T ValueAs() => (T)ParameterValue; + public VRChatOscData(OscData data) : base(data.Address, data.Values) { diff --git a/VRCOSC.Game/Processes/WinForms.cs b/VRCOSC.Game/Processes/WinForms.cs new file mode 100644 index 00000000..ada8cc46 --- /dev/null +++ b/VRCOSC.Game/Processes/WinForms.cs @@ -0,0 +1,31 @@ +// 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.Threading; +using System.Windows.Forms; + +namespace VRCOSC.Game.Processes; + +public static class WinForms +{ + [STAThread] + public static void OpenFileDialog(string filter, Action fileNameCallback) + { + var t = new Thread(() => + { + var dlg = new OpenFileDialog + { + Multiselect = false, + Filter = filter + }; + + if (dlg.ShowDialog() != DialogResult.OK) return; + + fileNameCallback.Invoke(dlg.FileName); + }); + + t.SetApartmentState(ApartmentState.STA); + t.Start(); + } +} diff --git a/VRCOSC.Game/Providers/Media/MediaProvider.cs b/VRCOSC.Game/Providers/Media/MediaProvider.cs new file mode 100644 index 00000000..ba98cee8 --- /dev/null +++ b/VRCOSC.Game/Providers/Media/MediaProvider.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.Threading.Tasks; + +namespace VRCOSC.Game.Providers.Media; + +public abstract class MediaProvider +{ + public MediaState State = new(); + + public abstract Task InitialiseAsync(); + public abstract Task TerminateAsync(); + + public abstract void Play(); + public abstract void Pause(); + public abstract void SkipNext(); + public abstract void SkipPrevious(); + public abstract void ChangeShuffle(bool active); + public abstract void ChangePlaybackPosition(TimeSpan playbackPosition); + public abstract void ChangeRepeatMode(MediaRepeatMode mode); + public abstract void TryChangeVolume(float percentage); + public abstract float TryGetVolume(); + + public Action? OnPlaybackStateChange; + public Action? OnPlaybackPositionChange; + public Action? OnTrackChange; +} diff --git a/VRCOSC.Game/Providers/Media/MediaState.cs b/VRCOSC.Game/Providers/Media/MediaState.cs new file mode 100644 index 00000000..9199cce6 --- /dev/null +++ b/VRCOSC.Game/Providers/Media/MediaState.cs @@ -0,0 +1,48 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System; + +namespace VRCOSC.Game.Providers.Media; + +public class MediaState +{ + public string? Identifier { get; internal set; } + public string Title { get; internal set; } = string.Empty; + public string Artist { get; internal set; } = string.Empty; + public int TrackNumber { get; internal set; } + public string AlbumTitle { get; internal set; } = string.Empty; + public string AlbumArtist { get; internal set; } = string.Empty; + public int AlbumTrackCount { get; internal set; } + public MediaRepeatMode RepeatMode { get; internal set; } + public bool IsShuffle { get; internal set; } + public MediaPlaybackStatus Status { get; internal set; } + public MediaTimelineProperties Timeline { get; } = new(); + + public bool IsStopped => Status == MediaPlaybackStatus.Stopped; + public bool IsPlaying => Status == MediaPlaybackStatus.Playing; + public bool IsPaused => Status == MediaPlaybackStatus.Paused; +} + +public enum MediaRepeatMode +{ + Off, + Single, + Multiple +} + +public enum MediaPlaybackStatus +{ + Stopped, + Playing, + Paused +} + +public class MediaTimelineProperties +{ + public TimeSpan Start { get; internal set; } + public TimeSpan End { get; internal set; } + public TimeSpan Position { get; internal set; } + + public float PositionPercentage => Position.Ticks / (float)End.Ticks; +} diff --git a/VRCOSC.Game/Providers/Media/WindowsMediaProvider.cs b/VRCOSC.Game/Providers/Media/WindowsMediaProvider.cs index 76ecb1f6..814b097b 100644 --- a/VRCOSC.Game/Providers/Media/WindowsMediaProvider.cs +++ b/VRCOSC.Game/Providers/Media/WindowsMediaProvider.cs @@ -13,18 +13,13 @@ namespace VRCOSC.Game.Providers.Media; -public class WindowsMediaProvider +public class WindowsMediaProvider : MediaProvider { private readonly List sessions = new(); private GlobalSystemMediaTransportControlsSessionManager? sessionManager; + private GlobalSystemMediaTransportControlsSession? controller => sessionManager?.GetCurrentSession(); - public GlobalSystemMediaTransportControlsSession? Controller => sessionManager?.GetCurrentSession(); - - public Action? OnPlaybackStateUpdate; - public Action? OnTrackChange; - public MediaState State { get; private set; } = null!; - - public async Task Hook() + public override async Task InitialiseAsync() { State = new MediaState(); sessionManager ??= await GlobalSystemMediaTransportControlsSessionManager.RequestAsync(); @@ -40,29 +35,46 @@ public async Task Hook() return true; } - public void UnHook() + public override Task TerminateAsync() { - if (sessionManager is null) return; + if (sessionManager is null) return Task.CompletedTask; sessionManager.CurrentSessionChanged -= onCurrentSessionChanged; sessionManager.SessionsChanged -= sessionsChanged; - sessions.Clear(); + + return Task.CompletedTask; } - private bool isFocusedSession(GlobalSystemMediaTransportControlsSession session) => session.SourceAppUserModelId == Controller?.SourceAppUserModelId; + private bool isFocusedSession(GlobalSystemMediaTransportControlsSession session) => session.SourceAppUserModelId == controller?.SourceAppUserModelId; private void onAnyPlaybackStateChanged(GlobalSystemMediaTransportControlsSession session, GlobalSystemMediaTransportControlsSessionPlaybackInfo args) { if (!isFocusedSession(session)) return; State.IsShuffle = args.IsShuffleActive ?? default; - State.RepeatMode = args.AutoRepeatMode ?? default; - State.Status = args.PlaybackStatus; + State.RepeatMode = convertWindowsRepeatMode(args.AutoRepeatMode); + State.Status = convertWindowsPlaybackStatus(args.PlaybackStatus); - OnPlaybackStateUpdate?.Invoke(); + OnPlaybackStateChange?.Invoke(); } + private static MediaRepeatMode convertWindowsRepeatMode(MediaPlaybackAutoRepeatMode? mode) => mode switch + { + MediaPlaybackAutoRepeatMode.None => MediaRepeatMode.Off, + MediaPlaybackAutoRepeatMode.Track => MediaRepeatMode.Single, + MediaPlaybackAutoRepeatMode.List => MediaRepeatMode.Multiple, + _ => MediaRepeatMode.Off + }; + + private static MediaPlaybackStatus convertWindowsPlaybackStatus(GlobalSystemMediaTransportControlsSessionPlaybackStatus status) => status switch + { + GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped => MediaPlaybackStatus.Stopped, + GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => MediaPlaybackStatus.Playing, + GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused => MediaPlaybackStatus.Paused, + _ => MediaPlaybackStatus.Stopped + }; + private void onAnyMediaPropertyChanged(GlobalSystemMediaTransportControlsSession session, GlobalSystemMediaTransportControlsSessionMediaProperties args) { if (!isFocusedSession(session)) return; @@ -81,25 +93,29 @@ private void onAnyTimelinePropertiesChanged(GlobalSystemMediaTransportControlsSe { if (!isFocusedSession(session)) return; - State.Position = args; + State.Timeline.Position = args.Position; + State.Timeline.End = args.EndTime; + State.Timeline.Start = args.StartTime; + + OnPlaybackPositionChange?.Invoke(); } private async void onCurrentSessionChanged(GlobalSystemMediaTransportControlsSessionManager? _, CurrentSessionChangedEventArgs? _2) { - if (Controller is null) + if (controller is null) { State = new MediaState(); return; } - State.ProcessId = Controller.SourceAppUserModelId; + State.Identifier = controller.SourceAppUserModelId; - onAnyPlaybackStateChanged(Controller, Controller.GetPlaybackInfo()); + onAnyPlaybackStateChanged(controller, controller.GetPlaybackInfo()); - try { onAnyMediaPropertyChanged(Controller, await Controller.TryGetMediaPropertiesAsync()); } + try { onAnyMediaPropertyChanged(controller, await controller.TryGetMediaPropertiesAsync()); } catch (COMException) { } - onAnyTimelinePropertiesChanged(Controller, Controller.GetTimelineProperties()); + onAnyTimelinePropertiesChanged(controller, controller.GetTimelineProperties()); } private void sessionsChanged(GlobalSystemMediaTransportControlsSessionManager? _, SessionsChangedEventArgs? _2) @@ -123,43 +139,14 @@ private void addControlSession(GlobalSystemMediaTransportControlsSession control sessions.Add(controlSession); } -} - -public class MediaState -{ - public string? ProcessId; - public string Title = string.Empty; - public string Artist = string.Empty; - public int TrackNumber; - public string AlbumTitle = string.Empty; - public string AlbumArtist = string.Empty; - public int AlbumTrackCount; - public MediaPlaybackAutoRepeatMode RepeatMode; - public bool IsShuffle; - public GlobalSystemMediaTransportControlsSessionPlaybackStatus Status; - public GlobalSystemMediaTransportControlsSessionTimelineProperties? Position; - - public bool IsPlaying => Status == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing; - - public float PositionPercentage - { - get - { - if (Position is null) return 0f; - - return Position.Position.Ticks / (float)Position.EndTime.Ticks; - } - } - public float Volume - { - set => ProcessExtensions.SetProcessVolume(ProcessId, value); - get => ProcessExtensions.RetrieveProcessVolume(ProcessId); - } - - public bool Muted - { - set => ProcessExtensions.SetProcessMuted(ProcessId, value); - get => ProcessExtensions.IsProcessMuted(ProcessId); - } + public override async void Play() => await controller?.TryPlayAsync(); + public override async void Pause() => await controller?.TryPauseAsync(); + public override async void SkipNext() => await controller?.TrySkipNextAsync(); + public override async void SkipPrevious() => await controller?.TrySkipPreviousAsync(); + public override async void ChangeShuffle(bool active) => await controller?.TryChangeShuffleActiveAsync(active); + public override async void ChangePlaybackPosition(TimeSpan playbackPosition) => await controller?.TryChangePlaybackPositionAsync(playbackPosition.Ticks); + public override async void ChangeRepeatMode(MediaRepeatMode mode) => await controller?.TryChangeAutoRepeatModeAsync((MediaPlaybackAutoRepeatMode)(int)mode); + public override void TryChangeVolume(float percentage) => ProcessExtensions.SetProcessVolume(State.Identifier, percentage); + public override float TryGetVolume() => ProcessExtensions.RetrieveProcessVolume(State.Identifier); } diff --git a/VRCOSC.Game/Router/Serialisation/Legacy/LegacyRouterSerialiser.cs b/VRCOSC.Game/Router/Serialisation/Legacy/LegacyRouterSerialiser.cs index f4a1e2f1..30f99c61 100644 --- a/VRCOSC.Game/Router/Serialisation/Legacy/LegacyRouterSerialiser.cs +++ b/VRCOSC.Game/Router/Serialisation/Legacy/LegacyRouterSerialiser.cs @@ -23,7 +23,6 @@ public LegacyRouterSerialiser(Storage storage, NotificationContainer notificatio protected override void ExecuteAfterDeserialisation(RouterManager routerManager, List data) { - routerManager.Store.Clear(); - routerManager.Store.AddRange(data); + routerManager.Store.ReplaceItems(data); } } diff --git a/VRCOSC.Game/Router/Serialisation/V1/RouterSerialiser.cs b/VRCOSC.Game/Router/Serialisation/V1/RouterSerialiser.cs index ba508c0b..02a5a165 100644 --- a/VRCOSC.Game/Router/Serialisation/V1/RouterSerialiser.cs +++ b/VRCOSC.Game/Router/Serialisation/V1/RouterSerialiser.cs @@ -1,6 +1,7 @@ // Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. // See the LICENSE file in the repository root for full license text. +using System.Linq; using osu.Framework.Platform; using VRCOSC.Game.Graphics.Notifications; using VRCOSC.Game.Managers; @@ -23,21 +24,16 @@ public RouterSerialiser(Storage storage, NotificationContainer notification, Rou protected override void ExecuteAfterDeserialisation(RouterManager routerManager, SerialisableRouterManager data) { - routerManager.Store.Clear(); - - data.Data.ForEach(routerData => + routerManager.Store.ReplaceItems(data.Data.Select(routerData => new RouterData { - routerManager.Store.Add(new RouterData + Label = { Value = routerData.Label }, + Endpoints = new OSCRouterEndpoints { - Label = { Value = routerData.Label }, - Endpoints = new OSCRouterEndpoints - { - ReceiveAddress = { Value = routerData.ReceiveAddress }, - ReceivePort = { Value = routerData.ReceivePort }, - SendAddress = { Value = routerData.SendAddress }, - SendPort = { Value = routerData.SendPort } - } - }); - }); + ReceiveAddress = { Value = routerData.ReceiveAddress }, + ReceivePort = { Value = routerData.ReceivePort }, + SendAddress = { Value = routerData.SendAddress }, + SendPort = { Value = routerData.SendPort } + } + })); } } diff --git a/VRCOSC.Game/Serialisation/ICanSerialise.cs b/VRCOSC.Game/Serialisation/ICanSerialise.cs deleted file mode 100644 index 7018a5cd..00000000 --- a/VRCOSC.Game/Serialisation/ICanSerialise.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. - -namespace VRCOSC.Game.Serialisation; - -/// -/// Represents an object that can be serialised using an -/// -public interface ICanSerialise -{ - public void Deserialise(); - public void Serialise(); -} diff --git a/VRCOSC.Game/Serialisation/ISerialiser.cs b/VRCOSC.Game/Serialisation/ISerialiser.cs index c3f7df3e..155fa0cb 100644 --- a/VRCOSC.Game/Serialisation/ISerialiser.cs +++ b/VRCOSC.Game/Serialisation/ISerialiser.cs @@ -5,8 +5,9 @@ namespace VRCOSC.Game.Serialisation; public interface ISerialiser { + public void Initialise(); public bool DoesFileExist(); public bool TryGetVersion(out int? version); - public bool Deserialise(); + public bool Deserialise(string filePathOverride); public bool Serialise(); } diff --git a/VRCOSC.Game/Serialisation/SerialisationManager.cs b/VRCOSC.Game/Serialisation/SerialisationManager.cs index 6b1160bd..16548d29 100644 --- a/VRCOSC.Game/Serialisation/SerialisationManager.cs +++ b/VRCOSC.Game/Serialisation/SerialisationManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace VRCOSC.Game.Serialisation; @@ -15,23 +16,24 @@ public class SerialisationManager public void RegisterSerialiser(int version, ISerialiser serialiser) { + serialiser.Initialise(); latestSerialiserVersion = Math.Max(version, latestSerialiserVersion); serialisers.Add(version, serialiser); } - public bool Deserialise() + public void Deserialise(string filePathOverride = "") { - var doesFileExist = false; - - foreach (var (_, serialiser) in serialisers.OrderBy(pair => pair.Key)) + if (string.IsNullOrEmpty(filePathOverride)) { - if (serialiser.DoesFileExist()) doesFileExist = true; + if (!serialisers.Values.Any(serialiser => serialiser.DoesFileExist())) + { + Serialise(); + return; + } } - - if (!doesFileExist) + else { - Serialise(); - return false; + if (!File.Exists(filePathOverride)) return; } foreach (var (version, serialiser) in serialisers.OrderBy(pair => pair.Key)) @@ -39,25 +41,30 @@ public bool Deserialise() if (!serialiser.TryGetVersion(out var foundVersion)) continue; if (version != foundVersion) continue; - return deserialise(serialiser); + deserialise(serialiser, filePathOverride); + return; } - // If there are no valid versions found there's either no file OR there is a file with no version - // Attempt to deserialise using the 0th serialiser which is reserved for files from before the serialisation standardisation and latest serialiser - + // Attempt to deserialise using the 0th serialiser which is reserved for files from before the serialisation standardisation // Note: 0th used for RouterManager migration - if (serialisers.TryGetValue(0, out var zerothSerialiser)) return deserialise(zerothSerialiser); - if (serialisers.TryGetValue(latestSerialiserVersion, out var latestSerialiser)) return deserialise(latestSerialiser); + if (serialisers.TryGetValue(0, out var zerothSerialiser)) + { + deserialise(zerothSerialiser, filePathOverride); + return; + } + + // Since we've got to this point that means a file exists that has no version, or the file is corrupt + // As a last resort, attempt to deserialise with the latest serialiser. This also triggers the error notification + if (!serialisers.TryGetValue(latestSerialiserVersion, out var latestSerialiser)) return; - return false; + deserialise(latestSerialiser, filePathOverride); } - private bool deserialise(ISerialiser serialiser) + private void deserialise(ISerialiser serialiser, string filePathOverride) { - if (!serialiser.Deserialise()) return false; + if (!serialiser.Deserialise(filePathOverride)) return; Serialise(); - return true; } public bool Serialise() => serialisers[latestSerialiserVersion].Serialise(); diff --git a/VRCOSC.Game/Serialisation/Serialiser.cs b/VRCOSC.Game/Serialisation/Serialiser.cs index 359bdb47..0ef923ff 100644 --- a/VRCOSC.Game/Serialisation/Serialiser.cs +++ b/VRCOSC.Game/Serialisation/Serialiser.cs @@ -15,10 +15,11 @@ namespace VRCOSC.Game.Serialisation; public abstract class Serialiser : ISerialiser where TSerialisable : class { private readonly object serialisationLock = new(); - private readonly Storage storage; private readonly NotificationContainer notification; private readonly TReference reference; + private Storage storage; + protected virtual string Directory => string.Empty; protected virtual string FileName => throw new NotImplementedException($"{typeof(Serialiser)} requires a file name"); protected Serialiser(Storage storage, NotificationContainer notification, TReference reference) @@ -28,6 +29,11 @@ protected Serialiser(Storage storage, NotificationContainer notification, TRefer this.reference = reference; } + public void Initialise() + { + if (!string.IsNullOrEmpty(Directory)) storage = storage.GetStorageForDirectory(Directory); + } + public bool DoesFileExist() => storage.Exists(FileName); public bool TryGetVersion([NotNullWhen(true)] out int? version) @@ -40,7 +46,7 @@ public bool TryGetVersion([NotNullWhen(true)] out int? version) try { - var data = performDeserialisation(); + var data = performDeserialisation(storage.GetFullPath(FileName)); if (data is null) { @@ -58,15 +64,17 @@ public bool TryGetVersion([NotNullWhen(true)] out int? version) } } - public bool Deserialise() + public bool Deserialise(string filePathOverride = "") { + var filePath = string.IsNullOrEmpty(filePathOverride) ? storage.GetFullPath(FileName) : filePathOverride; + Logger.Log($"Performing load for file {FileName}"); try { lock (serialisationLock) { - var data = performDeserialisation(); + var data = performDeserialisation(filePath); if (data is null) return false; ExecuteAfterDeserialisation(reference, data); @@ -102,16 +110,16 @@ public bool Serialise() } } - private T? performDeserialisation() + private T? performDeserialisation(string filePath) { try { - return JsonConvert.DeserializeObject(Encoding.Unicode.GetString(File.ReadAllBytes(storage.GetFullPath(FileName)))); + return JsonConvert.DeserializeObject(Encoding.Unicode.GetString(File.ReadAllBytes(filePath))); } catch // migration from UTF-8 { Logger.Log("UTF-8 possibly detected. Attempting conversion from UTF-8 to Unicode"); - return JsonConvert.DeserializeObject(File.ReadAllText(storage.GetFullPath(FileName))); + return JsonConvert.DeserializeObject(File.ReadAllText(filePath)); } } diff --git a/VRCOSC.Game/Startup/Serialisation/V1/StartupSerialiser.cs b/VRCOSC.Game/Startup/Serialisation/V1/StartupSerialiser.cs index 6f12cac5..3571d1a4 100644 --- a/VRCOSC.Game/Startup/Serialisation/V1/StartupSerialiser.cs +++ b/VRCOSC.Game/Startup/Serialisation/V1/StartupSerialiser.cs @@ -24,7 +24,6 @@ public StartupSerialiser(Storage storage, NotificationContainer notification, St protected override void ExecuteAfterDeserialisation(StartupManager startupManager, SerialisableStartupManager data) { - startupManager.FilePaths.Clear(); - startupManager.FilePaths.AddRange(data.FilePaths.Select(path => new Bindable(path))); + startupManager.FilePaths.ReplaceItems(data.FilePaths.Select(path => new Bindable(path))); } } diff --git a/VRCOSC.Game/VRCOSC.Game.csproj b/VRCOSC.Game/VRCOSC.Game.csproj index ee70bf50..be93cd4b 100644 --- a/VRCOSC.Game/VRCOSC.Game.csproj +++ b/VRCOSC.Game/VRCOSC.Game.csproj @@ -4,7 +4,7 @@ enable 11 VolcanicArts.VRCOSC.SDK - 2023.516.0 + 2023.531.0 VRCOSC SDK VolcanicArts SDK for creating custom modules with VRCOSC @@ -12,15 +12,17 @@ https://github.com/VolcanicArts/VRCOSC true true + true - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/VRCOSC.Game/VRCOSCGame.cs b/VRCOSC.Game/VRCOSCGame.cs index b93e8f86..4185cb49 100644 --- a/VRCOSC.Game/VRCOSCGame.cs +++ b/VRCOSC.Game/VRCOSCGame.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using VRCOSC.Game.Config; +using VRCOSC.Game.Github; using VRCOSC.Game.Graphics; using VRCOSC.Game.Graphics.Notifications; using VRCOSC.Game.Graphics.Settings; @@ -58,12 +59,16 @@ public abstract partial class VRCOSCGame : VRCOSCGameBase [BackgroundDependencyLoader] private void load() { + AddFont(Resources, @"Fonts/ArialUnicode/ArialUnicode"); + // Forcibly load + new FontUsage("ArialUnicode"); + ThemeManager.VRCOSCTheme = ConfigManager.Get(VRCOSCSetting.Theme); DependencyContainer.CacheAs(notificationContainer = new NotificationContainer()); - DependencyContainer.CacheAs(typeof(IVRCOSCSecrets), GetSecrets()); DependencyContainer.CacheAs(routerManager = new RouterManager(storage, notificationContainer)); DependencyContainer.CacheAs(startupManager = new StartupManager(storage, notificationContainer)); + DependencyContainer.CacheAs(new GitHubProvider(host.Name)); LoadComponent(notificationContainer); @@ -179,6 +184,5 @@ private void performExit() Exit(); } - protected abstract IVRCOSCSecrets GetSecrets(); protected abstract VRCOSCUpdateManager CreateUpdateManager(); } diff --git a/VRCOSC.Game/VRCOSCGameBase.cs b/VRCOSC.Game/VRCOSCGameBase.cs index 0faa0bb9..18086e14 100644 --- a/VRCOSC.Game/VRCOSCGameBase.cs +++ b/VRCOSC.Game/VRCOSCGameBase.cs @@ -16,12 +16,6 @@ namespace VRCOSC.Game; public partial class VRCOSCGameBase : osu.Framework.Game { -#if DEBUG - private const string base_game_name = @"VRCOSC-Development"; -#else - private const string base_game_name = @"VRCOSC"; -#endif - [Resolved] private GameHost host { get; set; } = null!; @@ -50,7 +44,7 @@ private void load(Storage storage) DependencyContainer.CacheAs(ConfigManager = new VRCOSCConfigManager(storage)); versionBindable = ConfigManager.GetBindable(VRCOSCSetting.Version); - versionBindable.BindValueChanged(version => host.Window.Title = $"{base_game_name} {version.NewValue}", true); + versionBindable.BindValueChanged(version => host.Window.Title = $"{host.Name} {version.NewValue}", true); Window.WindowState = ConfigManager.Get(VRCOSCSetting.WindowState); } diff --git a/VRCOSC.Game/IVRCOSCSecrets.cs b/VRCOSC.Game/VRCOSCGraphicsContants.cs similarity index 52% rename from VRCOSC.Game/IVRCOSCSecrets.cs rename to VRCOSC.Game/VRCOSCGraphicsContants.cs index c08b68d2..9aefe7b9 100644 --- a/VRCOSC.Game/IVRCOSCSecrets.cs +++ b/VRCOSC.Game/VRCOSCGraphicsContants.cs @@ -3,7 +3,8 @@ namespace VRCOSC.Game; -public interface IVRCOSCSecrets +public class VRCOSCGraphicsContants { - public string GetSecret(VRCOSCSecretsKeys key); + public const float GOLDEN_RATIO = 1.61803f; + public const float ONE_OVER_GOLDEN_RATIO = 1f / GOLDEN_RATIO; } diff --git a/VRCOSC.Modules/Clock/ClockModule.cs b/VRCOSC.Modules/Clock/ClockModule.cs index cc8169d1..481dc00b 100644 --- a/VRCOSC.Modules/Clock/ClockModule.cs +++ b/VRCOSC.Modules/Clock/ClockModule.cs @@ -87,20 +87,17 @@ protected override void OnModuleUpdate() private static float getSmoothedMinutes(DateTime time) => time.Minute + getSmoothedSeconds(time) / 60f; private static float getSmoothedHours(DateTime time) => time.Hour + getSmoothedMinutes(time) / 60f; - private static DateTime timezoneToTime(ClockTimeZone timeZone) + private static DateTime timezoneToTime(ClockTimeZone timeZone) => timeZone switch { - return timeZone switch - { - ClockTimeZone.Local => DateTime.Now, - ClockTimeZone.UTC => DateTime.UtcNow, - ClockTimeZone.GMT => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time")), - ClockTimeZone.EST => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time")), - ClockTimeZone.CST => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time")), - ClockTimeZone.MNT => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Mountain Standard Time")), - ClockTimeZone.PST => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")), - _ => throw new ArgumentOutOfRangeException(nameof(timeZone), timeZone, null) - }; - } + ClockTimeZone.Local => DateTime.Now, + ClockTimeZone.UTC => DateTime.UtcNow, + ClockTimeZone.GMT => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time")), + ClockTimeZone.EST => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time")), + ClockTimeZone.CST => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time")), + ClockTimeZone.MNT => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Mountain Standard Time")), + ClockTimeZone.PST => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")), + _ => throw new ArgumentOutOfRangeException(nameof(timeZone), timeZone, null) + }; private enum ClockParameter { diff --git a/VRCOSC.Modules/Counter/CounterModule.cs b/VRCOSC.Modules/Counter/CounterModule.cs new file mode 100644 index 00000000..cc964bab --- /dev/null +++ b/VRCOSC.Modules/Counter/CounterModule.cs @@ -0,0 +1,102 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using VRCOSC.Game.Modules.Attributes; +using VRCOSC.Game.Modules.ChatBox; +using VRCOSC.Game.OSC.VRChat; + +namespace VRCOSC.Modules.Counter; + +public class CounterModule : ChatBoxModule +{ + public override string Title => "Counter"; + public override string Description => "Counts how many times parameters are triggered based on boolean events"; + public override string Author => "VolcanicArts"; + public override ModuleType Type => ModuleType.General; + + private readonly Dictionary counts = new(); + + private class CountInstance + { + public readonly string Key; + public int Count; + + public CountInstance(string key, int count) + { + Key = key; + Count = count; + } + } + + protected override void CreateAttributes() + { + CreateSetting(CounterSetting.ResetOnAvatarChange, "Reset On Avatar Change", "Should the counter reset on avatar change?", false); + CreateSetting(CounterSetting.ParameterList, "Parameter List", "What parameters should be monitored for them becoming true?\nCounts can be accessed in the ChatBox using: {counter.value_Key}", new List(), "Key", "Parameter Name"); + + CreateVariable(CounterVariable.Value, "Value", "value"); + + CreateState(CounterState.Default, "Default", GetVariableFormat(CounterVariable.Value)); + + CreateEvent(CounterEvent.Changed, "Changed", GetVariableFormat(CounterVariable.Value), 5); + } + + protected override void OnModuleStart() + { + auditParameters(); + } + + protected override void OnAvatarChange() + { + if (GetSetting(CounterSetting.ResetOnAvatarChange)) auditParameters(); + } + + private void auditParameters() + { + counts.Clear(); + + GetSettingList(CounterSetting.ParameterList).ForEach(pair => + { + if (string.IsNullOrEmpty(pair.Key.Value) || string.IsNullOrEmpty(pair.Value.Value)) return; + + counts.Add(pair.Value.Value, new CountInstance(pair.Key.Value, 0)); + SetVariableValue(CounterVariable.Value, "0", pair.Key.Value); + }); + } + + protected override void OnAnyParameterReceived(VRChatOscData data) + { + if (!counts.TryGetValue(data.ParameterName, out var value)) return; + + if (data.IsValueType() && data.ValueAs() > 0.9f) counterChanged(value); + if (data.IsValueType() && data.ValueAs() != 0) counterChanged(value); + if (data.IsValueType() && data.ValueAs()) counterChanged(value); + } + + private void counterChanged(CountInstance instance) + { + instance.Count++; + SetVariableValue(CounterVariable.Value, instance.Count.ToString("N0"), instance.Key); + TriggerEvent(CounterEvent.Changed); + } + + private enum CounterSetting + { + ResetOnAvatarChange, + ParameterList + } + + private enum CounterVariable + { + Value + } + + private enum CounterState + { + Default + } + + private enum CounterEvent + { + Changed + } +} diff --git a/VRCOSC.Modules/Heartrate/HeartRateModule.cs b/VRCOSC.Modules/Heartrate/HeartRateModule.cs index 9804f820..8af6b7bf 100644 --- a/VRCOSC.Modules/Heartrate/HeartRateModule.cs +++ b/VRCOSC.Modules/Heartrate/HeartRateModule.cs @@ -113,8 +113,7 @@ private void handleHeartRateUpdate(int heartrate) try { - var absoluteDifference = Math.Abs(currentHeartrate - targetHeartrate); - targetInterval = absoluteDifference == 1 ? TimeSpan.Zero : TimeSpan.FromTicks(TimeSpan.FromMilliseconds(GetSetting(HeartrateSetting.SmoothingLength)).Ticks / absoluteDifference); + targetInterval = TimeSpan.FromTicks(TimeSpan.FromMilliseconds(GetSetting(HeartrateSetting.SmoothingLength)).Ticks / Math.Abs(currentHeartrate - targetHeartrate)); } catch (DivideByZeroException) { diff --git a/VRCOSC.Modules/Heartrate/HypeRate/HypeRateModule.cs b/VRCOSC.Modules/Heartrate/HypeRate/HypeRateModule.cs index 5f3a9d22..121c2991 100644 --- a/VRCOSC.Modules/Heartrate/HypeRate/HypeRateModule.cs +++ b/VRCOSC.Modules/Heartrate/HypeRate/HypeRateModule.cs @@ -1,7 +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 VRCOSC.Game; using VRCOSC.Game.Modules; namespace VRCOSC.Modules.Heartrate.HypeRate; @@ -12,7 +11,7 @@ public sealed class HypeRateModule : HeartRateModule public override string Description => @"Connects to HypeRate.io and sends your heartrate to VRChat"; protected override TimeSpan DeltaUpdate => TimeSpan.FromSeconds(10); - protected override HeartRateProvider CreateHeartRateProvider() => new HypeRateProvider(GetSetting(HypeRateSetting.Id), Secrets.GetSecret(VRCOSCSecretsKeys.Hyperate), new TerminalLogger(Title)); + protected override HeartRateProvider CreateHeartRateProvider() => new HypeRateProvider(GetSetting(HypeRateSetting.Id), OfficialModuleSecrets.GetSecret(OfficialModuleSecretsKeys.Hyperate), new TerminalLogger(Title)); protected override void CreateAttributes() { diff --git a/VRCOSC.Modules/Heartrate/HypeRate/HypeRateProvider.cs b/VRCOSC.Modules/Heartrate/HypeRate/HypeRateProvider.cs index f04d8554..f301c89d 100644 --- a/VRCOSC.Modules/Heartrate/HypeRate/HypeRateProvider.cs +++ b/VRCOSC.Modules/Heartrate/HypeRate/HypeRateProvider.cs @@ -34,36 +34,36 @@ protected override void HandleWsDisconnected() protected override void HandleWsMessage(string message) { - var eventModel = JsonConvert.DeserializeObject(message); - - if (eventModel is null) + try { - Log($"Received an unrecognised message:\n{message}"); - return; + var eventModel = JsonConvert.DeserializeObject(message); + + if (eventModel is null) + { + Log($"Received an unrecognised message:\n{message}"); + return; + } + + switch (eventModel.Event) + { + case "hr_update": + handleHrUpdate(JsonConvert.DeserializeObject(message)!); + break; + } } - - switch (eventModel.Event) + catch (JsonReaderException) { - case "hr_update": - handleHrUpdate(JsonConvert.DeserializeObject(message)!); - break; - - case "phx_reply": - handlePhxReply(JsonConvert.DeserializeObject(message)!); - break; + Log("Error receiving heartrate result"); } } public void SendWsHeartBeat() { - Log("Sending HypeRate websocket heartbeat"); SendData(new HeartBeatModel()); } private void sendJoinChannel() { - Log($"Requesting to hook into heartrate for Id {hypeRateId}"); - var joinChannelModel = new JoinChannelModel { Id = hypeRateId @@ -71,11 +71,6 @@ private void sendJoinChannel() SendData(joinChannelModel); } - private void handlePhxReply(PhxReplyModel reply) - { - Log($"Status of reply: {reply.Payload.Status}"); - } - private void handleHrUpdate(HeartRateUpdateModel update) { OnHeartRateUpdate?.Invoke(update.Payload.HeartRate); diff --git a/VRCOSC.Modules/Media/MediaModule.cs b/VRCOSC.Modules/Media/MediaModule.cs index 116597bb..45a0ef2a 100644 --- a/VRCOSC.Modules/Media/MediaModule.cs +++ b/VRCOSC.Modules/Media/MediaModule.cs @@ -1,7 +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 Windows.Media; using osu.Framework.Bindables; using VRCOSC.Game; using VRCOSC.Game.Modules; @@ -25,13 +24,13 @@ public class MediaModule : ChatBoxModule protected override TimeSpan DeltaUpdate => TimeSpan.FromSeconds(1); public override ModuleType Type => ModuleType.Integrations; - private readonly WindowsMediaProvider mediaProvider = new(); + private readonly MediaProvider mediaProvider = new WindowsMediaProvider(); private readonly Bindable currentlySeeking = new(); private TimeSpan targetPosition; public MediaModule() { - mediaProvider.OnPlaybackStateUpdate += onPlaybackStateUpdate; + mediaProvider.OnPlaybackStateChange += onPlaybackStateChange; mediaProvider.OnTrackChange += onTrackChange; } @@ -71,7 +70,7 @@ protected override void OnModuleStart() private void hookIntoMedia() => Task.Run(async () => { - var result = await mediaProvider.Hook(); + var result = await mediaProvider.InitialiseAsync(); if (!result) { @@ -84,7 +83,7 @@ private void hookIntoMedia() => Task.Run(async () => protected override void OnModuleStop() { - mediaProvider.UnHook(); + mediaProvider.TerminateAsync(); } protected override void OnAvatarChange() @@ -95,11 +94,8 @@ protected override void OnAvatarChange() protected override void OnModuleUpdate() { - if (mediaProvider.Controller is not null) - { - updateVariables(); - sendUpdatableParameters(); - } + updateVariables(); + sendUpdatableParameters(); } private void updateVariables() @@ -110,24 +106,14 @@ private void updateVariables() SetVariableValue(MediaVariable.AlbumTitle, mediaProvider.State.AlbumTitle); SetVariableValue(MediaVariable.AlbumArtist, mediaProvider.State.AlbumArtist); SetVariableValue(MediaVariable.AlbumTrackCount, mediaProvider.State.AlbumTrackCount.ToString()); - SetVariableValue(MediaVariable.Volume, (mediaProvider.State.Volume * 100).ToString("##0")); + SetVariableValue(MediaVariable.Volume, (mediaProvider.TryGetVolume() * 100).ToString("##0")); SetVariableValue(MediaVariable.ProgressVisual, getProgressVisual()); - - if (mediaProvider.State.Position is null) - { - SetVariableValue(MediaVariable.Time, string.Empty); - SetVariableValue(MediaVariable.TimeRemaining, string.Empty); - SetVariableValue(MediaVariable.Duration, string.Empty); - } - else - { - SetVariableValue(MediaVariable.Time, mediaProvider.State.Position.Position.Format()); - SetVariableValue(MediaVariable.TimeRemaining, (mediaProvider.State.Position.EndTime - mediaProvider.State.Position.Position).Format()); - SetVariableValue(MediaVariable.Duration, mediaProvider.State.Position.EndTime.Format()); - } + SetVariableValue(MediaVariable.Time, mediaProvider.State.Timeline.Position.Format()); + SetVariableValue(MediaVariable.TimeRemaining, (mediaProvider.State.Timeline.End - mediaProvider.State.Timeline.Position).Format()); + SetVariableValue(MediaVariable.Duration, mediaProvider.State.Timeline.End.Format()); } - private void onPlaybackStateUpdate() + private void onPlaybackStateChange() { updateVariables(); sendMediaParameters(); @@ -149,20 +135,17 @@ private void sendMediaParameters() private void sendUpdatableParameters() { - SendParameter(MediaParameter.Volume, mediaProvider.State.Volume); + SendParameter(MediaParameter.Volume, mediaProvider.TryGetVolume()); - var position = mediaProvider.State.Position; - - if (position is not null && !currentlySeeking.Value) + if (!currentlySeeking.Value) { - var percentagePosition = position.Position.Ticks / (float)(position.EndTime.Ticks - position.StartTime.Ticks); - SendParameter(MediaParameter.Position, percentagePosition); + SendParameter(MediaParameter.Position, mediaProvider.State.Timeline.PositionPercentage); } } private string getProgressVisual() { - var progressPercentage = progress_resolution * mediaProvider.State.PositionPercentage; + var progressPercentage = progress_resolution * mediaProvider.State.Timeline.PositionPercentage; var dotPosition = (int)(MathF.Floor(progressPercentage * 10f) / 10f); var visual = progress_start; @@ -181,18 +164,16 @@ protected override void OnFloatParameterReceived(Enum key, float value) { switch (key) { - case MediaParameter.Volume when mediaProvider.Controller is not null: - mediaProvider.State.Volume = value; + case MediaParameter.Volume: + mediaProvider.TryChangeVolume(value); break; - case MediaParameter.Position when mediaProvider.Controller is not null: + case MediaParameter.Position: if (!currentlySeeking.Value) return; - var position = mediaProvider.State.Position; - if (position is null) return; - - targetPosition = (position.EndTime - position.StartTime) * value; + var position = mediaProvider.State.Timeline; + targetPosition = (position.End - position.Start) * value; break; } } @@ -202,28 +183,28 @@ protected override void OnBoolParameterReceived(Enum key, bool value) switch (key) { case MediaParameter.Play when value: - mediaProvider.Controller?.TryPlayAsync(); + mediaProvider.Play(); break; case MediaParameter.Play when !value: - mediaProvider.Controller?.TryPauseAsync(); + mediaProvider.Pause(); break; case MediaParameter.Shuffle: - mediaProvider.Controller?.TryChangeShuffleActiveAsync(value); + mediaProvider.ChangeShuffle(value); break; case MediaParameter.Next when value: - mediaProvider.Controller?.TrySkipNextAsync(); + mediaProvider.SkipNext(); break; case MediaParameter.Previous when value: - mediaProvider.Controller?.TrySkipPreviousAsync(); + mediaProvider.SkipPrevious(); break; case MediaParameter.Seeking: currentlySeeking.Value = value; - if (!currentlySeeking.Value) mediaProvider.Controller?.TryChangePlaybackPositionAsync(targetPosition.Ticks); + if (!currentlySeeking.Value) mediaProvider.ChangePlaybackPosition(targetPosition); break; } } @@ -233,7 +214,7 @@ protected override void OnIntParameterReceived(Enum key, int value) switch (key) { case MediaParameter.Repeat: - mediaProvider.Controller?.TryChangeAutoRepeatModeAsync((MediaPlaybackAutoRepeatMode)value); + mediaProvider.ChangeRepeatMode((MediaRepeatMode)value); break; } } diff --git a/VRCOSC.Modules/OfficialModuleSecrets.cs b/VRCOSC.Modules/OfficialModuleSecrets.cs new file mode 100644 index 00000000..fe061628 --- /dev/null +++ b/VRCOSC.Modules/OfficialModuleSecrets.cs @@ -0,0 +1,19 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +namespace VRCOSC.Modules; + +internal static class OfficialModuleSecrets +{ + private static Dictionary secrets => new() + { + }; + + internal static string GetSecret(OfficialModuleSecretsKeys key) => secrets.TryGetValue(key, out var secret) ? secret : string.Empty; +} + +internal enum OfficialModuleSecretsKeys +{ + Hyperate, + Weather +} diff --git a/VRCOSC.Modules/OpenVR/HapticControlModule.cs b/VRCOSC.Modules/OpenVR/HapticControlModule.cs index 9dcab15a..4f504707 100644 --- a/VRCOSC.Modules/OpenVR/HapticControlModule.cs +++ b/VRCOSC.Modules/OpenVR/HapticControlModule.cs @@ -12,6 +12,13 @@ public class HapticControlModule : Module public override string Author => "VolcanicArts"; public override ModuleType Type => ModuleType.OpenVR; + public override IEnumerable Info => new List + { + "The duration, frequency, and amplitude parameters can be set from your animator", + "If you're designing a prefab, ensure these parameters are set each time before you attempt a trigger in the case that the user has restarted this module", + "Trigger parameters must be set back to false before attempting another trigger" + }; + private float duration; private float frequency; private float amplitude; diff --git a/VRCOSC.Modules/OpenVR/OpenVRStatisticsModule.cs b/VRCOSC.Modules/OpenVR/OpenVRStatisticsModule.cs index 333d34ec..7d72ed1d 100644 --- a/VRCOSC.Modules/OpenVR/OpenVRStatisticsModule.cs +++ b/VRCOSC.Modules/OpenVR/OpenVRStatisticsModule.cs @@ -15,6 +15,11 @@ public class OpenVRStatisticsModule : ChatBoxModule public override ModuleType Type => ModuleType.OpenVR; protected override TimeSpan DeltaUpdate => TimeSpan.FromSeconds(5); + public override IEnumerable Info => new List + { + "The tracker order in Unity is the order you must turn your trackers on IRL" + }; + protected override void CreateAttributes() { CreateParameter(OpenVrParameter.FPS, ParameterMode.Write, "VRCOSC/OpenVR/FPS", "FPS", "The current FPS normalised to 240 FPS"); @@ -43,7 +48,7 @@ protected override void CreateAttributes() CreateVariable(OpenVrVariable.RightControllerBattery, @"Right Controller Battery (%)", @"rightcontrollerbattery"); CreateVariable(OpenVrVariable.AverageTrackerBattery, @"Average Tracker Battery (%)", @"averagetrackerbattery"); - CreateState(OpenVrState.Default, "Default", $@"FPS: {GetVariableFormat(OpenVrVariable.FPS)} | HMD: {GetVariableFormat(OpenVrVariable.HMDBattery)} | LC: {GetVariableFormat(OpenVrVariable.LeftControllerBattery)} | RC: {GetVariableFormat(OpenVrVariable.RightControllerBattery)}"); + CreateState(OpenVrState.Default, "Default", $@"HMD: {GetVariableFormat(OpenVrVariable.HMDBattery)}/vLC: {GetVariableFormat(OpenVrVariable.LeftControllerBattery)}/vRC: {GetVariableFormat(OpenVrVariable.RightControllerBattery)}/vTrackers: {GetVariableFormat(OpenVrVariable.AverageTrackerBattery)}"); } protected override void OnModuleStart() @@ -56,9 +61,10 @@ protected override void OnModuleUpdate() if (OVRClient.HasInitialised) { SendParameter(OpenVrParameter.FPS, OVRClient.System.FPS / 240.0f); - handleHmd(); - handleControllers(); - handleTrackers(); + updateHmd(); + updateLeftController(); + updateRightController(); + updateTrackers(); var activeTrackers = OVRClient.Trackers.Where(tracker => tracker.IsConnected).ToList(); var trackerBatteryAverage = activeTrackers.Sum(tracker => tracker.BatteryPercentage) / activeTrackers.Count; @@ -76,10 +82,29 @@ protected override void OnModuleUpdate() SetVariableValue(OpenVrVariable.LeftControllerBattery, "0"); SetVariableValue(OpenVrVariable.RightControllerBattery, "0"); SetVariableValue(OpenVrVariable.AverageTrackerBattery, "0"); + + SendParameter(OpenVrParameter.HMD_Connected, false); + SendParameter(OpenVrParameter.HMD_Battery, 0); + SendParameter(OpenVrParameter.HMD_Charging, false); + + SendParameter(OpenVrParameter.LeftController_Connected, false); + SendParameter(OpenVrParameter.LeftController_Battery, 0); + SendParameter(OpenVrParameter.LeftController_Charging, false); + + SendParameter(OpenVrParameter.RightController_Connected, false); + SendParameter(OpenVrParameter.RightController_Battery, 0); + SendParameter(OpenVrParameter.RightController_Charging, false); + + for (int i = 0; i < OVRSystem.MAX_TRACKER_COUNT; i++) + { + SendParameter(OpenVrParameter.Tracker1_Connected + i, false); + SendParameter(OpenVrParameter.Tracker1_Battery + i, 0); + SendParameter(OpenVrParameter.Tracker1_Charging + i, false); + } } } - private void handleHmd() + private void updateHmd() { SendParameter(OpenVrParameter.HMD_Connected, OVRClient.HMD.IsConnected); @@ -90,7 +115,7 @@ private void handleHmd() } } - private void handleControllers() + private void updateLeftController() { SendParameter(OpenVrParameter.LeftController_Connected, OVRClient.LeftController.IsConnected); @@ -99,7 +124,15 @@ private void handleControllers() SendParameter(OpenVrParameter.LeftController_Battery, OVRClient.LeftController.BatteryPercentage); SendParameter(OpenVrParameter.LeftController_Charging, OVRClient.LeftController.IsCharging); } + else + { + SendParameter(OpenVrParameter.LeftController_Battery, 0); + SendParameter(OpenVrParameter.LeftController_Charging, false); + } + } + private void updateRightController() + { SendParameter(OpenVrParameter.RightController_Connected, OVRClient.RightController.IsConnected); if (OVRClient.RightController.IsConnected && OVRClient.RightController.ProvidesBatteryStatus) @@ -107,9 +140,14 @@ private void handleControllers() SendParameter(OpenVrParameter.RightController_Battery, OVRClient.RightController.BatteryPercentage); SendParameter(OpenVrParameter.RightController_Charging, OVRClient.RightController.IsCharging); } + else + { + SendParameter(OpenVrParameter.RightController_Battery, 0); + SendParameter(OpenVrParameter.RightController_Charging, false); + } } - private void handleTrackers() + private void updateTrackers() { var trackers = OVRClient.Trackers.ToList(); @@ -124,6 +162,11 @@ private void handleTrackers() SendParameter(OpenVrParameter.Tracker1_Battery + i, tracker.BatteryPercentage); SendParameter(OpenVrParameter.Tracker1_Charging + i, tracker.IsCharging); } + else + { + SendParameter(OpenVrParameter.Tracker1_Battery + i, 0); + SendParameter(OpenVrParameter.Tracker1_Charging + i, false); + } } } diff --git a/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs b/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs index c212b136..55547bbf 100644 --- a/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs +++ b/VRCOSC.Modules/SpeechToText/SpeechToTextModule.cs @@ -135,21 +135,24 @@ private void analyseAudio(byte[] buffer, int bytesRecorded) } else { - var result = JsonConvert.DeserializeObject(recogniser.Result())?.Text; + var result = JsonConvert.DeserializeObject(recogniser.Result()); - if (!string.IsNullOrEmpty(result) && result != "huh") + if (result is not null && result.Confidence > 0.5f) { - result = result[..1].ToUpper(CultureInfo.CurrentCulture) + result[1..]; - Log($"Recognised: \"{result}\""); + if (!string.IsNullOrEmpty(result.Text) && result.Text != "huh") + { + var finalText = result.Text[..1].ToUpper(CultureInfo.CurrentCulture) + result.Text[1..]; + Log($"Recognised: \"{result.Text}\""); - SetVariableValue(SpeechToTextVariable.Text, result); - ChangeStateTo(SpeechToTextState.TextGenerated); - TriggerEvent(SpeechToTextEvent.TextGenerated); - } - else - { - SetVariableValue(SpeechToTextVariable.Text, string.Empty); - ChangeStateTo(SpeechToTextState.Idle); + SetVariableValue(SpeechToTextVariable.Text, finalText); + ChangeStateTo(SpeechToTextState.TextGenerated); + TriggerEvent(SpeechToTextEvent.TextGenerated); + } + else + { + SetVariableValue(SpeechToTextVariable.Text, string.Empty); + ChangeStateTo(SpeechToTextState.Idle); + } } recogniser.Reset(); @@ -195,6 +198,9 @@ private class Recognition { [JsonProperty("text")] public string Text = null!; + + [JsonProperty("confidence")] + public float Confidence; } private class PartialRecognition diff --git a/VRCOSC.Modules/VRCOSC.Modules.csproj b/VRCOSC.Modules/VRCOSC.Modules.csproj index c964904f..64789acd 100644 --- a/VRCOSC.Modules/VRCOSC.Modules.csproj +++ b/VRCOSC.Modules/VRCOSC.Modules.csproj @@ -5,10 +5,19 @@ enable enable true + 11 + Official Modules + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/VRCOSC.Modules/VRCOSCSecrets.cs b/VRCOSC.Modules/VRCOSCSecrets.cs deleted file mode 100644 index c83c8079..00000000 --- a/VRCOSC.Modules/VRCOSCSecrets.cs +++ /dev/null @@ -1,17 +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 VRCOSC.Game; - -namespace VRCOSC.Modules; - -public class VRCOSCModuleSecrets : IVRCOSCSecrets -{ - private readonly Dictionary secrets = new(); - - public VRCOSCModuleSecrets() - { - } - - public string GetSecret(VRCOSCSecretsKeys key) => secrets.TryGetValue(key, out var value) ? value : string.Empty; -} diff --git a/VRCOSC.Modules/Weather/WeatherModule.cs b/VRCOSC.Modules/Weather/WeatherModule.cs index 13607dfe..5be20442 100644 --- a/VRCOSC.Modules/Weather/WeatherModule.cs +++ b/VRCOSC.Modules/Weather/WeatherModule.cs @@ -1,7 +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 VRCOSC.Game; using VRCOSC.Game.Modules; using VRCOSC.Game.Modules.ChatBox; @@ -35,7 +34,7 @@ protected override void OnModuleStart() { if (string.IsNullOrEmpty(GetSetting(WeatherSetting.Postcode))) Log("Please provide a post/zip code or city name"); - weatherProvider ??= new WeatherProvider(Secrets.GetSecret(VRCOSCSecretsKeys.Weather)); + weatherProvider ??= new WeatherProvider(OfficialModuleSecrets.GetSecret(OfficialModuleSecretsKeys.Weather)); ChangeStateTo(WeatherState.Default); } diff --git a/VRCOSC.Modules/Weather/WeatherProvider.cs b/VRCOSC.Modules/Weather/WeatherProvider.cs index 4a1746e6..f275f48a 100644 --- a/VRCOSC.Modules/Weather/WeatherProvider.cs +++ b/VRCOSC.Modules/Weather/WeatherProvider.cs @@ -20,6 +20,8 @@ public class WeatherProvider private string? lastLocation; private Weather? weather; + private Dictionary? conditions; + public WeatherProvider(string apiKey) { this.apiKey = apiKey; @@ -48,10 +50,9 @@ public WeatherProvider(string apiKey) if (!DateTime.TryParse(astronomyResponse.Sunrise, out var sunriseParsed)) return null; if (!DateTime.TryParse(astronomyResponse.Sunset, out var sunsetParsed)) return null; - var conditionResponseData = await httpClient.GetAsync(condition_url); - var conditionResponseString = await conditionResponseData.Content.ReadAsStringAsync(); - var conditionResponse = JsonConvert.DeserializeObject>(conditionResponseString)?.Single(condition => condition.Code == currentResponse.Condition.Code); + if (conditions is null) await retrieveConditions(); + var conditionResponse = conditions?[currentResponse.Condition.Code]; var dateTimeNow = DateTime.Now; if (dateTimeNow >= sunriseParsed && dateTimeNow < sunsetParsed) @@ -62,4 +63,16 @@ public WeatherProvider(string apiKey) weather = currentResponse; return weather; } + + private async Task retrieveConditions() + { + var conditionResponseData = await httpClient.GetAsync(condition_url); + var conditionResponseString = await conditionResponseData.Content.ReadAsStringAsync(); + var conditionData = JsonConvert.DeserializeObject>(conditionResponseString); + + if (conditionData is null) return; + + conditions = new Dictionary(); + conditionData.ForEach(condition => conditions.Add(condition.Code, condition)); + } } diff --git a/VRCOSC.Resources/VRCOSC.Resources.csproj b/VRCOSC.Resources/VRCOSC.Resources.csproj index ab35676d..6dd23687 100644 --- a/VRCOSC.Resources/VRCOSC.Resources.csproj +++ b/VRCOSC.Resources/VRCOSC.Resources.csproj @@ -2,7 +2,7 @@ netstandard2.1 VolcanicArts.VRCOSC.Resources - 2023.509.0 + 2023.531.0 VRCOSC Resources VolcanicArts VRCOSC's resources diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode.fnt b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode.fnt new file mode 100644 index 00000000..14ac2b8b Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode.fnt differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_00.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_00.png new file mode 100644 index 00000000..538586e3 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_00.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_01.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_01.png new file mode 100644 index 00000000..4494b2bc Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_01.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_02.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_02.png new file mode 100644 index 00000000..8251ee68 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_02.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_03.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_03.png new file mode 100644 index 00000000..ecf731b6 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_03.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_04.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_04.png new file mode 100644 index 00000000..565863fc Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_04.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_05.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_05.png new file mode 100644 index 00000000..a9899422 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_05.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_06.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_06.png new file mode 100644 index 00000000..a3d26017 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_06.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_07.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_07.png new file mode 100644 index 00000000..6a339e13 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_07.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_08.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_08.png new file mode 100644 index 00000000..2a8e243d Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_08.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_09.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_09.png new file mode 100644 index 00000000..fae43dd4 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_09.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_10.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_10.png new file mode 100644 index 00000000..d511e1e7 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_10.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_11.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_11.png new file mode 100644 index 00000000..9b9a7b2e Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_11.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_12.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_12.png new file mode 100644 index 00000000..b5aa937b Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_12.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_13.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_13.png new file mode 100644 index 00000000..b45342cb Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_13.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_14.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_14.png new file mode 100644 index 00000000..22e5d463 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_14.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_15.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_15.png new file mode 100644 index 00000000..d58f2b04 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_15.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_16.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_16.png new file mode 100644 index 00000000..9144d409 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_16.png differ diff --git a/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_17.png b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_17.png new file mode 100644 index 00000000..951e8ba9 Binary files /dev/null and b/VRCOSC.Resources/fonts/ArialUnicode/ArialUnicode_17.png differ diff --git a/VRCOSC.Templates/VRCOSC.Templates.csproj b/VRCOSC.Templates/VRCOSC.Templates.csproj index 5213f562..ba737278 100644 --- a/VRCOSC.Templates/VRCOSC.Templates.csproj +++ b/VRCOSC.Templates/VRCOSC.Templates.csproj @@ -11,7 +11,7 @@ true NU5128 - 2023.516.0 + 2023.531.0 VolcanicArts https://github.com/VolcanicArts/VRCOSC https://github.com/VolcanicArts/VRCOSC diff --git a/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj b/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj index 1234ffee..74537b10 100644 --- a/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj +++ b/VRCOSC.Templates/templates/template-default/TemplateModule/TemplateModule.csproj @@ -7,7 +7,7 @@ - +