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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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 4117a6adf757afa89277246396d2c5e6f59b7cfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 14:13:27 +0900 Subject: [PATCH 19/76] 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 20/76] 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 21/76] 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 22/76] 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 23/76] 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 24/76] 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 25/76] 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 26/76] 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 27/76] 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 28/76] 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 29/76] 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 30/76] 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 31/76] 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 32/76] 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 33/76] 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 34/76] 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 35/76] 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 36/76] 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 37/76] 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 c342030b2c084790724021aca3b0b3557c54fb76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:10:59 +0900 Subject: [PATCH 38/76] 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 39/76] 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 40/76] 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 41/76] 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 42/76] 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 43/76] 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 44/76] 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 45/76] 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 46/76] 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 47/76] 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 48/76] 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 49/76] 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 50/76] 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 51/76] 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 52/76] 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 53/76] 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 54/76] 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 55/76] 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 56/76] 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 57/76] 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 58/76] 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 a06d806fb911ddb1531eda8f5415c0e9a86dc7ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 15:43:01 +0900 Subject: [PATCH 59/76] Fix hit distribution graph midpoint rounding not looking great around zero Not sure this will be accepted and it's likely only ever going to show in tests, but seems to be a better approach to midpoint rounding for this case? --- .../TestSceneHitEventTimingDistributionGraph.cs | 6 ++++++ .../HitEventTimingDistributionGraph.cs | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 221001e40b..f31aec8975 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -23,6 +23,12 @@ namespace osu.Game.Tests.Visual.Ranking createTest(CreateDistributedHitEvents()); } + [Test] + public void TestManyDistributedEventsOffset() + { + createTest(CreateDistributedHitEvents(-3.5)); + } + [Test] public void TestAroundCentre() { diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 93885b6e02..3a4641cba9 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -64,10 +64,22 @@ namespace osu.Game.Screens.Ranking.Statistics // Prevent div-by-0 by enforcing a minimum bin size binSize = Math.Max(1, binSize); + bool roundUp = true; + foreach (var e in hitEvents) { - int binOffset = (int)Math.Round(e.TimeOffset / binSize, MidpointRounding.AwayFromZero); - bins[timing_distribution_centre_bin_index + binOffset]++; + double binOffset = e.TimeOffset / binSize; + + // .NET's round midpoint handling doesn't provide a behaviour that works amazingly for display + // purposes here. We want midpoint rounding to roughly distribute evenly to each adjacent bucket + // so the easiest way is to cycle between downwards and upwards rounding as we process events. + if (Math.Abs(binOffset - (int)binOffset) == 0.5) + { + binOffset += Math.Sign(binOffset) * (roundUp ? 1 : 0); + roundUp = !roundUp; + } + + bins[timing_distribution_centre_bin_index + (int)binOffset]++; } int maxCount = bins.Max(); From 9c43500ad358cc50ea399af79d47fa4f7f6ace5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 16:23:30 +0900 Subject: [PATCH 60/76] 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 61/76] 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 62/76] 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 63/76] 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 64/76] 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 3a37e6e8b181389426cdcff800f35817ab842a00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Mar 2022 12:00:02 +0900 Subject: [PATCH 65/76] Fix profile badges potentially showing on incorrect profile when switching users Closes https://github.com/ppy/osu/issues/17081. --- osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs index 5f513582e5..6333802549 100644 --- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.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.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -63,11 +64,15 @@ namespace osu.Game.Overlays.Profile.Header }; } + private CancellationTokenSource cancellationTokenSource; + private void updateDisplay(APIUser user) { var badges = user.Badges; badgeFlowContainer.Clear(); + cancellationTokenSource?.Cancel(); + if (badges?.Length > 0) { Show(); @@ -79,7 +84,7 @@ namespace osu.Game.Overlays.Profile.Header { // load in stable order regardless of async load order. badgeFlowContainer.Insert(displayIndex, asyncBadge); - }); + }, (cancellationTokenSource = new CancellationTokenSource()).Token); } } else From ac914878b80a5efef642d1158954258bcd4af42d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Mar 2022 12:31:57 +0900 Subject: [PATCH 66/76] 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. From 8b504bb5ac2944efffcd3a7e0a378a26db83abda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Mar 2022 12:42:07 +0900 Subject: [PATCH 67/76] Ensure rounding is still applied in non-midpoint cases --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 3a4641cba9..235eac7f78 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -75,11 +75,11 @@ namespace osu.Game.Screens.Ranking.Statistics // so the easiest way is to cycle between downwards and upwards rounding as we process events. if (Math.Abs(binOffset - (int)binOffset) == 0.5) { - binOffset += Math.Sign(binOffset) * (roundUp ? 1 : 0); + binOffset = (int)binOffset + Math.Sign(binOffset) * (roundUp ? 1 : 0); roundUp = !roundUp; } - bins[timing_distribution_centre_bin_index + (int)binOffset]++; + bins[timing_distribution_centre_bin_index + (int)Math.Round(binOffset, MidpointRounding.AwayFromZero)]++; } int maxCount = bins.Max(); From 81a49057ec7e1fa089d40b103f55fbc2ae5c7f8f Mon Sep 17 00:00:00 2001 From: Riley Quinn Date: Thu, 3 Mar 2022 22:30:08 -0600 Subject: [PATCH 68/76] Fix wiki links containing locale not loading when opened from chat. --- osu.Game/Overlays/WikiOverlay.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 44713d637d..63191900d8 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -7,6 +7,7 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -100,7 +101,16 @@ namespace osu.Game.Overlays cancellationToken?.Cancel(); request?.Cancel(); - request = new GetWikiRequest(e.NewValue); + string[] values = e.NewValue.Split('/', 2); + + if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out _)) + { + request = new GetWikiRequest(values[1], values[0]); + } + else + { + request = new GetWikiRequest(e.NewValue); + } Loading.Show(); From e9e92b991e0de0921e0b040be63813c951911dae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Mar 2022 12:55:35 +0900 Subject: [PATCH 69/76] Fix calibrating offset from previous non-zero offset not applying adjustment correctly --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 30 ++++++++++++++++++- .../PlayerSettings/BeatmapOffsetControl.cs | 6 ++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 67f5db548b..4b079cbb2c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestDisplay() + public void TestCalibrationFromZero() { const double average_error = -4.5; @@ -70,5 +70,33 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } + + /// + /// When a beatmap offset was already set, the calibration should take it into account. + /// + [Test] + public void TestCalibrationFromNonZero() + { + const double average_error = -4.5; + const double initial_offset = -2; + + AddStep("Set offset non-neutral", () => offsetControl.Current.Value = initial_offset); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + AddStep("Set reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + 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 == initial_offset - 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 dc3e80d695..fc5c50f32f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -51,6 +51,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private OsuColour colours { get; set; } = null!; private double lastPlayAverage; + private double lastPlayBeatmapOffset; private SettingsButton? useAverageButton; @@ -130,7 +131,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } if (useAverageButton != null) - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value, Current.Precision / 2); + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value + lastPlayBeatmapOffset, Current.Precision / 2); realmWriteTask = realm.WriteAsync(r => { @@ -187,6 +188,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } lastPlayAverage = average; + lastPlayBeatmapOffset = Current.Value; referenceScoreContainer.AddRange(new Drawable[] { @@ -199,7 +201,7 @@ namespace osu.Game.Screens.Play.PlayerSettings useAverageButton = new SettingsButton { Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, - Action = () => Current.Value = -lastPlayAverage + Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage }, }); } From 8491bab77c47391f0f7ee386787befcb4d5dafbf Mon Sep 17 00:00:00 2001 From: Riley Quinn Date: Fri, 4 Mar 2022 00:57:13 -0600 Subject: [PATCH 70/76] Replace string locale with Language --- osu.Game/Online/API/Requests/GetWikiRequest.cs | 10 ++++++---- osu.Game/Overlays/WikiOverlay.cs | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetWikiRequest.cs b/osu.Game/Online/API/Requests/GetWikiRequest.cs index 248fcc03e3..09571ab0a8 100644 --- a/osu.Game/Online/API/Requests/GetWikiRequest.cs +++ b/osu.Game/Online/API/Requests/GetWikiRequest.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Extensions; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests @@ -8,14 +10,14 @@ namespace osu.Game.Online.API.Requests public class GetWikiRequest : APIRequest { private readonly string path; - private readonly string locale; + private readonly Language language; - public GetWikiRequest(string path, string locale = "en") + public GetWikiRequest(string path, Language language = Language.en) { this.path = path; - this.locale = locale; + this.language = language; } - protected override string Target => $"wiki/{locale}/{path}"; + protected override string Target => $"wiki/{language.ToCultureCode()}/{path}"; } } diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 63191900d8..d6a379dc56 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -103,9 +103,9 @@ namespace osu.Game.Overlays string[] values = e.NewValue.Split('/', 2); - if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out _)) + if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out var language)) { - request = new GetWikiRequest(values[1], values[0]); + request = new GetWikiRequest(values[1], language); } else { From 76c293b9e982650e6b9b6edb576b37e427b95089 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Mar 2022 10:00:56 +0300 Subject: [PATCH 71/76] Fix cancellation token source recreated on every medal --- osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs index 6333802549..9341ad63fc 100644 --- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs @@ -68,10 +68,12 @@ namespace osu.Game.Overlays.Profile.Header private void updateDisplay(APIUser user) { - var badges = user.Badges; + cancellationTokenSource?.Cancel(); + cancellationTokenSource = new CancellationTokenSource(); + badgeFlowContainer.Clear(); - cancellationTokenSource?.Cancel(); + var badges = user.Badges; if (badges?.Length > 0) { @@ -84,7 +86,7 @@ namespace osu.Game.Overlays.Profile.Header { // load in stable order regardless of async load order. badgeFlowContainer.Insert(displayIndex, asyncBadge); - }, (cancellationTokenSource = new CancellationTokenSource()).Token); + }, cancellationTokenSource.Token); } } else From 129c290ca07d3d23608615d84b0e80f3337ca96e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Mar 2022 10:01:07 +0300 Subject: [PATCH 72/76] Dispose cancellation token source on disposal --- osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs index 9341ad63fc..922f3832e4 100644 --- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs @@ -94,5 +94,11 @@ namespace osu.Game.Overlays.Profile.Header Hide(); } } + + protected override void Dispose(bool isDisposing) + { + cancellationTokenSource?.Cancel(); + base.Dispose(isDisposing); + } } } From 3fdc7ed9d2b80533cda71a9e4829706eab1ded51 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Mar 2022 10:14:19 +0300 Subject: [PATCH 73/76] Remove brackets surrounding one-line statements --- osu.Game/Overlays/WikiOverlay.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index d6a379dc56..4015d8e196 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -104,13 +104,9 @@ namespace osu.Game.Overlays string[] values = e.NewValue.Split('/', 2); if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out var language)) - { request = new GetWikiRequest(values[1], language); - } else - { request = new GetWikiRequest(e.NewValue); - } Loading.Show(); From 1c40fcb79e65b31c46dbd7fb49cbde6ce2e389b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Mar 2022 17:54:04 +0900 Subject: [PATCH 74/76] Reorder math to be easier to pass Co-authored-by: Salman Ahmed --- 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 fc5c50f32f..8253c2e38e 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 + lastPlayBeatmapOffset, Current.Precision / 2); + useAverageButton.Enabled.Value = !Precision.AlmostEquals(Current.Value, lastPlayBeatmapOffset - lastPlayAverage, Current.Precision / 2); realmWriteTask = realm.WriteAsync(r => { From 1e246bf5608676b0ca7f2c95eae5881421bc81f6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 4 Mar 2022 20:14:14 +0900 Subject: [PATCH 75/76] Reduce 'cursor-tap' sample playback volume on MouseUp --- osu.Game/Graphics/Cursor/MenuCursor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 0cc751ea21..03fad00e41 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -140,6 +140,7 @@ namespace osu.Game.Graphics.Cursor // Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird) channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75; channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range); + channel.Volume.Value = baseFrequency; channel.Play(); } From c132fc19e75752191dda416d38474f2bac0a94dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Mar 2022 22:59:33 +0900 Subject: [PATCH 76/76] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 526ce959a6..d418dcaccf 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7dfd099df1..64785ab566 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 80600655aa..a7bffd28b5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - +