From 2e04a83554b982d3468e3692dbd70bef36e6c46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Feb 2022 17:52:16 +0100 Subject: [PATCH 01/68] Implement column display for new mod design --- .../UserInterface/TestSceneModColumn.cs | 48 +++++ osu.Game/Overlays/Mods/ModColumn.cs | 196 ++++++++++++++++++ osu.Game/Overlays/Mods/ModPanel.cs | 4 +- 3 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs create mode 100644 osu.Game/Overlays/Mods/ModColumn.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs new file mode 100644 index 0000000000..59641ae000 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneModColumn : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [TestCase(ModType.DifficultyReduction)] + [TestCase(ModType.DifficultyIncrease)] + [TestCase(ModType.Conversion)] + [TestCase(ModType.Automation)] + [TestCase(ModType.Fun)] + public void TestBasic(ModType modType) + { + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = new ModColumn(modType) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + + AddStep("change ruleset to osu!", () => Ruleset.Value = new OsuRuleset().RulesetInfo); + AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); + AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs new file mode 100644 index 0000000000..03ef3b889a --- /dev/null +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -0,0 +1,196 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; +using osuTK; + +#nullable enable + +namespace osu.Game.Overlays.Mods +{ + public class ModColumn : CompositeDrawable + { + private readonly ModType modType; + + private readonly Bindable>> availableMods = new Bindable>>(); + + private readonly TextFlowContainer headerText; + private readonly Box headerBackground; + private readonly Container contentContainer; + private readonly Box contentBackground; + private readonly FillFlowContainer panelFlow; + + private Colour4 accentColour; + + private const float header_height = 60; + + public ModColumn(ModType modType) + { + this.modType = modType; + + Width = 450; + RelativeSizeAxes = Axes.Y; + Shear = new Vector2(ModPanel.SHEAR_X, 0); + CornerRadius = ModPanel.CORNER_RADIUS; + Masking = true; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = header_height + ModPanel.CORNER_RADIUS, + Children = new Drawable[] + { + headerBackground = new Box + { + RelativeSizeAxes = Axes.X, + Height = header_height + ModPanel.CORNER_RADIUS + }, + headerText = new OsuTextFlowContainer(t => + { + t.Font = OsuFont.TorusAlternate.With(size: 24); + t.Shadow = false; + t.Colour = Colour4.Black; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Shear = new Vector2(-ModPanel.SHEAR_X, 0), + Padding = new MarginPadding + { + Horizontal = 15, + Bottom = ModPanel.CORNER_RADIUS + } + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = header_height }, + Child = contentContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = ModPanel.CORNER_RADIUS, + BorderThickness = 4, + Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10) + }, + Content = new[] + { + new[] + { + Empty() + }, + new Drawable[] + { + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Child = panelFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 10), + Padding = new MarginPadding + { + Horizontal = 10 + }, + } + } + }, + new[] { Empty() } + } + } + } + } + } + }; + + createHeaderText(); + } + + private void createHeaderText() + { + IEnumerable headerTextWords = modType.Humanize(LetterCasing.Title).Split(' '); + + if (headerTextWords.Count() > 1) + { + headerText.AddText($"{headerTextWords.First()} ", t => t.Font = t.Font.With(weight: FontWeight.SemiBold)); + headerTextWords = headerTextWords.Skip(1); + } + + headerText.AddText(string.Join(' ', headerTextWords)); + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours) + { + availableMods.BindTo(game.AvailableMods); + + headerBackground.Colour = accentColour = colours.ForModType(modType); + + contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3); + contentBackground.Colour = colourProvider.Background4; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods), true); + } + + private CancellationTokenSource? cancellationTokenSource; + + private void updateMods() + { + var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(modType) ?? Array.Empty()).ToList(); + + if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod))) + return; + + cancellationTokenSource?.Cancel(); + + var panels = newMods.Select(mod => new ModPanel(mod) + { + Shear = new Vector2(-ModPanel.SHEAR_X, 0) + }); + + LoadComponentsAsync(panels, loaded => + { + panelFlow.ChildrenEnumerable = loaded; + }, (cancellationTokenSource = new CancellationTokenSource()).Token); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index af8ad3eb18..446ebe12c5 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -40,10 +40,10 @@ namespace osu.Game.Overlays.Mods protected const double TRANSITION_DURATION = 150; - protected const float SHEAR_X = 0.2f; + public const float SHEAR_X = 0.2f; + public const float CORNER_RADIUS = 7; protected const float HEIGHT = 42; - protected const float CORNER_RADIUS = 7; protected const float IDLE_SWITCH_WIDTH = 54; protected const float EXPANDED_SWITCH_WIDTH = 70; From f40bd394871ddff25ef2a50827500663c747df8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Feb 2022 18:33:13 +0100 Subject: [PATCH 02/68] Add toggle all checkbox to column display --- osu.Game/Overlays/Mods/ModColumn.cs | 85 ++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 03ef3b889a..a42b2bcb96 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -8,15 +8,19 @@ using System.Threading; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; 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 osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; +using osuTK.Graphics; #nullable enable @@ -33,12 +37,13 @@ namespace osu.Game.Overlays.Mods private readonly Container contentContainer; private readonly Box contentBackground; private readonly FillFlowContainer panelFlow; + private readonly ToggleAllCheckbox? toggleAllCheckbox; private Colour4 accentColour; private const float header_height = 60; - public ModColumn(ModType modType) + public ModColumn(ModType modType, bool allowBulkSelection) { this.modType = modType; @@ -48,6 +53,7 @@ namespace osu.Game.Overlays.Mods CornerRadius = ModPanel.CORNER_RADIUS; Masking = true; + Container controlContainer; InternalChildren = new Drawable[] { new Container @@ -108,9 +114,13 @@ namespace osu.Game.Overlays.Mods }, Content = new[] { - new[] + new Drawable[] { - Empty() + controlContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 20 } + } }, new Drawable[] { @@ -139,6 +149,18 @@ namespace osu.Game.Overlays.Mods }; createHeaderText(); + + if (allowBulkSelection) + { + controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + LabelText = "Enable All", + Shear = new Vector2(-ModPanel.SHEAR_X, 0) + }); + } } private void createHeaderText() @@ -161,6 +183,12 @@ namespace osu.Game.Overlays.Mods headerBackground.Colour = accentColour = colours.ForModType(modType); + if (toggleAllCheckbox != null) + { + toggleAllCheckbox.AccentColour = accentColour; + toggleAllCheckbox.AccentHoverColour = accentColour.Lighten(0.3f); + } + contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3); contentBackground.Colour = colourProvider.Background4; } @@ -192,5 +220,56 @@ namespace osu.Game.Overlays.Mods panelFlow.ChildrenEnumerable = loaded; }, (cancellationTokenSource = new CancellationTokenSource()).Token); } + + private class ToggleAllCheckbox : OsuCheckbox + { + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + accentColour = value; + updateState(); + } + } + + private Color4 accentHoverColour; + + public Color4 AccentHoverColour + { + get => accentHoverColour; + set + { + accentHoverColour = value; + updateState(); + } + } + + public ToggleAllCheckbox() + : base(false) + { + } + + protected override void ApplyLabelParameters(SpriteText text) + { + base.ApplyLabelParameters(text); + text.Font = text.Font.With(weight: FontWeight.SemiBold); + } + + [BackgroundDependencyLoader] + private void load() + { + updateState(); + } + + private void updateState() + { + Nub.AccentColour = AccentColour; + Nub.GlowingAccentColour = AccentHoverColour; + Nub.GlowColour = AccentHoverColour.Opacity(0.2f); + } + } } } From 53e8072632ae3302ec6ee56c0f45e2fddecf7946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Feb 2022 18:45:04 +0100 Subject: [PATCH 03/68] Port multiselection from previous design --- .../UserInterface/TestSceneModColumn.cs | 17 +++- osu.Game/Overlays/Mods/ModColumn.cs | 81 ++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 59641ae000..e58649d989 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(30), - Child = new ModColumn(modType) + Child = new ModColumn(modType, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -44,5 +44,20 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); } + + [Test] + public void TestMultiSelection() + { + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = new ModColumn(ModType.DifficultyIncrease, true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + } } } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index a42b2bcb96..8ca968368e 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -152,7 +152,8 @@ namespace osu.Game.Overlays.Mods if (allowBulkSelection) { - controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox + controlContainer.Height = 50; + controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -218,9 +219,72 @@ namespace osu.Game.Overlays.Mods LoadComponentsAsync(panels, loaded => { panelFlow.ChildrenEnumerable = loaded; + foreach (var panel in panelFlow) + panel.Active.BindValueChanged(_ => updateToggleState()); + updateToggleState(); }, (cancellationTokenSource = new CancellationTokenSource()).Token); } + #region Bulk select / deselect + + private const double initial_multiple_selection_delay = 120; + + private double selectionDelay = initial_multiple_selection_delay; + private double lastSelection; + + private readonly Queue pendingSelectionOperations = new Queue(); + + protected override void Update() + { + base.Update(); + + if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay) + { + if (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) + { + dequeuedAction(); + + // each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements). + selectionDelay = Math.Max(30, selectionDelay * 0.8f); + lastSelection = Time.Current; + } + else + { + // reset the selection delay after all animations have been completed. + // this will cause the next action to be immediately performed. + selectionDelay = initial_multiple_selection_delay; + } + } + } + + private void updateToggleState() + { + if (toggleAllCheckbox != null && pendingSelectionOperations.Count == 0) + toggleAllCheckbox.Current.Value = panelFlow.All(panel => panel.Active.Value); + } + + /// + /// Selects all mods. + /// + public void SelectAll() + { + pendingSelectionOperations.Clear(); + + foreach (var button in panelFlow.Where(b => !b.Active.Value)) + pendingSelectionOperations.Enqueue(() => button.Active.Value = true); + } + + /// + /// Deselects all mods. + /// + public void DeselectAll() + { + pendingSelectionOperations.Clear(); + + foreach (var button in panelFlow.Where(b => b.Active.Value)) + pendingSelectionOperations.Enqueue(() => button.Active.Value = false); + } + private class ToggleAllCheckbox : OsuCheckbox { private Color4 accentColour; @@ -247,9 +311,12 @@ namespace osu.Game.Overlays.Mods } } - public ToggleAllCheckbox() + private readonly ModColumn column; + + public ToggleAllCheckbox(ModColumn column) : base(false) { + this.column = column; } protected override void ApplyLabelParameters(SpriteText text) @@ -270,6 +337,16 @@ namespace osu.Game.Overlays.Mods Nub.GlowingAccentColour = AccentHoverColour; Nub.GlowColour = AccentHoverColour.Opacity(0.2f); } + + protected override void OnUserChange(bool value) + { + if (value) + column.SelectAll(); + else + column.DeselectAll(); + } } + + #endregion } } From a80b4334ff841c5e09662159589b9354c39f40ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 12:18:59 +0100 Subject: [PATCH 04/68] Tweak layout of column display for better spacing --- osu.Game/Overlays/Mods/ModColumn.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 8ca968368e..15b2874edb 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -108,9 +108,8 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, RowDimensions = new[] { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10) + new Dimension(GridSizeMode.AutoSize), + new Dimension() }, Content = new[] { @@ -118,7 +117,7 @@ namespace osu.Game.Overlays.Mods { controlContainer = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, Padding = new MarginPadding { Horizontal = 20 } } }, @@ -133,14 +132,10 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0, 10), - Padding = new MarginPadding - { - Horizontal = 10 - }, + Padding = new MarginPadding(10) } } - }, - new[] { Empty() } + } } } } @@ -161,6 +156,12 @@ namespace osu.Game.Overlays.Mods LabelText = "Enable All", Shear = new Vector2(-ModPanel.SHEAR_X, 0) }); + panelFlow.Padding = new MarginPadding + { + Top = 0, + Bottom = 10, + Horizontal = 10 + }; } } From fe4e4bf9c5295d04bfcbac3e38e9127b3855b0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 13:09:06 +0100 Subject: [PATCH 05/68] Add test coverage of multiselection behaviour --- .../UserInterface/TestSceneModColumn.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index e58649d989..0401d516ed 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Catch; @@ -12,11 +16,12 @@ using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneModColumn : OsuTestScene + public class TestSceneModColumn : OsuManualInputManagerTestScene { [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); @@ -58,6 +63,25 @@ namespace osu.Game.Tests.Visual.UserInterface Origin = Anchor.Centre } }); + + clickToggle(); + AddUntilStep("all panels selected", () => this.ChildrenOfType().All(panel => panel.Active.Value)); + + clickToggle(); + AddUntilStep("all panels deselected", () => this.ChildrenOfType().All(panel => !panel.Active.Value)); + + AddStep("manually activate all panels", () => this.ChildrenOfType().ForEach(panel => panel.Active.Value = true)); + AddUntilStep("checkbox selected", () => this.ChildrenOfType().Single().Current.Value); + + AddStep("deselect first panel", () => this.ChildrenOfType().First().Active.Value = false); + AddUntilStep("checkbox selected", () => !this.ChildrenOfType().Single().Current.Value); + + void clickToggle() => AddStep("click toggle", () => + { + var checkbox = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(checkbox); + InputManager.Click(MouseButton.Left); + }); } } } From a83f96b026a9d5657d60eda44b594ef090014d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 13:40:52 +0100 Subject: [PATCH 06/68] Add filtering support to mod column --- .../UserInterface/TestSceneModColumn.cs | 50 +++++++++++++++++++ osu.Game/Overlays/Mods/ModColumn.cs | 37 ++++++++++++-- osu.Game/Overlays/Mods/ModPanel.cs | 17 +++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 0401d516ed..71a974da7d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -83,5 +84,54 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); } + + [Test] + public void TestFiltering() + { + TestModColumn column = null; + + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = column = new TestModColumn(ModType.Fun, true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + + AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + + clickToggle(); + AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); + AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); + + AddStep("unset filter", () => column.Filter = null); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value); + + AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); + + void clickToggle() => AddStep("click toggle", () => + { + var checkbox = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(checkbox); + InputManager.Click(MouseButton.Left); + }); + } + + private class TestModColumn : ModColumn + { + public new bool SelectionAnimationRunning => base.SelectionAnimationRunning; + + public TestModColumn(ModType modType, bool allowBulkSelection) + : base(modType, allowBulkSelection) + { + } + } } } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 15b2874edb..eae57f3574 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -28,6 +28,18 @@ namespace osu.Game.Overlays.Mods { public class ModColumn : CompositeDrawable { + private Func? filter; + + public Func? Filter + { + get => filter; + set + { + filter = value; + updateFilter(); + } + } + private readonly ModType modType; private readonly Bindable>> availableMods = new Bindable>>(); @@ -220,9 +232,12 @@ namespace osu.Game.Overlays.Mods LoadComponentsAsync(panels, loaded => { panelFlow.ChildrenEnumerable = loaded; + foreach (var panel in panelFlow) panel.Active.BindValueChanged(_ => updateToggleState()); updateToggleState(); + + updateFilter(); }, (cancellationTokenSource = new CancellationTokenSource()).Token); } @@ -235,6 +250,8 @@ namespace osu.Game.Overlays.Mods private readonly Queue pendingSelectionOperations = new Queue(); + protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0; + protected override void Update() { base.Update(); @@ -260,8 +277,8 @@ namespace osu.Game.Overlays.Mods private void updateToggleState() { - if (toggleAllCheckbox != null && pendingSelectionOperations.Count == 0) - toggleAllCheckbox.Current.Value = panelFlow.All(panel => panel.Active.Value); + if (toggleAllCheckbox != null && !SelectionAnimationRunning) + toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); } /// @@ -271,7 +288,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in panelFlow.Where(b => !b.Active.Value)) + foreach (var button in panelFlow.Where(b => !b.Active.Value && !b.Filtered.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = true); } @@ -282,7 +299,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in panelFlow.Where(b => b.Active.Value)) + foreach (var button in panelFlow.Where(b => b.Active.Value && !b.Filtered.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = false); } @@ -349,5 +366,17 @@ namespace osu.Game.Overlays.Mods } #endregion + + #region Filtering support + + private void updateFilter() + { + foreach (var modPanel in panelFlow) + modPanel.ApplyFilter(Filter); + + updateToggleState(); + } + + #endregion } } diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 446ebe12c5..7e4d19850d 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -28,6 +29,7 @@ namespace osu.Game.Overlays.Mods { public Mod Mod { get; } public BindableBool Active { get; } = new BindableBool(); + public BindableBool Filtered { get; } = new BindableBool(); protected readonly Box Background; protected readonly Container SwitchContainer; @@ -157,6 +159,7 @@ namespace osu.Game.Overlays.Mods playStateChangeSamples(); UpdateState(); }); + Filtered.BindValueChanged(_ => updateFilterState()); UpdateState(); FinishTransforms(true); @@ -235,5 +238,19 @@ namespace osu.Game.Overlays.Mods TextBackground.FadeColour(textBackgroundColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); } + + #region Filtering support + + public void ApplyFilter(Func? filter) + { + Filtered.Value = filter != null && !filter.Invoke(Mod); + } + + private void updateFilterState() + { + this.FadeTo(Filtered.Value ? 0 : 1); + } + + #endregion } } From b690df05de195561d8d691d476982323087aee93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 14:14:19 +0100 Subject: [PATCH 07/68] Hide multiselection checkbox if everything is filtered --- osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs | 8 ++++++++ osu.Game/Overlays/Mods/ModColumn.cs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 71a974da7d..534e9e0144 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -116,6 +116,14 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); + AddStep("filter out everything", () => column.Filter = _ => false); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => panel.Filtered.Value)); + AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); + + AddStep("inset filter", () => column.Filter = null); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("checkbox hidden", () => column.ChildrenOfType().Single().IsPresent); + void clickToggle() => AddStep("click toggle", () => { var checkbox = this.ChildrenOfType().Single(); diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index eae57f3574..649f54a96a 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -278,7 +278,10 @@ namespace osu.Game.Overlays.Mods private void updateToggleState() { if (toggleAllCheckbox != null && !SelectionAnimationRunning) + { + toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0; toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); + } } /// From 16c6b9b3b315096d0a8be6638aaac445a475f124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 14:10:35 +0100 Subject: [PATCH 08/68] Add keyboard selection support to mod column --- .../UserInterface/TestSceneModColumn.cs | 37 +++++++++++++++++++ osu.Game/Overlays/Mods/ModColumn.cs | 25 ++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 534e9e0144..01ef4b4b60 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -132,6 +132,43 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + [Test] + public void TestKeyboardSelection() + { + ModColumn column = null; + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = column = new ModColumn(ModType.DifficultyReduction, true, new Key[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + + AddStep("press W", () => InputManager.Key(Key.W)); + AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("press W again", () => InputManager.Key(Key.W)); + AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("set filter to NF", () => column.Filter = mod => mod.Acronym == "NF"); + + AddStep("press W", () => InputManager.Key(Key.W)); + AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("press W again", () => InputManager.Key(Key.W)); + AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("filter out everything", () => column.Filter = _ => false); + + AddStep("press W", () => InputManager.Key(Key.W)); + AddAssert("NF panel not selected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("clear filter", () => column.Filter = null); + } + private class TestModColumn : ModColumn { public new bool SelectionAnimationRunning => base.SelectionAnimationRunning; diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 649f54a96a..dd3b7274fa 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -21,6 +22,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; using osuTK.Graphics; +using osuTK.Input; #nullable enable @@ -41,6 +43,7 @@ namespace osu.Game.Overlays.Mods } private readonly ModType modType; + private readonly Key[]? toggleKeys; private readonly Bindable>> availableMods = new Bindable>>(); @@ -55,9 +58,10 @@ namespace osu.Game.Overlays.Mods private const float header_height = 60; - public ModColumn(ModType modType, bool allowBulkSelection) + public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) { this.modType = modType; + this.toggleKeys = toggleKeys; Width = 450; RelativeSizeAxes = Axes.Y; @@ -381,5 +385,24 @@ namespace osu.Game.Overlays.Mods } #endregion + + #region Keyboard selection support + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.ControlPressed || e.AltPressed) return false; + if (toggleKeys == null) return false; + + int index = Array.IndexOf(toggleKeys, e.Key); + if (index < 0) return false; + + var panel = panelFlow.ElementAtOrDefault(index); + if (panel == null || panel.Filtered.Value) return false; + + panel.Active.Toggle(); + return true; + } + + #endregion } } From 774952addaf2c2f65a59b470a8c3475cd0024f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Feb 2022 23:08:31 +0100 Subject: [PATCH 09/68] Rescale components from figma to real dimensions --- osu.Game/Overlays/Mods/ModColumn.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index dd3b7274fa..7f3cc8249f 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -56,14 +56,14 @@ namespace osu.Game.Overlays.Mods private Colour4 accentColour; - private const float header_height = 60; + private const float header_height = 42; public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) { this.modType = modType; this.toggleKeys = toggleKeys; - Width = 450; + Width = 320; RelativeSizeAxes = Axes.Y; Shear = new Vector2(ModPanel.SHEAR_X, 0); CornerRadius = ModPanel.CORNER_RADIUS; @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Mods }, headerText = new OsuTextFlowContainer(t => { - t.Font = OsuFont.TorusAlternate.With(size: 24); + t.Font = OsuFont.TorusAlternate.With(size: 17); t.Shadow = false; t.Colour = Colour4.Black; }) @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Mods Shear = new Vector2(-ModPanel.SHEAR_X, 0), Padding = new MarginPadding { - Horizontal = 15, + Horizontal = 17, Bottom = ModPanel.CORNER_RADIUS } } @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = ModPanel.CORNER_RADIUS, - BorderThickness = 4, + BorderThickness = 3, Children = new Drawable[] { contentBackground = new Box @@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Mods controlContainer = new Container { RelativeSizeAxes = Axes.X, - Padding = new MarginPadding { Horizontal = 20 } + Padding = new MarginPadding { Horizontal = 14 } } }, new Drawable[] @@ -147,8 +147,8 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0, 10), - Padding = new MarginPadding(10) + Spacing = new Vector2(0, 7), + Padding = new MarginPadding(7) } } } @@ -163,11 +163,12 @@ namespace osu.Game.Overlays.Mods if (allowBulkSelection) { - controlContainer.Height = 50; + controlContainer.Height = 35; controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, LabelText = "Enable All", Shear = new Vector2(-ModPanel.SHEAR_X, 0) @@ -175,8 +176,8 @@ namespace osu.Game.Overlays.Mods panelFlow.Padding = new MarginPadding { Top = 0, - Bottom = 10, - Horizontal = 10 + Bottom = 7, + Horizontal = 7 }; } } From 4a555d067da4804d307b74c7bfd90fb7fbb6635c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 14:32:50 +0900 Subject: [PATCH 10/68] Change `ModPanel` to not handle `OnMouseDown` to allow drag scrolling in `ModColumn` --- osu.Game/Overlays/Mods/ModPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 7e4d19850d..312171cf74 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -193,7 +193,7 @@ namespace osu.Game.Overlays.Mods mouseDown = true; UpdateState(); - return true; + return false; } protected override void OnMouseUp(MouseUpEvent e) From 2e96f74c94a008e62a523e9ca15cf0b21ba05a71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 16:37:14 +0900 Subject: [PATCH 11/68] Allow `LegacyScoreEncoder` to be used without a beatmap if frames are already legacy frames --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 9460ec680c..14a6495282 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -5,9 +5,11 @@ using System; using System.IO; using System.Linq; using System.Text; +using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; +using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -29,12 +31,21 @@ namespace osu.Game.Scoring.Legacy private readonly Score score; private readonly IBeatmap beatmap; - public LegacyScoreEncoder(Score score, IBeatmap beatmap) + /// + /// Create a new score encoder for a specific score. + /// + /// The score to be encoded. + /// The beatmap used to convert frames for the score. May be null if the frames are already s. + /// + public LegacyScoreEncoder(Score score, [CanBeNull] IBeatmap beatmap) { this.score = score; this.beatmap = beatmap; - if (score.ScoreInfo.BeatmapInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.BeatmapInfo.Ruleset.OnlineID > 3) + if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame)) + throw new ArgumentException("Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); + + if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > 3) throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } From 723e96309ae8b75b3a89a940ddebeef8e0ea45cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 16:40:00 +0900 Subject: [PATCH 12/68] Only convert non-legacy frames (and throw on conversion failure) --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 14a6495282..3355efc830 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -112,11 +113,16 @@ namespace osu.Game.Scoring.Legacy { int lastTime = 0; - foreach (var f in score.Replay.Frames.OfType().Select(f => f.ToLegacy(beatmap))) + foreach (var f in score.Replay.Frames) { + var legacyFrame = getLegacyFrame(f); + + if (legacyFrame == null) + throw new ArgumentException("One or more frames could not be converted to legacy frames"); + // Rounding because stable could only parse integral values - int time = (int)Math.Round(f.Time); - replayData.Append(FormattableString.Invariant($"{time - lastTime}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState},")); + int time = (int)Math.Round(legacyFrame.Time); + replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); lastTime = time; } } @@ -128,6 +134,14 @@ namespace osu.Game.Scoring.Legacy } } + private LegacyReplayFrame getLegacyFrame(ReplayFrame replayFrame) + { + if (replayFrame is LegacyReplayFrame legacyFrame) + return legacyFrame; + + return (replayFrame as IConvertibleReplayFrame)?.ToLegacy(beatmap); + } + private string getHpGraphFormatted() { // todo: implement, maybe? From 52e50db6b94ca11aa8df4ca2b14dbbb39c68ac47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 16:41:39 +0900 Subject: [PATCH 13/68] Enable `nullable` for `LegacyScoreEncoder` --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 3355efc830..1e39b66ead 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -5,7 +5,6 @@ using System; using System.IO; using System.Linq; using System.Text; -using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; @@ -14,6 +13,8 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; +#nullable enable + namespace osu.Game.Scoring.Legacy { public class LegacyScoreEncoder @@ -30,7 +31,7 @@ namespace osu.Game.Scoring.Legacy public const int FIRST_LAZER_VERSION = 30000000; private readonly Score score; - private readonly IBeatmap beatmap; + private readonly IBeatmap? beatmap; /// /// Create a new score encoder for a specific score. @@ -38,16 +39,16 @@ namespace osu.Game.Scoring.Legacy /// The score to be encoded. /// The beatmap used to convert frames for the score. May be null if the frames are already s. /// - public LegacyScoreEncoder(Score score, [CanBeNull] IBeatmap beatmap) + public LegacyScoreEncoder(Score score, IBeatmap? beatmap) { this.score = score; this.beatmap = beatmap; if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame)) - throw new ArgumentException("Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); + throw new ArgumentException(@"Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > 3) - throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); + throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } public void Encode(Stream stream) @@ -117,9 +118,6 @@ namespace osu.Game.Scoring.Legacy { var legacyFrame = getLegacyFrame(f); - if (legacyFrame == null) - throw new ArgumentException("One or more frames could not be converted to legacy frames"); - // Rounding because stable could only parse integral values int time = (int)Math.Round(legacyFrame.Time); replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); @@ -136,10 +134,17 @@ namespace osu.Game.Scoring.Legacy private LegacyReplayFrame getLegacyFrame(ReplayFrame replayFrame) { - if (replayFrame is LegacyReplayFrame legacyFrame) - return legacyFrame; + switch (replayFrame) + { + case LegacyReplayFrame legacyFrame: + return legacyFrame; - return (replayFrame as IConvertibleReplayFrame)?.ToLegacy(beatmap); + case IConvertibleReplayFrame convertibleFrame: + return convertibleFrame.ToLegacy(beatmap); + + default: + throw new ArgumentException(@"Frame could not be converted to legacy frames", nameof(replayFrame)); + } } private string getHpGraphFormatted() From 42b27e305081884b3442b01e1b6de3e7ecbaee74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Feb 2022 20:44:13 +0100 Subject: [PATCH 14/68] Clean up test step names --- osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 01ef4b4b60..01afde8f7e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("checkbox selected", () => this.ChildrenOfType().Single().Current.Value); AddStep("deselect first panel", () => this.ChildrenOfType().First().Active.Value = false); - AddUntilStep("checkbox selected", () => !this.ChildrenOfType().Single().Current.Value); + AddUntilStep("checkbox not selected", () => !this.ChildrenOfType().Single().Current.Value); void clickToggle() => AddStep("click toggle", () => { @@ -121,8 +121,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); AddStep("inset filter", () => column.Filter = null); - AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); - AddUntilStep("checkbox hidden", () => column.ChildrenOfType().Single().IsPresent); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent); void clickToggle() => AddStep("click toggle", () => { From 6cc972aa6afc457ca0815ed11194d9d5ce07a79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Feb 2022 21:36:13 +0100 Subject: [PATCH 15/68] Fix test failures by waiting for panel load --- .../Visual/UserInterface/TestSceneModColumn.cs | 9 +++++++-- osu.Game/Overlays/Mods/ModColumn.cs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 01afde8f7e..e47ae860c6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -54,17 +54,20 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestMultiSelection() { + ModColumn column = null; AddStep("create content", () => Child = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(30), - Child = new ModColumn(ModType.DifficultyIncrease, true) + Child = column = new ModColumn(ModType.DifficultyIncrease, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre } }); + AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded); + clickToggle(); AddUntilStep("all panels selected", () => this.ChildrenOfType().All(panel => panel.Active.Value)); @@ -140,13 +143,15 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(30), - Child = column = new ModColumn(ModType.DifficultyReduction, true, new Key[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) + Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) { Anchor = Anchor.Centre, Origin = Anchor.Centre } }); + AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded); + AddStep("press W", () => InputManager.Key(Key.W)); AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 7f3cc8249f..b615b7bd32 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -56,6 +57,9 @@ namespace osu.Game.Overlays.Mods private Colour4 accentColour; + private Task? latestLoadTask; + internal bool ItemsLoaded => latestLoadTask == null; + private const float header_height = 42; public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) @@ -234,7 +238,9 @@ namespace osu.Game.Overlays.Mods Shear = new Vector2(-ModPanel.SHEAR_X, 0) }); - LoadComponentsAsync(panels, loaded => + Task? loadTask; + + latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded => { panelFlow.ChildrenEnumerable = loaded; @@ -244,6 +250,11 @@ namespace osu.Game.Overlays.Mods updateFilter(); }, (cancellationTokenSource = new CancellationTokenSource()).Token); + loadTask.ContinueWith(_ => + { + if (loadTask == latestLoadTask) + latestLoadTask = null; + }); } #region Bulk select / deselect From e8701f46f115348b502686916cc9f27fbbd5df01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Feb 2022 21:39:21 +0100 Subject: [PATCH 16/68] Add xmldoc to `Filter` to explain usage --- osu.Game/Overlays/Mods/ModColumn.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index b615b7bd32..3654f5246d 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -33,6 +33,11 @@ namespace osu.Game.Overlays.Mods { private Func? filter; + /// + /// Function determining whether each mod in the column should be displayed. + /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. + /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden. + /// public Func? Filter { get => filter; From 899b95e61bfb7d0a21a58cd2a9ede511fe554408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Feb 2022 21:46:58 +0100 Subject: [PATCH 17/68] Do not delay inital mod update by a frame --- osu.Game/Overlays/Mods/ModColumn.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 3654f5246d..736a0205e2 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -224,7 +224,8 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); - availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods), true); + availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); + updateMods(); } private CancellationTokenSource? cancellationTokenSource; From eb75a29b2024b1001001fe4ef7b523624e33ef3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 12:07:03 +0900 Subject: [PATCH 18/68] Use constant for maximum legacy ruleset id --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 1e39b66ead..1326395695 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -47,7 +48,7 @@ namespace osu.Game.Scoring.Legacy if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame)) throw new ArgumentException(@"Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); - if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > 3) + if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > ILegacyRuleset.MAX_LEGACY_RULESET_ID) throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } From 7fa58427832e5dfaa6f9743650be7df15645a414 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 18:30:55 +0900 Subject: [PATCH 19/68] Add global statistics output for all realm reads/writes --- osu.Game/Database/RealmAccess.cs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 9bdbebfe89..f63e858b6f 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -14,14 +14,14 @@ using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Game.Configuration; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; -using osu.Game.Skinning; -using osu.Game.Stores; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Skinning; +using osu.Game.Stores; using Realms; using Realms.Exceptions; @@ -85,6 +85,14 @@ namespace osu.Game.Database private static readonly GlobalStatistic total_subscriptions = GlobalStatistics.Get(@"Realm", @"Subscriptions"); + private static readonly GlobalStatistic total_reads_update = GlobalStatistics.Get(@"Realm", @"Reads (Update)"); + + private static readonly GlobalStatistic total_reads_async = GlobalStatistics.Get(@"Realm", @"Reads (Async)"); + + private static readonly GlobalStatistic total_writes_update = GlobalStatistics.Get(@"Realm", @"Writes (Update)"); + + private static readonly GlobalStatistic total_writes_async = GlobalStatistics.Get(@"Realm", @"Writes (Async)"); + private readonly object realmLock = new object(); private Realm? updateRealm; @@ -213,8 +221,12 @@ namespace osu.Game.Database public T Run(Func action) { if (ThreadSafety.IsUpdateThread) + { + total_reads_update.Value++; return action(Realm); + } + total_reads_async.Value++; using (var realm = getRealmInstance()) return action(realm); } @@ -226,9 +238,13 @@ namespace osu.Game.Database public void Run(Action action) { if (ThreadSafety.IsUpdateThread) + { + total_reads_update.Value++; action(Realm); + } else { + total_reads_async.Value++; using (var realm = getRealmInstance()) action(realm); } @@ -241,9 +257,14 @@ namespace osu.Game.Database public void Write(Action action) { if (ThreadSafety.IsUpdateThread) + { + total_writes_update.Value++; Realm.Write(action); + } else { + total_writes_async.Value++; + using (var realm = getRealmInstance()) realm.Write(action); } From 9a117467b5ea36389da063102c051ce7eef86ed2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 18:31:33 +0900 Subject: [PATCH 20/68] Add `RealmAccess.WriteAsync` method --- .../RealmSubscriptionRegistrationTests.cs | 29 +++++++++++++++++++ osu.Game/Database/RealmAccess.cs | 12 ++++++++ 2 files changed, 41 insertions(+) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index d62ce3b585..02d617d0e0 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Tests.Resources; @@ -18,6 +20,33 @@ namespace osu.Game.Tests.Database [TestFixture] public class RealmSubscriptionRegistrationTests : RealmTest { + [Test] + public void TestSubscriptionWithAsyncWrite() + { + ChangeSet? lastChanges = null; + + RunTestWithRealm((realm, _) => + { + var registration = realm.RegisterForNotifications(r => r.All(), onChanged); + + realm.Run(r => r.Refresh()); + + // Without forcing the write onto its own thread, realm will internally run the operation synchronously, which can cause a deadlock with `WaitSafely`. + Task.Run(async () => + { + await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + }).WaitSafely(); + + realm.Run(r => r.Refresh()); + + Assert.That(lastChanges?.InsertedIndices, Has.One.Items); + + registration.Dispose(); + }); + + void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => lastChanges = changes; + } + [Test] public void TestSubscriptionWithContextLoss() { diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f63e858b6f..bf2b48ea52 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Input.Bindings; @@ -270,6 +271,17 @@ namespace osu.Game.Database } } + /// + /// Write changes to realm asynchronously, guaranteeing order of execution. + /// + /// The work to run. + public async Task WriteAsync(Action action) + { + total_writes_async.Value++; + using (var realm = getRealmInstance()) + await realm.WriteAsync(action); + } + /// /// Subscribe to a realm collection and begin watching for asynchronous changes. /// From 4117a6adf757afa89277246396d2c5e6f59b7cfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 14:13:27 +0900 Subject: [PATCH 21/68] Move player loader audio settings to new group --- osu.Game/Screens/Play/PlayerLoader.cs | 1 + .../Play/PlayerSettings/AudioSettings.cs | 29 +++++++++++++++++++ .../Play/PlayerSettings/VisualSettings.cs | 3 -- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 6009c85583..20c41958c9 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -167,6 +167,7 @@ namespace osu.Game.Screens.Play Children = new PlayerSettingsGroup[] { VisualSettings = new VisualSettings(), + new AudioSettings(), new InputSettings() } }, diff --git a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs new file mode 100644 index 0000000000..93457980f3 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Configuration; + +namespace osu.Game.Screens.Play.PlayerSettings +{ + public class AudioSettings : PlayerSettingsGroup + { + private readonly PlayerCheckbox beatmapHitsoundsToggle; + + public AudioSettings() + : base("Audio Settings") + { + Children = new Drawable[] + { + beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); + } + } +} diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index a97078c461..81950efa9e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -15,7 +15,6 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerCheckbox showStoryboardToggle; private readonly PlayerCheckbox beatmapSkinsToggle; private readonly PlayerCheckbox beatmapColorsToggle; - private readonly PlayerCheckbox beatmapHitsoundsToggle; public VisualSettings() : base("Visual Settings") @@ -45,7 +44,6 @@ namespace osu.Game.Screens.Play.PlayerSettings showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboard / Video" }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" }, beatmapColorsToggle = new PlayerCheckbox { LabelText = "Beatmap colours" }, - beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } }; } @@ -57,7 +55,6 @@ namespace osu.Game.Screens.Play.PlayerSettings showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapColorsToggle.Current = config.GetBindable(OsuSetting.BeatmapColours); - beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); } } } From 5e47e35f0d2b9d85b3f01eeaa54e40db1b3fa998 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 15:40:37 +0900 Subject: [PATCH 22/68] Add ability to change distribution of test `HitEvent`s --- .../Ranking/TestSceneHitEventTimingDistributionGraph.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 4bc843096f..221001e40b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -71,16 +71,16 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - public static List CreateDistributedHitEvents() + public static List CreateDistributedHitEvents(double centre = 0, double range = 25) { var hitEvents = new List(); - for (int i = 0; i < 50; i++) + for (int i = 0; i < range * 2; i++) { - int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2)); + int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)); for (int j = 0; j < count; j++) - hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null)); + hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, new HitCircle(), new HitCircle(), null)); } return hitEvents; From 1847f69bf95a95fe7dce07ebb2d4119e946aa470 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 15:40:19 +0900 Subject: [PATCH 23/68] Add basic beatmap offset adjustment control --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 27 ++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 88 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs create mode 100644 osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs new file mode 100644 index 0000000000..bd87039797 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Tests.Visual.Ranking; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneBeatmapOffsetControl : OsuTestScene + { + [Test] + public void TestDisplay() + { + Child = new PlayerSettingsGroup("Some settings") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new BeatmapOffsetControl(TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(-4.5)) + } + }; + } + } +} diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs new file mode 100644 index 0000000000..c05c5beb31 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -0,0 +1,88 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Ranking.Statistics; +using osuTK; + +namespace osu.Game.Screens.Play.PlayerSettings +{ + public class BeatmapOffsetControl : CompositeDrawable + { + private readonly SettingsButton useAverageButton; + + private readonly double lastPlayAverage; + + public Bindable Current { get; } = new BindableDouble + { + Default = 0, + Value = 0, + MinValue = -50, + MaxValue = 50, + Precision = 0.1, + }; + + public BeatmapOffsetControl(IReadOnlyList hitEvents) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + FillFlowContainer flow; + + InternalChild = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new PlayerSliderBar + { + KeyboardStep = 5, + LabelText = "Beatmap offset", + Current = Current, + }, + } + }; + + if (hitEvents.CalculateAverageHitError() is double average) + { + lastPlayAverage = average; + + flow.AddRange(new Drawable[] + { + new OsuSpriteText + { + Text = "Last play:" + }, + new HitEventTimingDistributionGraph(hitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 50, + }, + new AverageHitError(hitEvents), + useAverageButton = new SettingsButton + { + Text = "Calibrate using last play", + Action = () => Current.Value = lastPlayAverage + }, + }); + } + + Current.BindValueChanged(offset => + { + if (useAverageButton != null) + { + useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; + } + }, true); + } + } +} From 350b0b488c048d114cb25adc4353517d75ac2081 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 14:47:06 +0900 Subject: [PATCH 24/68] TODO: Get score from previous play session for further analysis --- osu.Game/Screens/Play/Player.cs | 6 +++++- osu.Game/Screens/Play/PlayerLoader.cs | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d4b02622d3..86ea412488 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -136,7 +136,11 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; - protected Score Score { get; private set; } + /// + /// The score for the current play session. + /// Available only after the player is loaded. + /// + public Score Score { get; private set; } /// /// Create a new player instance. diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 20c41958c9..863246cd05 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -23,6 +23,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Users; @@ -226,6 +227,14 @@ namespace osu.Game.Screens.Play { base.OnResuming(last); + var lastScore = player.Score; + + if (lastScore != null) + { + // TODO: use this + double? lastPlayHitError = lastScore.ScoreInfo.HitEvents.CalculateAverageHitError(); + } + // prepare for a retry. player = null; playerConsumed = false; From 7215f3f66b6c7891f7d9b8de14ff6ba7fe155ed8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:02:48 +0900 Subject: [PATCH 25/68] Fix `CalculateAverageHitError` throwing if there are zero `HitEvent`s --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 637d0a872a..fea13cf4b6 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -29,8 +29,15 @@ namespace osu.Game.Rulesets.Scoring /// A non-null value if unstable rate could be calculated, /// and if unstable rate cannot be calculated due to being empty. /// - public static double? CalculateAverageHitError(this IEnumerable hitEvents) => - hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).Average(); + public static double? CalculateAverageHitError(this IEnumerable hitEvents) + { + double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + + if (timeOffsets.Length == 0) + return null; + + return timeOffsets.Average(); + } private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); From 2901d2a6505c6a1a44ee86101635cd35837cc231 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:14:57 +0900 Subject: [PATCH 26/68] Hook offset adjustment control up to last play via `PlayerLoader` --- osu.Game/Screens/Play/PlayerLoader.cs | 11 +-- .../Play/PlayerSettings/AudioSettings.cs | 10 ++- .../PlayerSettings/BeatmapOffsetControl.cs | 79 +++++++++++-------- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 863246cd05..f6d63a8ec5 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -23,7 +23,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Users; @@ -62,6 +61,8 @@ namespace osu.Game.Screens.Play protected VisualSettings VisualSettings { get; private set; } + protected AudioSettings AudioSettings { get; private set; } + protected Task LoadTask { get; private set; } protected Task DisposalTask { get; private set; } @@ -168,7 +169,7 @@ namespace osu.Game.Screens.Play Children = new PlayerSettingsGroup[] { VisualSettings = new VisualSettings(), - new AudioSettings(), + AudioSettings = new AudioSettings(), new InputSettings() } }, @@ -229,11 +230,7 @@ namespace osu.Game.Screens.Play var lastScore = player.Score; - if (lastScore != null) - { - // TODO: use this - double? lastPlayHitError = lastScore.ScoreInfo.HitEvents.CalculateAverageHitError(); - } + AudioSettings.ReferenceScore.Value = lastScore?.ScoreInfo; // prepare for a retry. player = null; diff --git a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs index 93457980f3..32de5333e1 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs @@ -2,13 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Configuration; +using osu.Game.Scoring; namespace osu.Game.Screens.Play.PlayerSettings { public class AudioSettings : PlayerSettingsGroup { + public Bindable ReferenceScore { get; } = new Bindable(); + private readonly PlayerCheckbox beatmapHitsoundsToggle; public AudioSettings() @@ -16,7 +20,11 @@ namespace osu.Game.Screens.Play.PlayerSettings { Children = new Drawable[] { - beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } + beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" }, + new BeatmapOffsetControl + { + ReferenceScore = { BindTarget = ReferenceScore }, + }, }; } diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index c05c5beb31..5d287a3730 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -1,13 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; @@ -15,9 +15,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { public class BeatmapOffsetControl : CompositeDrawable { - private readonly SettingsButton useAverageButton; - - private readonly double lastPlayAverage; + public Bindable ReferenceScore { get; } = new Bindable(); public Bindable Current { get; } = new BindableDouble { @@ -28,14 +26,18 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.1, }; - public BeatmapOffsetControl(IReadOnlyList hitEvents) + private SettingsButton useAverageButton; + + private double lastPlayAverage; + + private readonly FillFlowContainer referenceScoreContainer; + + public BeatmapOffsetControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - FillFlowContainer flow; - - InternalChild = flow = new FillFlowContainer + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -49,32 +51,17 @@ namespace osu.Game.Screens.Play.PlayerSettings LabelText = "Beatmap offset", Current = Current, }, + referenceScoreContainer = new FillFlowContainer + { + Spacing = new Vector2(10), + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, } }; - if (hitEvents.CalculateAverageHitError() is double average) - { - lastPlayAverage = average; - - flow.AddRange(new Drawable[] - { - new OsuSpriteText - { - Text = "Last play:" - }, - new HitEventTimingDistributionGraph(hitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 50, - }, - new AverageHitError(hitEvents), - useAverageButton = new SettingsButton - { - Text = "Calibrate using last play", - Action = () => Current.Value = lastPlayAverage - }, - }); - } + ReferenceScore.BindValueChanged(scoreChanged, true); Current.BindValueChanged(offset => { @@ -84,5 +71,35 @@ namespace osu.Game.Screens.Play.PlayerSettings } }, true); } + + private void scoreChanged(ValueChangedEvent score) + { + if (!(score.NewValue?.HitEvents.CalculateAverageHitError() is double average)) + { + referenceScoreContainer.Clear(); + return; + } + + lastPlayAverage = average; + + referenceScoreContainer.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Last play:" + }, + new HitEventTimingDistributionGraph(score.NewValue.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 50, + }, + new AverageHitError(score.NewValue.HitEvents), + useAverageButton = new SettingsButton + { + Text = "Calibrate using last play", + Action = () => Current.Value = lastPlayAverage + }, + }; + } } } From fab09575ec30d2cec7aef5487f59b6b8fa10732a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:15:13 +0900 Subject: [PATCH 27/68] Add full testing flow for `BeatmapOffsetControl` --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index bd87039797..3fb10b2d5c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -1,8 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Overlays.Settings; +using osu.Game.Scoring; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Tests.Visual.Ranking; @@ -10,18 +14,46 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneBeatmapOffsetControl : OsuTestScene { + private BeatmapOffsetControl offsetControl; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create control", () => + { + Child = new PlayerSettingsGroup("Some settings") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + offsetControl = new BeatmapOffsetControl() + } + }; + }); + } + [Test] public void TestDisplay() { - Child = new PlayerSettingsGroup("Some settings") + const double average_error = -4.5; + + AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + AddStep("Set reference score", () => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + offsetControl.ReferenceScore.Value = new ScoreInfo { - new BeatmapOffsetControl(TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(-4.5)) - } - }; + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error) + }; + }); + + AddAssert("Has calibration button", () => offsetControl.ChildrenOfType().Any()); + AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value == average_error); + + AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } } } From acf8db13acad60857ed919f00399f0f50c80f869 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:22:51 +0900 Subject: [PATCH 28/68] Store user settings to realm --- osu.Game/Beatmaps/BeatmapInfo.cs | 3 +++ osu.Game/Beatmaps/BeatmapUserSettings.cs | 19 +++++++++++++++++++ osu.Game/Database/RealmAccess.cs | 3 ++- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Beatmaps/BeatmapUserSettings.cs diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 305b3979a0..c6f69286cd 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -40,6 +40,8 @@ namespace osu.Game.Beatmaps [Backlink(nameof(ScoreInfo.BeatmapInfo))] public IQueryable Scores { get; } = null!; + public BeatmapUserSettings UserSettings { get; set; } = null!; + public BeatmapInfo(RulesetInfo? ruleset = null, BeatmapDifficulty? difficulty = null, BeatmapMetadata? metadata = null) { ID = Guid.NewGuid(); @@ -51,6 +53,7 @@ namespace osu.Game.Beatmaps }; Difficulty = difficulty ?? new BeatmapDifficulty(); Metadata = metadata ?? new BeatmapMetadata(); + UserSettings = new BeatmapUserSettings(); } [UsedImplicitly] diff --git a/osu.Game/Beatmaps/BeatmapUserSettings.cs b/osu.Game/Beatmaps/BeatmapUserSettings.cs new file mode 100644 index 0000000000..5c71bf34b1 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapUserSettings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable +using Realms; + +namespace osu.Game.Beatmaps +{ + /// + /// User settings overrides that are attached to a beatmap. + /// + public class BeatmapUserSettings : EmbeddedObject + { + /// + /// An audio offset that can be used for timing adjustments. + /// + public double Offset { get; set; } + } +} diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index bf2b48ea52..fc2f519ead 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -54,8 +54,9 @@ namespace osu.Game.Database /// 11 2021-11-22 Use ShortName instead of RulesetID for ruleset key bindings. /// 12 2021-11-24 Add Status to RealmBeatmapSet. /// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields). + /// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo. /// - private const int schema_version = 13; + private const int schema_version = 14; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. From 047e801da9f9fe31b9a13d6c8c59b6240626fcc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:59:33 +0900 Subject: [PATCH 29/68] Store and retrieve offset from realm --- osu.Game/Database/RealmAccess.cs | 5 +++ .../Play/MasterGameplayClockContainer.cs | 17 +++++---- .../PlayerSettings/BeatmapOffsetControl.cs | 36 +++++++++++++++---- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index fc2f519ead..fb3052d850 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -565,6 +565,11 @@ namespace osu.Game.Database } break; + + case 14: + foreach (var beatmap in migration.NewRealm.All()) + beatmap.UserSettings = new BeatmapUserSettings(); + break; } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 200921680e..d27a989067 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; namespace osu.Game.Screens.Play { @@ -43,7 +44,7 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; - private double totalAppliedOffset => userOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; + private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); @@ -52,7 +53,8 @@ namespace osu.Game.Screens.Play private readonly bool startAtGameplayStart; private readonly double firstHitObjectTime; - private HardwareCorrectionOffsetClock userOffsetClock; + private HardwareCorrectionOffsetClock userGlobalOffsetClock; + private HardwareCorrectionOffsetClock userBeatmapOffsetClock; private HardwareCorrectionOffsetClock platformOffsetClock; private MasterGameplayClock masterGameplayClock; private Bindable userAudioOffset; @@ -69,10 +71,12 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, RealmAccess realm) { userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); + userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + + userBeatmapOffsetClock.Offset = realm.Run(r => r.Find(beatmap.BeatmapInfo.ID).UserSettings?.Offset) ?? 0; // sane default provided by ruleset. startOffset = gameplayStartTime; @@ -161,9 +165,10 @@ namespace osu.Game.Screens.Play platformOffsetClock = new HardwareCorrectionOffsetClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // the final usable gameplay clock with user-set offsets applied. - userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust); + userGlobalOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust); + userBeatmapOffsetClock = new HardwareCorrectionOffsetClock(userGlobalOffsetClock, pauseFreqAdjust); - return masterGameplayClock = new MasterGameplayClock(userOffsetClock); + return masterGameplayClock = new MasterGameplayClock(userBeatmapOffsetClock); } /// diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 5d287a3730..75f8c89d34 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; @@ -60,16 +63,37 @@ namespace osu.Game.Screens.Play.PlayerSettings }, } }; + } + + [Resolved] + private RealmAccess realm { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); ReferenceScore.BindValueChanged(scoreChanged, true); - Current.BindValueChanged(offset => + Current.BindValueChanged(currentChanged); + Current.Value = realm.Run(r => r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings?.Offset) ?? 0; + } + + private void currentChanged(ValueChangedEvent offset) + { + if (useAverageButton != null) { - if (useAverageButton != null) - { - useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; - } - }, true); + useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; + } + + realm.Write(r => + { + var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + + settings.Offset = offset.NewValue; + }); } private void scoreChanged(ValueChangedEvent score) From 071ba5c1dfbf785d45a12713bf3a2d3d3121a710 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 18:28:53 +0900 Subject: [PATCH 30/68] Make writes asynchronously to avoid synchronous overhead --- .../PlayerSettings/BeatmapOffsetControl.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 75f8c89d34..2f2d1b81e5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -78,9 +80,11 @@ namespace osu.Game.Screens.Play.PlayerSettings ReferenceScore.BindValueChanged(scoreChanged, true); Current.BindValueChanged(currentChanged); - Current.Value = realm.Run(r => r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings?.Offset) ?? 0; + Current.Value = realm.Run(r => r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings.Offset); } + private Task realmWrite; + private void currentChanged(ValueChangedEvent offset) { if (useAverageButton != null) @@ -88,12 +92,25 @@ namespace osu.Game.Screens.Play.PlayerSettings useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; } - realm.Write(r => - { - var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + Scheduler.AddOnce(updateOffset); - settings.Offset = offset.NewValue; - }); + void updateOffset() + { + // ensure the previous write has completed. + if (realmWrite?.IsCompleted == false) + { + Scheduler.AddOnce(updateOffset); + return; + } + + realmWrite?.WaitSafely(); + realmWrite = realm.WriteAsync(r => + { + var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + + settings.Offset = offset.NewValue; + }); + } } private void scoreChanged(ValueChangedEvent score) From bb8caabb8be3e0391fee7adede8f0bed09948737 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 18:29:04 +0900 Subject: [PATCH 31/68] Subscribe to changes in offset --- .../Play/MasterGameplayClockContainer.cs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index d27a989067..6a90e7adea 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using osu.Framework; using osu.Framework.Allocation; @@ -70,13 +71,38 @@ namespace osu.Game.Screens.Play firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; } - [BackgroundDependencyLoader] - private void load(OsuConfigManager config, RealmAccess realm) + [Resolved] + private RealmAccess realm { get; set; } + + [Resolved] + private OsuConfigManager config { get; set; } + + private IDisposable beatmapOffsetSubscription; + + protected override void LoadComplete() { + base.LoadComplete(); + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); - userBeatmapOffsetClock.Offset = realm.Run(r => r.Find(beatmap.BeatmapInfo.ID).UserSettings?.Offset) ?? 0; + beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => + { + var userSettings = r.Find(beatmap.BeatmapInfo.ID).UserSettings; + + void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(BeatmapUserSettings.Offset)) + updateOffset(); + } + + updateOffset(); + userSettings.PropertyChanged += onUserSettingsOnPropertyChanged; + + return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged); + + void updateOffset() => userBeatmapOffsetClock.Offset = userSettings.Offset; + }); // sane default provided by ruleset. startOffset = gameplayStartTime; @@ -214,6 +240,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + beatmapOffsetSubscription?.Dispose(); removeSourceClockAdjustments(); } From 99c1ba19aa22f5f88f21f136a27cb3b2c63a392f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:13:40 +0900 Subject: [PATCH 32/68] Allow `BeatmapOffsetControl` to react to external changes to offset --- .../PlayerSettings/BeatmapOffsetControl.cs | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 2f2d1b81e5..487d044f5b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.ComponentModel; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Sprites; @@ -22,7 +24,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { public Bindable ReferenceScore { get; } = new Bindable(); - public Bindable Current { get; } = new BindableDouble + public BindableDouble Current { get; } = new BindableDouble { Default = 0, Value = 0, @@ -73,42 +75,59 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private IBindable beatmap { get; set; } + private IDisposable beatmapOffsetSubscription; + protected override void LoadComplete() { base.LoadComplete(); ReferenceScore.BindValueChanged(scoreChanged, true); + beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => + { + var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + + Current.Value = userSettings.Offset; + userSettings.PropertyChanged += onUserSettingsOnPropertyChanged; + + return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged); + + void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(BeatmapUserSettings.Offset)) + Current.Value = userSettings.Offset; + } + }); + Current.BindValueChanged(currentChanged); - Current.Value = realm.Run(r => r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings.Offset); } private Task realmWrite; private void currentChanged(ValueChangedEvent offset) { - if (useAverageButton != null) - { - useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; - } - Scheduler.AddOnce(updateOffset); void updateOffset() { - // ensure the previous write has completed. + // ensure the previous write has completed. ignoring performance concerns, if we don't do this, the async writes could be out of sequence. if (realmWrite?.IsCompleted == false) { Scheduler.AddOnce(updateOffset); return; } - realmWrite?.WaitSafely(); + if (useAverageButton != null) + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, Current.Value, Current.Precision); + realmWrite = realm.WriteAsync(r => { var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; - settings.Offset = offset.NewValue; + if (Precision.AlmostEquals(settings.Offset, Current.Value)) + return; + + settings.Offset = Current.Value; }); } } @@ -142,5 +161,11 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + beatmapOffsetSubscription?.Dispose(); + } } } From bc2a15db96f945cb0660617027901a4bb784da37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:20:18 +0900 Subject: [PATCH 33/68] Handle cases of beatmaps not existing in realm for tests --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 5 ++++- .../Play/PlayerSettings/BeatmapOffsetControl.cs | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 6a90e7adea..c7c967abfd 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -88,7 +88,10 @@ namespace osu.Game.Screens.Play beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => { - var userSettings = r.Find(beatmap.BeatmapInfo.ID).UserSettings; + var userSettings = r.Find(beatmap.BeatmapInfo.ID)?.UserSettings; + + if (userSettings == null) // only the case for tests. + return null; void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args) { diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 487d044f5b..7fbaaaeffc 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -85,7 +85,10 @@ namespace osu.Game.Screens.Play.PlayerSettings beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => { - var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; + + if (userSettings == null) // only the case for tests. + return null; Current.Value = userSettings.Offset; userSettings.PropertyChanged += onUserSettingsOnPropertyChanged; @@ -122,7 +125,10 @@ namespace osu.Game.Screens.Play.PlayerSettings realmWrite = realm.WriteAsync(r => { - var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + var settings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; + + if (settings == null) // only the case for tests. + return; if (Precision.AlmostEquals(settings.Offset, Current.Value)) return; From 4d9efe771bd5aab0db19e057410a2d678b882ca5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:31:11 +0900 Subject: [PATCH 34/68] Don't display calibration options when the previous play was too short to be useful --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 14 ++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 46 +++++++++++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 3fb10b2d5c..7704233adf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -33,6 +33,20 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [Test] + public void TestTooShortToDisplay() + { + AddStep("Set short reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2) + }; + }); + + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + } + [Test] public void TestDisplay() { diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 7fbaaaeffc..a8ed3f562b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; @@ -75,6 +77,9 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private OsuColour colours { get; set; } + private IDisposable beatmapOffsetSubscription; protected override void LoadComplete() @@ -140,32 +145,53 @@ namespace osu.Game.Screens.Play.PlayerSettings private void scoreChanged(ValueChangedEvent score) { - if (!(score.NewValue?.HitEvents.CalculateAverageHitError() is double average)) - { - referenceScoreContainer.Clear(); - return; - } + var hitEvents = score.NewValue?.HitEvents; - lastPlayAverage = average; + referenceScoreContainer.Clear(); + + if (!(hitEvents?.CalculateAverageHitError() is double average)) + return; referenceScoreContainer.Children = new Drawable[] { new OsuSpriteText { - Text = "Last play:" + Text = "Previous play:" }, - new HitEventTimingDistributionGraph(score.NewValue.HitEvents) + }; + + if (hitEvents.Count < 10) + { + referenceScoreContainer.AddRange(new Drawable[] + { + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Colour = colours.Red1, + Text = "Previous play too short to use for calibration" + }, + }); + + return; + } + + lastPlayAverage = average; + + referenceScoreContainer.AddRange(new Drawable[] + { + new HitEventTimingDistributionGraph(hitEvents) { RelativeSizeAxes = Axes.X, Height = 50, }, - new AverageHitError(score.NewValue.HitEvents), + new AverageHitError(hitEvents), useAverageButton = new SettingsButton { Text = "Calibrate using last play", Action = () => Current.Value = lastPlayAverage }, - }; + }); } protected override void Dispose(bool isDisposing) From 4aee57c9c1f4700116fc740ef6db0c1dddd40860 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:33:30 +0900 Subject: [PATCH 35/68] Add localisation of all beatmap offset strings --- .../BeatmapOffsetControlStrings.cs | 34 +++++++++++++++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 9 ++--- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Localisation/BeatmapOffsetControlStrings.cs diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs new file mode 100644 index 0000000000..7b2a9e50b2 --- /dev/null +++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class BeatmapOffsetControlStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOffsetControl"; + + /// + /// "Beatmap offset" + /// + public static LocalisableString BeatmapOffset => new TranslatableString(getKey(@"beatmap_offset"), @"Beatmap offset"); + + /// + /// "Previous play:" + /// + public static LocalisableString PreviousPlay => new TranslatableString(getKey(@"previous_play"), @"Previous play:"); + + /// + /// "Previous play too short to use for calibration" + /// + public static LocalisableString PreviousPlayTooShortToUseForCalibration => new TranslatableString(getKey(@"previous_play_too_short_to_use_for_calibration"), @"Previous play too short to use for calibration"); + + /// + /// "Calibrate using last play" + /// + public static LocalisableString CalibrateUsingLastPlay => new TranslatableString(getKey(@"calibrate_using_last_play"), @"Calibrate using last play"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index a8ed3f562b..864a46fc8f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.Play.PlayerSettings { @@ -57,7 +58,7 @@ namespace osu.Game.Screens.Play.PlayerSettings new PlayerSliderBar { KeyboardStep = 5, - LabelText = "Beatmap offset", + LabelText = BeatmapOffsetControlStrings.BeatmapOffset, Current = Current, }, referenceScoreContainer = new FillFlowContainer @@ -156,7 +157,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { new OsuSpriteText { - Text = "Previous play:" + Text = BeatmapOffsetControlStrings.PreviousPlay }, }; @@ -169,7 +170,7 @@ namespace osu.Game.Screens.Play.PlayerSettings RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Colour = colours.Red1, - Text = "Previous play too short to use for calibration" + Text = BeatmapOffsetControlStrings.PreviousPlayTooShortToUseForCalibration }, }); @@ -188,7 +189,7 @@ namespace osu.Game.Screens.Play.PlayerSettings new AverageHitError(hitEvents), useAverageButton = new SettingsButton { - Text = "Calibrate using last play", + Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => Current.Value = lastPlayAverage }, }); From 9792f0653ad46fe0b324fa16d341ede9f5f0d0d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:41:51 +0900 Subject: [PATCH 36/68] Don't show calibration controls for autoplay --- .../Play/PlayerSettings/BeatmapOffsetControl.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 864a46fc8f..1cd89f691e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -20,6 +21,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; using osu.Game.Localisation; +using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play.PlayerSettings { @@ -146,11 +148,17 @@ namespace osu.Game.Screens.Play.PlayerSettings private void scoreChanged(ValueChangedEvent score) { - var hitEvents = score.NewValue?.HitEvents; - referenceScoreContainer.Clear(); - if (!(hitEvents?.CalculateAverageHitError() is double average)) + if (score.NewValue == null) + return; + + if (score.NewValue.Mods.Any(m => m is ModAutoplay)) + return; + + var hitEvents = score.NewValue.HitEvents; + + if (!(hitEvents.CalculateAverageHitError() is double average)) return; referenceScoreContainer.Children = new Drawable[] From 7d11cfb301249b20f49ca9f150eaec936f5389fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:51:15 +0900 Subject: [PATCH 37/68] Add detach mapping for `BeatmapUserSettings` --- osu.Game/Database/RealmObjectExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index f89bbbe19d..6dc18df9e0 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -38,6 +38,7 @@ namespace osu.Game.Database c.CreateMap() .ForMember(s => s.Ruleset, cc => cc.Ignore()) .ForMember(s => s.Metadata, cc => cc.Ignore()) + .ForMember(s => s.UserSettings, cc => cc.Ignore()) .ForMember(s => s.Difficulty, cc => cc.Ignore()) .ForMember(s => s.BeatmapSet, cc => cc.Ignore()) .AfterMap((s, d) => @@ -154,6 +155,7 @@ namespace osu.Game.Database c.CreateMap(); c.CreateMap(); + c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); From 6c09237956c6535c54c37ef4ee2877af3cd0f10a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:57:09 +0900 Subject: [PATCH 38/68] Reorder fields in `BeatmapOffsetControl` and `MasterGameplayClockContainer` --- .../Play/MasterGameplayClockContainer.cs | 16 +++++++------- .../PlayerSettings/BeatmapOffsetControl.cs | 22 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index c7c967abfd..2b6db5f59e 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -61,6 +61,14 @@ namespace osu.Game.Screens.Play private Bindable userAudioOffset; private double startOffset; + private IDisposable beatmapOffsetSubscription; + + [Resolved] + private RealmAccess realm { get; set; } + + [Resolved] + private OsuConfigManager config { get; set; } + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) : base(beatmap.Track) { @@ -71,14 +79,6 @@ namespace osu.Game.Screens.Play firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; } - [Resolved] - private RealmAccess realm { get; set; } - - [Resolved] - private OsuConfigManager config { get; set; } - - private IDisposable beatmapOffsetSubscription; - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 1cd89f691e..02359afd16 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -44,6 +44,17 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly FillFlowContainer referenceScoreContainer; + private IDisposable beatmapOffsetSubscription; + + [Resolved] + private RealmAccess realm { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + public BeatmapOffsetControl() { RelativeSizeAxes = Axes.X; @@ -74,17 +85,6 @@ namespace osu.Game.Screens.Play.PlayerSettings }; } - [Resolved] - private RealmAccess realm { get; set; } - - [Resolved] - private IBindable beatmap { get; set; } - - [Resolved] - private OsuColour colours { get; set; } - - private IDisposable beatmapOffsetSubscription; - protected override void LoadComplete() { base.LoadComplete(); From 222f50d2119b9041c430ba7f4a54ab4c87d86dfe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 20:41:54 +0900 Subject: [PATCH 39/68] Fix calibration being back-to-front --- osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs | 2 +- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 7704233adf..42b579bc89 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("Has calibration button", () => offsetControl.ChildrenOfType().Any()); AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); - AddAssert("Offset is adjusted", () => offsetControl.Current.Value == average_error); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error); AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 02359afd16..e6bc510564 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -198,7 +198,7 @@ namespace osu.Game.Screens.Play.PlayerSettings useAverageButton = new SettingsButton { Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, - Action = () => Current.Value = lastPlayAverage + Action = () => Current.Value = -lastPlayAverage }, }); } From 2767dda9d63bf11a9eff75737cd47b53bb705649 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 1 Mar 2022 20:21:32 +0300 Subject: [PATCH 40/68] Add failing test case --- .../Visual/Ranking/TestSceneResultsScreen.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 167acc94c4..cc380df183 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -17,10 +17,13 @@ using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Ranking.Expanded.Statistics; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Tests.Resources; using osuTK; @@ -256,6 +259,23 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value); } + [Test] + public void TestRulesetWithNoPerformanceCalculator() + { + var ruleset = new RulesetWithNoPerformanceCalculator(); + var score = TestResources.CreateTestScoreInfo(ruleset.RulesetInfo); + + AddStep("load results", () => Child = new TestResultsContainer(createResultsScreen(score))); + AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible); + + AddAssert("PP displayed as 0", () => + { + var performance = this.ChildrenOfType().Single(); + var counter = performance.ChildrenOfType().Single(); + return counter.Current.Value == 0; + }); + } + private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo()); private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo()); @@ -367,5 +387,10 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } } + + private class RulesetWithNoPerformanceCalculator : OsuRuleset + { + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; + } } } From 97c54de3bff4787aa8161b4a33dd53500dd3bfdc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 1 Mar 2022 20:43:20 +0300 Subject: [PATCH 41/68] Fix performance statistic not handling rulesets with unimplemented calculator --- .../Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 859b42d66d..95f017d625 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics else { performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely().Total)), cancellationTokenSource.Token); + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()?.Total)), cancellationTokenSource.Token); } } From c342030b2c084790724021aca3b0b3557c54fb76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:10:59 +0900 Subject: [PATCH 42/68] Add specific placeholder message for custom rulesets rather than showing network error --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 3 ++- osu.Game/Online/Leaderboards/Leaderboard.cs | 5 ++++- osu.Game/Online/Leaderboards/LeaderboardState.cs | 3 ++- .../Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 11 +++++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 31bd3a203c..1ed6648131 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -119,7 +119,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure)); AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter)); AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn)); - AddStep(@"Unavailable", () => leaderboard.SetErrorState(LeaderboardState.Unavailable)); + AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable)); + AddStep(@"Beatmap unavailable", () => leaderboard.SetErrorState(LeaderboardState.BeatmapUnavailable)); AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 5dd3e46b4a..dde53c39e4 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -319,7 +319,10 @@ namespace osu.Game.Online.Leaderboards case LeaderboardState.NoneSelected: return new MessagePlaceholder(@"Please select a beatmap!"); - case LeaderboardState.Unavailable: + case LeaderboardState.RulesetUnavailable: + return new MessagePlaceholder(@"Leaderboards are not available for this ruleset!"); + + case LeaderboardState.BeatmapUnavailable: return new MessagePlaceholder(@"Leaderboards are not available for this beatmap!"); case LeaderboardState.NoScores: diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs index 75e2c6e6db..6b07500a98 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -8,7 +8,8 @@ namespace osu.Game.Online.Leaderboards Success, Retrieving, NetworkFailure, - Unavailable, + BeatmapUnavailable, + RulesetUnavailable, NoneSelected, NoScores, NotLoggedIn, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 907a2c9bda..6daaae9d04 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -98,6 +98,7 @@ namespace osu.Game.Screens.Select.Leaderboards protected override APIRequest FetchScores(CancellationToken cancellationToken) { var fetchBeatmapInfo = BeatmapInfo; + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; if (fetchBeatmapInfo == null) { @@ -117,9 +118,15 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } + if (fetchRuleset.OnlineID <= 0 || fetchRuleset.OnlineID > ILegacyRuleset.MAX_LEGACY_RULESET_ID) + { + SetErrorState(LeaderboardState.RulesetUnavailable); + return null; + } + if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) { - SetErrorState(LeaderboardState.Unavailable); + SetErrorState(LeaderboardState.BeatmapUnavailable); return null; } @@ -137,7 +144,7 @@ namespace osu.Game.Screens.Select.Leaderboards else if (filterMods) requestMods = mods.Value; - var req = new GetScoresRequest(fetchBeatmapInfo, ruleset.Value ?? fetchBeatmapInfo.Ruleset, Scope, requestMods); + var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); req.Success += r => { From d4a2645510d2a7e133d60a664d7950244e9dedaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:14:44 +0900 Subject: [PATCH 43/68] Add localisation support for leaderboard error text --- osu.Game/Localisation/LeaderboardStrings.cs | 49 +++++++++++++++++++ osu.Game/Online/Leaderboards/Leaderboard.cs | 15 +++--- .../Online/Placeholders/LoginPlaceholder.cs | 3 +- 3 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Localisation/LeaderboardStrings.cs diff --git a/osu.Game/Localisation/LeaderboardStrings.cs b/osu.Game/Localisation/LeaderboardStrings.cs new file mode 100644 index 0000000000..8e53f8e88c --- /dev/null +++ b/osu.Game/Localisation/LeaderboardStrings.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class LeaderboardStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.Leaderboard"; + + /// + /// "Couldn't fetch scores!" + /// + public static LocalisableString CouldntFetchScores => new TranslatableString(getKey(@"couldnt_fetch_scores"), @"Couldn't fetch scores!"); + + /// + /// "Please select a beatmap!" + /// + public static LocalisableString PleaseSelectABeatmap => new TranslatableString(getKey(@"please_select_a_beatmap"), @"Please select a beatmap!"); + + /// + /// "Leaderboards are not available for this ruleset!" + /// + public static LocalisableString LeaderboardsAreNotAvailableForThisRuleset => new TranslatableString(getKey(@"leaderboards_are_not_available_for_this_ruleset"), @"Leaderboards are not available for this ruleset!"); + + /// + /// "Leaderboards are not available for this beatmap!" + /// + public static LocalisableString LeaderboardsAreNotAvailableForThisBeatmap => new TranslatableString(getKey(@"leaderboards_are_not_available_for_this_beatmap"), @"Leaderboards are not available for this beatmap!"); + + /// + /// "No records yet!" + /// + public static LocalisableString NoRecordsYet => new TranslatableString(getKey(@"no_records_yet"), @"No records yet!"); + + /// + /// "Please sign in to view online leaderboards!" + /// + public static LocalisableString PleaseSignInToViewOnlineLeaderboards => new TranslatableString(getKey(@"please_sign_in_to_view_online_leaderboards"), @"Please sign in to view online leaderboards!"); + + /// + /// "Please invest in an osu!supporter tag to view this leaderboard!" + /// + public static LocalisableString PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard => new TranslatableString(getKey(@"please_invest_in_an_osu_supporter_tag_to_view_this_leaderboard"), @"Please invest in an osu!supporter tag to view this leaderboard!"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index dde53c39e4..c94a6d3361 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -22,6 +22,7 @@ using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Online.Leaderboards { @@ -311,28 +312,28 @@ namespace osu.Game.Online.Leaderboards switch (state) { case LeaderboardState.NetworkFailure: - return new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) + return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync) { Action = RefetchScores }; case LeaderboardState.NoneSelected: - return new MessagePlaceholder(@"Please select a beatmap!"); + return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap); case LeaderboardState.RulesetUnavailable: - return new MessagePlaceholder(@"Leaderboards are not available for this ruleset!"); + return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset); case LeaderboardState.BeatmapUnavailable: - return new MessagePlaceholder(@"Leaderboards are not available for this beatmap!"); + return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap); case LeaderboardState.NoScores: - return new MessagePlaceholder(@"No records yet!"); + return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet); case LeaderboardState.NotLoggedIn: - return new LoginPlaceholder(@"Please sign in to view online leaderboards!"); + return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards); case LeaderboardState.NotSupporter: - return new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!"); + return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard); case LeaderboardState.Retrieving: return null; diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs index f8a326a52e..d03b3d8ffc 100644 --- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs +++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Overlays; namespace osu.Game.Online.Placeholders @@ -12,7 +13,7 @@ namespace osu.Game.Online.Placeholders [Resolved(CanBeNull = true)] private LoginOverlay login { get; set; } - public LoginPlaceholder(string actionMessage) + public LoginPlaceholder(LocalisableString actionMessage) : base(actionMessage, FontAwesome.Solid.UserLock) { Action = () => login?.Show(); From c07f7545653edc5efd3f4f90b1faa292774cc9b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:34:24 +0900 Subject: [PATCH 44/68] Enable `nullable` on `BeatmapOffsetControl` --- .../PlayerSettings/BeatmapOffsetControl.cs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index e6bc510564..98820cabf8 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -23,6 +23,8 @@ using osuTK; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; +#nullable enable + namespace osu.Game.Screens.Play.PlayerSettings { public class BeatmapOffsetControl : CompositeDrawable @@ -38,22 +40,24 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.1, }; - private SettingsButton useAverageButton; + private readonly FillFlowContainer referenceScoreContainer; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; private double lastPlayAverage; - private readonly FillFlowContainer referenceScoreContainer; + private SettingsButton? useAverageButton; - private IDisposable beatmapOffsetSubscription; + private IDisposable? beatmapOffsetSubscription; - [Resolved] - private RealmAccess realm { get; set; } - - [Resolved] - private IBindable beatmap { get; set; } - - [Resolved] - private OsuColour colours { get; set; } + private Task? realmWriteTask; public BeatmapOffsetControl() { @@ -113,8 +117,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Current.BindValueChanged(currentChanged); } - private Task realmWrite; - private void currentChanged(ValueChangedEvent offset) { Scheduler.AddOnce(updateOffset); @@ -122,7 +124,7 @@ namespace osu.Game.Screens.Play.PlayerSettings void updateOffset() { // ensure the previous write has completed. ignoring performance concerns, if we don't do this, the async writes could be out of sequence. - if (realmWrite?.IsCompleted == false) + if (realmWriteTask?.IsCompleted == false) { Scheduler.AddOnce(updateOffset); return; @@ -131,7 +133,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (useAverageButton != null) useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, Current.Value, Current.Precision); - realmWrite = realm.WriteAsync(r => + realmWriteTask = realm.WriteAsync(r => { var settings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; From 3cbcb702f6f4aa9d3d165f63735d6c57b2cf829c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:36:15 +0900 Subject: [PATCH 45/68] Fix calibration button disabled state not checking in corrrect direction --- osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs | 1 + osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 42b579bc89..67f5db548b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -66,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error); + AddAssert("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 98820cabf8..ca683cec66 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } if (useAverageButton != null) - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, Current.Value, Current.Precision); + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value, Current.Precision); realmWriteTask = realm.WriteAsync(r => { From 8bd66f1ed7302577801926dc55baeeac4d1720d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:36:49 +0900 Subject: [PATCH 46/68] Fix incorrect precision specification for button disable check --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index ca683cec66..63045013b5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } if (useAverageButton != null) - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value, Current.Precision); + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value, Current.Precision / 2); realmWriteTask = realm.WriteAsync(r => { From e184b26cdd67101488bbbcdf8c03940997a13da3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:39:28 +0900 Subject: [PATCH 47/68] Remove `Precision` call for database write shortcutting Shouldn't be required. --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 63045013b5..f19a326cdf 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (settings == null) // only the case for tests. return; - if (Precision.AlmostEquals(settings.Offset, Current.Value)) + if (settings.Offset == Current.Value) return; settings.Offset = Current.Value; From 763f881d4a0ade9930cfffafb974883c6f6e2385 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:42:08 +0900 Subject: [PATCH 48/68] Use more correct mod check to encompass more than just autoplay --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index f19a326cdf..dc3e80d695 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -15,13 +15,12 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; -using osu.Game.Localisation; -using osu.Game.Rulesets.Mods; #nullable enable @@ -155,7 +154,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; - if (score.NewValue.Mods.Any(m => m is ModAutoplay)) + if (score.NewValue.Mods.Any(m => !m.UserPlayable)) return; var hitEvents = score.NewValue.HitEvents; From ed9ecd695114e1afabd51213767a206798791c92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:45:39 +0900 Subject: [PATCH 49/68] Fix test scene failures by ensuring that first `GameplayClock` frame has processed first --- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index e03c8d7561..b195d2aa74 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -131,9 +131,9 @@ namespace osu.Game.Tests.Visual.Gameplay public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime; - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); + base.UpdateAfterChildren(); if (!FirstFrameClockTime.HasValue) { From 1a474592625ccc97b47b11bdb953dcdbf3fdd8a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 18:38:17 +0900 Subject: [PATCH 50/68] Fix taiko difficulty adjust scroll speed being shown with too low precision --- osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 9540e35780..99a064d35f 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { get { - string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N1}"; + string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}"; return string.Join(", ", new[] { From f15b8781bb92e9d74c89357a10b32e77a0de84b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 19:19:39 +0900 Subject: [PATCH 51/68] Move editor mode selector out of `EditorMenuBar` to allow for better reuse --- .../Editing/TestSceneEditorScreenModes.cs | 4 +- .../Edit/Components/Menus/EditorMenuBar.cs | 22 ----- osu.Game/Screens/Edit/Editor.cs | 82 +++++++++++-------- 3 files changed, 47 insertions(+), 61 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs index 98d8a41674..2efd125f81 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs @@ -4,11 +4,9 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Components.Menus; namespace osu.Game.Tests.Visual.Editing { @@ -22,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("switch between all screens at once", () => { foreach (var screen in Enum.GetValues(typeof(EditorScreenMode)).Cast()) - Editor.ChildrenOfType().Single().Mode.Value = screen; + Editor.Mode.Value = screen; }); } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index c6787a1fb1..2a8435ff47 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components.Menus { public class EditorMenuBar : OsuMenu { - public readonly Bindable Mode = new Bindable(); - public EditorMenuBar() : base(Direction.Horizontal, true) { @@ -28,25 +25,6 @@ namespace osu.Game.Screens.Edit.Components.Menus MaskingContainer.CornerRadius = 0; ItemsContainer.Padding = new MarginPadding { Left = 100 }; BackgroundColour = Color4Extensions.FromHex("111"); - - ScreenSelectionTabControl tabControl; - AddRangeInternal(new Drawable[] - { - tabControl = new ScreenSelectionTabControl - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - X = -15 - } - }); - - Mode.BindTo(tabControl.Current); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Mode.TriggerChange(); } protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c2775ae101..dcb7e3a282 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -89,6 +89,8 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private NotificationOverlay notifications { get; set; } + public readonly Bindable Mode = new Bindable(); + public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; private readonly Bindable samplePlaybackDisabled = new Bindable(); @@ -115,8 +117,6 @@ namespace osu.Game.Screens.Edit [CanBeNull] // Should be non-null once it can support custom rulesets. private EditorChangeHandler changeHandler; - private EditorMenuBar menuBar; - private DependencyContainer dependencies; private TestGameplayButton testGameplayButton; @@ -239,40 +239,49 @@ namespace osu.Game.Screens.Edit Name = "Top bar", RelativeSizeAxes = Axes.X, Height = 40, - Child = menuBar = new EditorMenuBar + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Mode = { Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose }, - Items = new[] + new EditorMenuBar { - new MenuItem("File") + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Items = new[] { - Items = createFileMenuItems() - }, - new MenuItem("Edit") - { - Items = new[] + new MenuItem("File") { - undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo), - new EditorMenuItemSpacer(), - cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), - } - }, - new MenuItem("View") - { - Items = new MenuItem[] + Items = createFileMenuItems() + }, + new MenuItem("Edit") { - new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), - new HitAnimationsMenuItem(config.GetBindable(OsuSetting.EditorHitAnimations)) + Items = new[] + { + undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo), + new EditorMenuItemSpacer(), + cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), + } + }, + new MenuItem("View") + { + Items = new MenuItem[] + { + new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), + new HitAnimationsMenuItem(config.GetBindable(OsuSetting.EditorHitAnimations)) + } } } - } - } + }, + new ScreenSelectionTabControl + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -15, + Current = Mode, + }, + }, }, new Container { @@ -340,14 +349,15 @@ namespace osu.Game.Screens.Edit changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); - - menuBar.Mode.ValueChanged += onModeChanged; } protected override void LoadComplete() { base.LoadComplete(); setUpClipboardActionAvailability(); + + Mode.Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose; + Mode.BindValueChanged(onModeChanged, true); } /// @@ -517,23 +527,23 @@ namespace osu.Game.Screens.Edit return true; case GlobalAction.EditorComposeMode: - menuBar.Mode.Value = EditorScreenMode.Compose; + Mode.Value = EditorScreenMode.Compose; return true; case GlobalAction.EditorDesignMode: - menuBar.Mode.Value = EditorScreenMode.Design; + Mode.Value = EditorScreenMode.Design; return true; case GlobalAction.EditorTimingMode: - menuBar.Mode.Value = EditorScreenMode.Timing; + Mode.Value = EditorScreenMode.Timing; return true; case GlobalAction.EditorSetupMode: - menuBar.Mode.Value = EditorScreenMode.SongSetup; + Mode.Value = EditorScreenMode.SongSetup; return true; case GlobalAction.EditorVerifyMode: - menuBar.Mode.Value = EditorScreenMode.Verify; + Mode.Value = EditorScreenMode.Verify; return true; case GlobalAction.EditorTestGameplay: From 1916011ebff0064370a5589a948caa33fcafde91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 19:41:47 +0900 Subject: [PATCH 52/68] Tween corner radius when scaling container becomes non-fullscreen --- osu.Game/Graphics/Containers/ScalingContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 0d543bdbc8..781e85f82e 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -56,6 +56,8 @@ namespace osu.Game.Graphics.Containers } } + private const float corner_radius = 10; + /// /// Create a new instance. /// @@ -69,7 +71,7 @@ namespace osu.Game.Graphics.Containers { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, - CornerRadius = 10, + CornerRadius = corner_radius, Child = content = new ScalingDrawSizePreservingFillContainer(targetMode != ScalingMode.Gameplay) }; } @@ -176,6 +178,7 @@ namespace osu.Game.Graphics.Containers sizableContainer.MoveTo(targetPosition, 500, Easing.OutQuart); sizableContainer.ResizeTo(targetSize, 500, Easing.OutQuart).OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); + sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, 500, requiresMasking ? Easing.OutQuart : Easing.None); } private class ScalingBackgroundScreen : BackgroundScreenDefault From ff7db4f4059f70a67d91f6c37fb683c6a7effa23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 20:05:37 +0900 Subject: [PATCH 53/68] Replace jank buttons with menu in skin editor --- osu.Game/Skinning/Editor/SkinEditor.cs | 88 ++++++++++++-------------- 1 file changed, 39 insertions(+), 49 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 8052f82c93..ae5cbc95f0 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -8,14 +8,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; -using osu.Game.Resources.Localisation.Web; -using osuTK; +using osu.Game.Screens.Edit.Components.Menus; namespace osu.Game.Skinning.Editor { @@ -57,13 +57,43 @@ namespace osu.Game.Skinning.Editor RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - headerText = new OsuTextFlowContainer + new Container { - TextAnchor = Anchor.TopCentre, - Padding = new MarginPadding(20), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X + Name = "Top bar", + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, + Height = 40, + Children = new Drawable[] + { + new EditorMenuBar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Items = new[] + { + new MenuItem("File") + { + Items = new[] + { + new EditorMenuItem("Save", MenuItemType.Standard, Save), + new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert), + new EditorMenuItemSpacer(), + new EditorMenuItem("Exit", MenuItemType.Standard, Hide), + }, + }, + } + }, + headerText = new OsuTextFlowContainer + { + TextAnchor = Anchor.TopRight, + Padding = new MarginPadding(5), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + }, + }, }, new GridContainer { @@ -89,46 +119,6 @@ namespace osu.Game.Skinning.Editor Children = new Drawable[] { new SkinBlueprintContainer(targetScreen), - new TriangleButton - { - Margin = new MarginPadding(10), - Text = CommonStrings.ButtonsClose, - Width = 100, - Action = Hide, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Spacing = new Vector2(5), - Padding = new MarginPadding - { - Top = 10, - Left = 10, - }, - Margin = new MarginPadding - { - Right = 10, - Bottom = 10, - }, - Children = new Drawable[] - { - new TriangleButton - { - Text = "Save Changes", - Width = 140, - Action = Save, - }, - new DangerousTriangleButton - { - Text = "Revert to default", - Width = 140, - Action = revert, - }, - } - }, } }, } @@ -161,7 +151,7 @@ namespace osu.Game.Skinning.Editor { headerText.Clear(); - headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 24)); + headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 16)); headerText.NewParagraph(); headerText.AddText("Currently editing ", cp => { From 29ed419d537f140943e77b23a4a965a757b7ca1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 20:04:53 +0900 Subject: [PATCH 54/68] Change how custom scales are applied to `ScalingContainer` to allow for better transitions --- .../Graphics/Containers/ScalingContainer.cs | 45 ++++++++++++------- .../Skinning/Editor/SkinComponentToolbox.cs | 4 +- osu.Game/Skinning/Editor/SkinEditorOverlay.cs | 21 ++------- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 781e85f82e..5888be2ae7 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Screens; using osu.Game.Configuration; using osu.Game.Screens; @@ -38,22 +39,18 @@ namespace osu.Game.Graphics.Containers private BackgroundScreenStack backgroundStack; - private bool allowScaling = true; + private RectangleF? customScale; + private bool customScaleIsRelativePosition; /// - /// Whether user scaling preferences should be applied. Enabled by default. + /// Set a custom position and scale which overrides any user specification. /// - public bool AllowScaling + public void SetCustomScale(RectangleF? scale, bool relativePosition = false) { - get => allowScaling; - set - { - if (value == allowScaling) - return; + customScale = scale; + customScaleIsRelativePosition = relativePosition; - allowScaling = value; - if (IsLoaded) Scheduler.AddOnce(updateSize); - } + if (IsLoaded) Scheduler.AddOnce(updateSize); } private const float corner_radius = 10; @@ -164,11 +161,25 @@ namespace osu.Game.Graphics.Containers backgroundStack?.FadeOut(fade_time); } - bool scaling = AllowScaling && (targetMode == null || scalingMode.Value == targetMode); + RectangleF targetSize = new RectangleF(Vector2.Zero, Vector2.One); - var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; - var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero; - bool requiresMasking = (scaling && targetSize != Vector2.One) + if (customScale != null) + { + sizableContainer.RelativePositionAxes = customScaleIsRelativePosition ? Axes.Both : Axes.None; + + targetSize = customScale.Value; + } + else if (targetMode == null || scalingMode.Value == targetMode) + { + sizableContainer.RelativePositionAxes = Axes.Both; + + Vector2 scale = new Vector2(sizeX.Value, sizeY.Value); + Vector2 pos = new Vector2(posX.Value, posY.Value) * (Vector2.One - scale); + + targetSize = new RectangleF(pos, scale); + } + + bool requiresMasking = targetSize.Size != Vector2.One // For the top level scaling container, for now we apply masking if safe areas are in use. // In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas. || (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero); @@ -176,8 +187,8 @@ namespace osu.Game.Graphics.Containers if (requiresMasking) sizableContainer.Masking = true; - sizableContainer.MoveTo(targetPosition, 500, Easing.OutQuart); - sizableContainer.ResizeTo(targetSize, 500, Easing.OutQuart).OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); + sizableContainer.MoveTo(targetSize.Location, 500, Easing.OutQuart); + sizableContainer.ResizeTo(targetSize.Size, 500, Easing.OutQuart).OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, 500, requiresMasking ? Easing.OutQuart : Easing.None); } diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs index 935d2756fb..ce9afd650a 100644 --- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs +++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs @@ -23,6 +23,8 @@ namespace osu.Game.Skinning.Editor { public class SkinComponentToolbox : ScrollingToolboxGroup { + public const float WIDTH = 200; + public Action RequestPlacement; private const float component_display_scale = 0.8f; @@ -41,7 +43,7 @@ namespace osu.Game.Skinning.Editor : base("Components", height) { RelativeSizeAxes = Axes.None; - Width = 200; + Width = WIDTH; } [BackgroundDependencyLoader] diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index 86854ab6ff..dcfe28aaea 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -100,30 +101,14 @@ namespace osu.Game.Skinning.Editor { if (visibility.NewValue == Visibility.Visible) { - updateMasking(); - target.AllowScaling = false; - target.RelativePositionAxes = Axes.Both; - - target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); - target.MoveToX(0.095f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); + target.SetCustomScale(new RectangleF(0.18f, 0.1f, VISIBLE_TARGET_SCALE, VISIBLE_TARGET_SCALE), true); } else { - target.AllowScaling = true; - - target.ScaleTo(1, SkinEditor.TRANSITION_DURATION, Easing.OutQuint).OnComplete(_ => updateMasking()); - target.MoveToX(0f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); + target.SetCustomScale(null); } } - private void updateMasking() - { - if (skinEditor == null) - return; - - target.Masking = skinEditor.State.Value == Visibility.Visible; - } - public void OnReleased(KeyBindingReleaseEvent e) { } From 8d7cdbd8833ba4db7990bd5d5fcbf808d5c65d21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 20:25:34 +0900 Subject: [PATCH 55/68] Add note about nested masking case --- .../Graphics/Containers/ScalingContainer.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 5888be2ae7..df27c561d5 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -136,7 +136,7 @@ namespace osu.Game.Graphics.Containers private void updateSize() { - const float fade_time = 500; + const float duration = 500; if (targetMode == ScalingMode.Everything) { @@ -155,10 +155,10 @@ namespace osu.Game.Graphics.Containers backgroundStack.Push(new ScalingBackgroundScreen()); } - backgroundStack.FadeIn(fade_time); + backgroundStack.FadeIn(duration); } else - backgroundStack?.FadeOut(fade_time); + backgroundStack?.FadeOut(duration); } RectangleF targetSize = new RectangleF(Vector2.Zero, Vector2.One); @@ -187,9 +187,14 @@ namespace osu.Game.Graphics.Containers if (requiresMasking) sizableContainer.Masking = true; - sizableContainer.MoveTo(targetSize.Location, 500, Easing.OutQuart); - sizableContainer.ResizeTo(targetSize.Size, 500, Easing.OutQuart).OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); - sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, 500, requiresMasking ? Easing.OutQuart : Easing.None); + sizableContainer.MoveTo(targetSize.Location, duration, Easing.OutQuart); + sizableContainer.ResizeTo(targetSize.Size, duration, Easing.OutQuart); + + // Of note, this will not working great in the case of nested ScalingContainers where multiple are applying corner radius. + // There should likely only be masking and corner radius applied at one point in the full game stack to fix this. + // An example of how this can occur is it the skin editor is visible and the game screen scaling is set to "Everything". + sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None) + .OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); } private class ScalingBackgroundScreen : BackgroundScreenDefault From b5684aaa76d8b345397eb1fff8c60dd2c25fcd60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 20:33:28 +0900 Subject: [PATCH 56/68] Scale -> Rect to read better --- .../Graphics/Containers/ScalingContainer.cs | 26 +++++++++---------- osu.Game/Skinning/Editor/SkinEditorOverlay.cs | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index df27c561d5..dd611b0904 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -39,16 +39,16 @@ namespace osu.Game.Graphics.Containers private BackgroundScreenStack backgroundStack; - private RectangleF? customScale; - private bool customScaleIsRelativePosition; + private RectangleF? customRect; + private bool customRectIsRelativePosition; /// /// Set a custom position and scale which overrides any user specification. /// - public void SetCustomScale(RectangleF? scale, bool relativePosition = false) + public void SetCustomRect(RectangleF? rect, bool relativePosition = false) { - customScale = scale; - customScaleIsRelativePosition = relativePosition; + customRect = rect; + customRectIsRelativePosition = relativePosition; if (IsLoaded) Scheduler.AddOnce(updateSize); } @@ -161,13 +161,13 @@ namespace osu.Game.Graphics.Containers backgroundStack?.FadeOut(duration); } - RectangleF targetSize = new RectangleF(Vector2.Zero, Vector2.One); + RectangleF targetRect = new RectangleF(Vector2.Zero, Vector2.One); - if (customScale != null) + if (customRect != null) { - sizableContainer.RelativePositionAxes = customScaleIsRelativePosition ? Axes.Both : Axes.None; + sizableContainer.RelativePositionAxes = customRectIsRelativePosition ? Axes.Both : Axes.None; - targetSize = customScale.Value; + targetRect = customRect.Value; } else if (targetMode == null || scalingMode.Value == targetMode) { @@ -176,10 +176,10 @@ namespace osu.Game.Graphics.Containers Vector2 scale = new Vector2(sizeX.Value, sizeY.Value); Vector2 pos = new Vector2(posX.Value, posY.Value) * (Vector2.One - scale); - targetSize = new RectangleF(pos, scale); + targetRect = new RectangleF(pos, scale); } - bool requiresMasking = targetSize.Size != Vector2.One + bool requiresMasking = targetRect.Size != Vector2.One // For the top level scaling container, for now we apply masking if safe areas are in use. // In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas. || (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero); @@ -187,8 +187,8 @@ namespace osu.Game.Graphics.Containers if (requiresMasking) sizableContainer.Masking = true; - sizableContainer.MoveTo(targetSize.Location, duration, Easing.OutQuart); - sizableContainer.ResizeTo(targetSize.Size, duration, Easing.OutQuart); + sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart); + sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart); // Of note, this will not working great in the case of nested ScalingContainers where multiple are applying corner radius. // There should likely only be masking and corner radius applied at one point in the full game stack to fix this. diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index dcfe28aaea..61c363b019 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -101,11 +101,11 @@ namespace osu.Game.Skinning.Editor { if (visibility.NewValue == Visibility.Visible) { - target.SetCustomScale(new RectangleF(0.18f, 0.1f, VISIBLE_TARGET_SCALE, VISIBLE_TARGET_SCALE), true); + target.SetCustomRect(new RectangleF(0.18f, 0.1f, VISIBLE_TARGET_SCALE, VISIBLE_TARGET_SCALE), true); } else { - target.SetCustomScale(null); + target.SetCustomRect(null); } } From fab9323707655e3ce08145290bfeb84f886e0f8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 14:08:48 +0900 Subject: [PATCH 57/68] Replace all legacy ruleset checks with a helper property call --- osu.Desktop/DiscordRichPresence.cs | 5 +---- osu.Game/Beatmaps/DifficultyRecommender.cs | 2 +- osu.Game/Rulesets/IRulesetInfo.cs | 5 +++++ osu.Game/Rulesets/RulesetInfo.cs | 5 +++++ osu.Game/Screens/Play/SoloPlayer.cs | 3 +-- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 3642f70a56..fe687e8dab 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -108,10 +108,7 @@ namespace osu.Desktop presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); // update ruleset - int onlineID = ruleset.Value.OnlineID; - bool isLegacyRuleset = onlineID >= 0 && onlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID; - - presence.Assets.SmallImageKey = isLegacyRuleset ? $"mode_{onlineID}" : "mode_custom"; + presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; presence.Assets.SmallImageText = ruleset.Value.Name; client.SetPresence(presence); diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 3949e84f4a..8c3e832293 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -83,7 +83,7 @@ namespace osu.Game.Beatmaps requestedUserId = api.LocalUser.Value.Id; // only query API for built-in rulesets - rulesets.AvailableRulesets.Where(ruleset => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID).ForEach(rulesetInfo => + rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset).ForEach(rulesetInfo => { var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 60a02212fc..a8ed1683c2 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -29,5 +29,10 @@ namespace osu.Game.Rulesets string InstantiationInfo { get; } Ruleset CreateInstance(); + + /// + /// Whether this ruleset's online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania). + /// + public bool IsLegacyRuleset => OnlineID >= 0 && OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID; } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 88e3988431..cf7d84c2b4 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -91,6 +91,11 @@ namespace osu.Game.Rulesets Available = Available }; + /// + /// Whether this ruleset's online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania). + /// + public bool IsLegacyRuleset => ((IRulesetInfo)this).IsLegacyRuleset; + public Ruleset CreateInstance() { if (!Available) diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index 824c0072e3..b877ee1c11 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -7,7 +7,6 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; -using osu.Game.Rulesets; using osu.Game.Scoring; namespace osu.Game.Screens.Play @@ -32,7 +31,7 @@ namespace osu.Game.Screens.Play if (beatmapId <= 0) return null; - if (rulesetId < 0 || rulesetId > ILegacyRuleset.MAX_LEGACY_RULESET_ID) + if (!Ruleset.Value.IsLegacyRuleset) return null; return new CreateSoloScoreRequest(beatmapId, rulesetId, Game.VersionHash); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 6daaae9d04..95910ed0aa 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (fetchRuleset.OnlineID <= 0 || fetchRuleset.OnlineID > ILegacyRuleset.MAX_LEGACY_RULESET_ID) + if (!fetchRuleset.IsLegacyRuleset) { SetErrorState(LeaderboardState.RulesetUnavailable); return null; From 42e07b7308c05bd0792ceee0ae691a54f74f8e0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 14:15:25 +0900 Subject: [PATCH 58/68] Convert to extension method to avoid recursive calls --- osu.Desktop/DiscordRichPresence.cs | 3 ++- osu.Game/Beatmaps/DifficultyRecommender.cs | 3 ++- osu.Game/Extensions/ModelExtensions.cs | 5 +++++ osu.Game/Rulesets/IRulesetInfo.cs | 5 ----- osu.Game/Rulesets/RulesetInfo.cs | 5 ----- osu.Game/Screens/Play/SoloPlayer.cs | 3 ++- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 3 ++- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index fe687e8dab..d87b25a4c7 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; @@ -108,7 +109,7 @@ namespace osu.Desktop presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); // update ruleset - presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; + presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; presence.Assets.SmallImageText = ruleset.Value.Name; client.SetPresence(presence); diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 8c3e832293..93c2fccbc7 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; @@ -83,7 +84,7 @@ namespace osu.Game.Beatmaps requestedUserId = api.LocalUser.Value.Id; // only query API for built-in rulesets - rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset).ForEach(rulesetInfo => + rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo => { var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index f178a5c97b..13c25e45c8 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -72,6 +72,11 @@ namespace osu.Game.Extensions return result; } + /// + /// Check whether this 's online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania). + /// + public static bool IsLegacyRuleset(this IRulesetInfo ruleset) => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID; + /// /// Check whether the online ID of two s match. /// diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index a8ed1683c2..60a02212fc 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -29,10 +29,5 @@ namespace osu.Game.Rulesets string InstantiationInfo { get; } Ruleset CreateInstance(); - - /// - /// Whether this ruleset's online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania). - /// - public bool IsLegacyRuleset => OnlineID >= 0 && OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID; } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index cf7d84c2b4..88e3988431 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -91,11 +91,6 @@ namespace osu.Game.Rulesets Available = Available }; - /// - /// Whether this ruleset's online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania). - /// - public bool IsLegacyRuleset => ((IRulesetInfo)this).IsLegacyRuleset; - public Ruleset CreateInstance() { if (!Available) diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index b877ee1c11..a935ce49eb 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; @@ -31,7 +32,7 @@ namespace osu.Game.Screens.Play if (beatmapId <= 0) return null; - if (!Ruleset.Value.IsLegacyRuleset) + if (!Ruleset.Value.IsLegacyRuleset()) return null; return new CreateSoloScoreRequest(beatmapId, rulesetId, Game.VersionHash); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 95910ed0aa..eb0addd377 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Leaderboards; @@ -118,7 +119,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (!fetchRuleset.IsLegacyRuleset) + if (!fetchRuleset.IsLegacyRuleset()) { SetErrorState(LeaderboardState.RulesetUnavailable); return null; From 29bf7d0bde958ca2d7e30515ae4086a5bfddd2ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 14:35:52 +0900 Subject: [PATCH 59/68] Fix shocking grammar and typos in block comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Graphics/Containers/ScalingContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index dd611b0904..248bb8ca1f 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -190,9 +190,9 @@ namespace osu.Game.Graphics.Containers sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart); sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart); - // Of note, this will not working great in the case of nested ScalingContainers where multiple are applying corner radius. - // There should likely only be masking and corner radius applied at one point in the full game stack to fix this. - // An example of how this can occur is it the skin editor is visible and the game screen scaling is set to "Everything". + // Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius. + // Masking and corner radius should likely only be applied at one point in the full game stack to fix this. + // An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything". sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None) .OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); } From cb0d643f7047d0f594a4baf76f7751cb895166ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 14:38:20 +0900 Subject: [PATCH 60/68] Add parameter xmldoc to explain what a null rect does --- osu.Game/Graphics/Containers/ScalingContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 248bb8ca1f..d331b818a1 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -45,6 +45,8 @@ namespace osu.Game.Graphics.Containers /// /// Set a custom position and scale which overrides any user specification. /// + /// A rectangle with positional and sizing information for this container to conform to. null will clear the custom rect and revert to user settings. + /// Whether the position portion of the provided rect is in relative coordinate space or not. public void SetCustomRect(RectangleF? rect, bool relativePosition = false) { customRect = rect; From ab0ee265408ad406cdbd71de743c9ab1ef9e6d99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 15:13:42 +0900 Subject: [PATCH 61/68] Remove padding from distribution graph bars to fix some bars becoming invisible at low sizes --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 93885b6e02..372c0d4849 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -160,8 +160,6 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding { Horizontal = 1 }; - InternalChild = new Circle { RelativeSizeAxes = Axes.Both, From 464be6e64c52b129e36c0e412b00d5f29a4205a1 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 14:37:39 +0800 Subject: [PATCH 62/68] Only call `IUpdatableByPlayfield.Update` if the playfield isn't nested --- osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs | 11 +++++++++++ osu.Game/Rulesets/UI/Playfield.cs | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs index 9baa252caf..7cf480a11b 100644 --- a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs +++ b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs @@ -5,8 +5,19 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { + /// + /// An interface for s that are updated every frame by a . + /// public interface IUpdatableByPlayfield : IApplicableMod { + /// + /// Update this . + /// + /// The main + /// + /// This method is called once per frame during gameplay by the main only. + /// To access nested s, use . + /// void Update(Playfield playfield); } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d0bbf859af..30e71dde1c 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -79,6 +79,11 @@ namespace osu.Game.Rulesets.UI private readonly List nestedPlayfields = new List(); + /// + /// Whether this is nested in another . + /// + public bool IsNested { get; private set; } + /// /// Whether judgements should be displayed by this and and all nested s. /// @@ -206,6 +211,8 @@ namespace osu.Game.Rulesets.UI /// The to add. protected void AddNested(Playfield otherPlayfield) { + otherPlayfield.IsNested = true; + otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); @@ -229,7 +236,7 @@ namespace osu.Game.Rulesets.UI { base.Update(); - if (mods != null) + if (!IsNested && mods != null) { foreach (var mod in mods) { From 9c43500ad358cc50ea399af79d47fa4f7f6ace5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 16:23:30 +0900 Subject: [PATCH 63/68] Add ability for player loading screen settings to scroll As we add more items here this is going to become necessary. Until the design no doubt gets changed. --- osu.Game/Overlays/SettingsToolboxGroup.cs | 7 +++--- osu.Game/Screens/Play/PlayerLoader.cs | 29 +++++++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index 08321f68fe..b4178359a4 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -22,8 +22,9 @@ namespace osu.Game.Overlays { public class SettingsToolboxGroup : Container, IExpandable { + public const int CONTAINER_WIDTH = 270; + private const float transition_duration = 250; - private const int container_width = 270; private const int border_thickness = 2; private const int header_height = 30; private const int corner_radius = 5; @@ -49,7 +50,7 @@ namespace osu.Game.Overlays public SettingsToolboxGroup(string title) { AutoSizeAxes = Axes.Y; - Width = container_width; + Width = CONTAINER_WIDTH; Masking = true; CornerRadius = corner_radius; BorderColour = Color4.Black; @@ -201,7 +202,5 @@ namespace osu.Game.Overlays } protected override Container Content => content; - - protected override bool OnMouseDown(MouseDownEvent e) => true; } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index f6d63a8ec5..41eb822e39 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -143,6 +143,8 @@ namespace osu.Game.Screens.Play muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); + const float padding = 25; + InternalChildren = new Drawable[] { (content = new LogoTrackingContainer @@ -158,20 +160,27 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - PlayerSettings = new FillFlowContainer + new OsuScrollContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding(25), - Children = new PlayerSettingsGroup[] + RelativeSizeAxes = Axes.Y, + Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2, + Padding = new MarginPadding { Vertical = padding }, + Masking = false, + Child = PlayerSettings = new FillFlowContainer { - VisualSettings = new VisualSettings(), - AudioSettings = new AudioSettings(), - new InputSettings() - } + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Padding = new MarginPadding { Horizontal = padding }, + Children = new PlayerSettingsGroup[] + { + VisualSettings = new VisualSettings(), + AudioSettings = new AudioSettings(), + new InputSettings() + } + }, }, idleTracker = new IdleTracker(750), }), From f09a4e9c5b0e5507d9d3a22b92682a2190030832 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 16:28:32 +0900 Subject: [PATCH 64/68] Fix potential crash in tests when attempting to lookup key bindings in cases the lookup is not available --- osu.Game/Configuration/DevelopmentOsuConfigManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs index ff19dd874c..f1cb0731fe 100644 --- a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs +++ b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs @@ -14,6 +14,8 @@ namespace osu.Game.Configuration public DevelopmentOsuConfigManager(Storage storage) : base(storage) { + LookupKeyBindings = _ => "unknown"; + LookupSkinName = _ => "unknown"; } } } From 36263b4dbf09bcbcaed7559be689314d40434e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Mar 2022 23:09:56 +0100 Subject: [PATCH 65/68] Replace remaining manual online ID check with extension method --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 1326395695..f0ead05280 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -7,9 +7,9 @@ using System.Linq; using System.Text; using osu.Framework.Extensions; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.IO.Legacy; using osu.Game.Replays.Legacy; -using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -48,7 +48,7 @@ namespace osu.Game.Scoring.Legacy if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame)) throw new ArgumentException(@"Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); - if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > ILegacyRuleset.MAX_LEGACY_RULESET_ID) + if (!score.ScoreInfo.Ruleset.IsLegacyRuleset()) throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } From 17729f060582ed948ec6ba32a186549dc651f6c7 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 3 Mar 2022 14:53:49 -0800 Subject: [PATCH 66/68] Reword ide section of readme to always use latest version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ace47a74f..67e28dad97 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Please make sure you have the following prerequisites: - A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed. - When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). -- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). +- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). - When running on Linux, please have a system-wide FFmpeg installation available to support video decoding. ### Downloading the source code From 53f23a429bcf56f426513a5625b98937193bd380 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 3 Mar 2022 15:01:21 -0800 Subject: [PATCH 67/68] Fix full stop being inside code backticks --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67e28dad97..f64240f67a 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ git pull Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). -- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations. +- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations. You can also build and run *osu!* from the command-line with a single command: From ac914878b80a5efef642d1158954258bcd4af42d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Mar 2022 12:31:57 +0900 Subject: [PATCH 68/68] Move default function specifications to `OsuConfigManager` This ensures that running tests in release configuration will not fail due to the same issue being fixed in this PR. --- osu.Game/Configuration/DevelopmentOsuConfigManager.cs | 2 -- osu.Game/Configuration/OsuConfigManager.cs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs index f1cb0731fe..ff19dd874c 100644 --- a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs +++ b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs @@ -14,8 +14,6 @@ namespace osu.Game.Configuration public DevelopmentOsuConfigManager(Storage storage) : base(storage) { - LookupKeyBindings = _ => "unknown"; - LookupSkinName = _ => "unknown"; } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 07d2026c65..1358b41ad2 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -240,9 +240,9 @@ namespace osu.Game.Configuration }; } - public Func LookupSkinName { private get; set; } + public Func LookupSkinName { private get; set; } = _ => @"unknown"; - public Func LookupKeyBindings { get; set; } + public Func LookupKeyBindings { get; set; } = _ => @"unknown"; } // IMPORTANT: These are used in user configuration files.