From b1e0cf8532da3f2ed8a46da281ae350cb75366c4 Mon Sep 17 00:00:00 2001 From: LukynkaCZE Date: Sat, 8 Mar 2025 21:07:51 +0100 Subject: [PATCH 001/267] add ArgonJudgementCounterDisplay --- .../Play/HUD/ArgonCounterTextComponent.cs | 12 +- .../Components/ArgonJudgmentCounter.cs | 68 ++++++++ .../Components/ArgonJudgmentCounterDisplay.cs | 146 ++++++++++++++++++ 3 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Skinning/Components/ArgonJudgmentCounter.cs create mode 100644 osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index bd8f17185b..3789fb1645 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -15,6 +15,7 @@ using osu.Framework.Text; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -26,6 +27,8 @@ namespace osu.Game.Screens.Play.HUD public IBindable WireframeOpacity { get; } = new BindableFloat(); public Bindable ShowLabel { get; } = new BindableBool(); + public Bindable LabelColour { get; } = new Bindable(Color4.White); + public Bindable TextColour { get; } = new Bindable(Color4.White); public Container NumberContainer { get; private set; } @@ -58,7 +61,6 @@ namespace osu.Game.Screens.Play.HUD labelText = new OsuSpriteText { Alpha = 0, - BypassAutoSizeAxes = Axes.X, Text = label.GetValueOrDefault(), Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), Margin = new MarginPadding { Left = 2.5f }, @@ -110,7 +112,7 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuColour colours) { - labelText.Colour = colours.Blue0; + LabelColour.Value = colours.Blue0; } protected override void LoadComplete() @@ -122,6 +124,12 @@ namespace osu.Game.Screens.Play.HUD labelText.Alpha = s.NewValue ? 1 : 0; NumberContainer.Y = s.NewValue ? 12 : 0; }, true); + LabelColour.BindValueChanged(c => labelText.Colour = c.NewValue, true); + TextColour.BindValueChanged(c => + { + textPart.Colour = c.NewValue; + wireframesPart.Colour = c.NewValue; + }, true); } private partial class ArgonCounterSpriteText : OsuSpriteText diff --git a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs b/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs new file mode 100644 index 0000000000..2ab395eb6c --- /dev/null +++ b/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs @@ -0,0 +1,68 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.JudgementCounter; +using osuTK.Graphics; + +namespace osu.Game.Skinning.Components +{ + public sealed class ArgonJudgmentCounter : VisibilityContainer + { + public ArgonCounterTextComponent TextComponent; + private OsuColour colours = null!; + public readonly JudgementCount JudgementCounter; + public BindableInt DisplayedValue = new BindableInt(); + + public ArgonJudgmentCounter(JudgementCount judgementCounter) + { + this.JudgementCounter = judgementCounter; + + AutoSizeAxes = Axes.Both; + AddInternal(TextComponent = new ArgonCounterTextComponent(Anchor.TopRight, judgementCounter.DisplayName.ToUpper())); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + this.colours = colours; + } + + private void updateWireframe() + { + int wireframeLenght = Math.Max(2, TextComponent.Text.ToString().Length); + TextComponent.WireframeTemplate = new string('#', wireframeLenght); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + DisplayedValue.BindValueChanged(v => + { + TextComponent.Text = v.NewValue.ToString(); + updateWireframe(); + }, true); + + var result = JudgementCounter.Types.First(); + TextComponent.LabelColour.Value = getJudgementColor(result); + TextComponent.ShowLabel.BindValueChanged(v => TextComponent.TextColour.Value = !v.NewValue ? getJudgementColor(result) : Color4.White); + } + + private Color4 getJudgementColor(HitResult result) + { + return result.IsBasic() ? colours.ForHitResult(result) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter; + } + + protected override void PopIn() => this.FadeIn(JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(100); + } +} diff --git a/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs b/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs new file mode 100644 index 0000000000..dc705b0981 --- /dev/null +++ b/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs @@ -0,0 +1,146 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Localisation.HUD; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD.JudgementCounter; +using osuTK; + +namespace osu.Game.Skinning.Components +{ + [UsedImplicitly] + public partial class ArgonJudgmentCounterDisplay : CompositeDrawable, ISerialisableDrawable + { + [Resolved] + private JudgementCountController judgementCountController { get; set; } = null!; + + [SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")] + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) + { + Precision = 0.01f, + MinValue = 0, + MaxValue = 1, + }; + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + public Bindable ShowLabel { get; } = new BindableBool(true); + + [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowMaxJudgement))] + public BindableBool ShowMaxJudgement { get; } = new BindableBool(true); + + [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayMode))] + public Bindable Mode { get; } = new Bindable(); + + [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.FlowDirection))] + public Bindable FlowDirection { get; } = new Bindable(); + + private FillFlowContainer counterFlow = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + InternalChild = counterFlow = new FillFlowContainer + { + Direction = getFillDirection(FlowDirection.Value), + Spacing = new Vector2(16), + AutoSizeAxes = Axes.Both, + }; + + foreach (var counter in judgementCountController.Counters) + { + ArgonJudgmentCounter counterComponent = new ArgonJudgmentCounter(counter); + counterComponent.TextComponent.WireframeOpacity.BindTo(WireframeOpacity); + counterComponent.TextComponent.ShowLabel.BindTo(ShowLabel); + counterComponent.DisplayedValue.BindTo(counter.ResultCount); + counterFlow.Add(counterComponent); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Mode.BindValueChanged(_ => updateVisibility(), true); + ShowMaxJudgement.BindValueChanged(_ => updateVisibility(), true); + FlowDirection.BindValueChanged(d => counterFlow.Direction = getFillDirection(d.NewValue)); + } + + private void updateVisibility() + { + for (int i = 0; i < counterFlow.Children.Count; i++) + { + ArgonJudgmentCounter counter = counterFlow.Children[i]; + + if (shouldBeVisible(i, counter)) + counter.Show(); + else + counter.Hide(); + } + } + + private bool shouldBeVisible(int index, ArgonJudgmentCounter counter) + { + if (index == 0 && !ShowMaxJudgement.Value) + return false; + + var hitResult = counter.JudgementCounter.Types.First(); + if (hitResult.IsBasic()) + return true; + + switch (Mode.Value) + { + case DisplayMode.Simple: + return false; + + case DisplayMode.Normal: + return !hitResult.IsBonus(); + + case DisplayMode.All: + return true; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + private FillDirection getFillDirection(Direction flow) + { + switch (flow) + { + case Direction.Horizontal: + return FillDirection.Horizontal; + + case Direction.Vertical: + return FillDirection.Vertical; + + default: + throw new ArgumentOutOfRangeException(nameof(flow), flow, @"Unsupported direction"); + } + } + + public enum DisplayMode + { + [LocalisableDescription(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayModeSimple))] + Simple, + + [LocalisableDescription(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayModeNormal))] + Normal, + + [LocalisableDescription(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayModeAll))] + All + } + + public bool UsesFixedAnchor { get; set; } + } +} From cbab183ea14f6fea46c1a7b443ad00d30e628768 Mon Sep 17 00:00:00 2001 From: LukynkaCZE Date: Sat, 8 Mar 2025 21:34:27 +0100 Subject: [PATCH 002/267] add test scene --- .../TestSceneArgonJudgementCounter.cs | 193 ++++++++++++++++++ .../Components/ArgonJudgmentCounter.cs | 6 +- .../Components/ArgonJudgmentCounterDisplay.cs | 12 +- 3 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs new file mode 100644 index 0000000000..45b58b60ca --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs @@ -0,0 +1,193 @@ +// 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.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD.JudgementCounter; +using osu.Game.Skinning.Components; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneArgonJudgementCounter : OsuTestScene + { + private ScoreProcessor scoreProcessor = null!; + private JudgementCountController judgementCountController = null!; + private TestArgonJudgementCounterDisplay counterDisplay = null!; + + private DependencyProvidingContainer content = null!; + + protected override Container Content => content; + + private readonly Bindable lastJudgementResult = new Bindable(); + + private int iteration; + + [SetUpSteps] + public void SetUpSteps() => AddStep("Create components", () => + { + var ruleset = CreateRuleset(); + + Debug.Assert(ruleset != null); + + scoreProcessor = new ScoreProcessor(ruleset); + base.Content.Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) }, + Children = new Drawable[] + { + judgementCountController = new JudgementCountController(), + content = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(JudgementCountController), judgementCountController) }, + } + }, + }; + }); + + protected override Ruleset CreateRuleset() => new OsuRuleset(); + + private void applyOneJudgement(HitResult result) + { + lastJudgementResult.Value = new OsuJudgementResult(new HitObject + { + StartTime = iteration * 10000 + }, new OsuJudgement()) + { + Type = result, + }; + scoreProcessor.ApplyResult(lastJudgementResult.Value); + + iteration++; + } + + [Test] + public void TestAddJudgementsToCounters() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); + + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Great), 2); + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Miss), 2); + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Meh), 2); + } + + [Test] + public void TestAddWhilstHidden() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); + + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2); + AddAssert("Check value added whilst hidden", () => hiddenCount() == 2); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.All); + } + + [Test] + public void TestChangeFlowDirection() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); + + AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical); + AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal); + } + + [Test] + public void TestToggleJudgementNames() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); + + AddStep("Show label", () => counterDisplay.ShowLabel.Value = true); + AddWaitStep("wait some", 2); + AddAssert("Assert hidden", () => counterDisplay.CounterFlow.Children.First().Alpha == 1); + AddStep("Hide label", () => counterDisplay.ShowLabel.Value = false); + AddWaitStep("wait some", 2); + AddAssert("Assert shown", () => counterDisplay.CounterFlow.Children.First().Alpha == 1); + } + + [Test] + public void TestHideMaxValue() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); + + AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false); + AddWaitStep("wait some", 2); + AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); + AddStep("Show max judgement", () => counterDisplay.ShowMaxJudgement.Value = true); + } + + [Test] + public void TestMaxValueStartsHidden() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay + { + ShowMaxJudgement = { Value = false } + }); + AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); + } + + [Test] + public void TestMaxValueHiddenOnModeChange() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); + + AddStep("Set max judgement to hide itself", () => counterDisplay.ShowMaxJudgement.Value = false); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.All); + AddWaitStep("wait some", 2); + AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); + } + + [Test] + public void TestNoDuplicates() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.All); + AddAssert("Check no duplicates", + () => counterDisplay.CounterFlow.ChildrenOfType().Count(), + () => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType().Select(c => c.JudgementName).Distinct().Count())); + } + + [Test] + public void TestCycleDisplayModes() + { + AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); + + AddStep("Show basic judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.Simple); + AddWaitStep("wait some", 2); + AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 0); + AddStep("Show normal judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.Normal); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.All); + AddWaitStep("wait some", 2); + AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 1); + } + + private int hiddenCount() + { + var num = counterDisplay.CounterFlow.Children.First(child => child.JudgementCounter.Types.Contains(HitResult.LargeTickHit)); + return num.JudgementCounter.ResultCount.Value; + } + + private partial class TestArgonJudgementCounterDisplay : ArgonJudgmentCounterDisplay + { + public new FillFlowContainer CounterFlow => base.CounterFlow; + + public TestArgonJudgementCounterDisplay() + { + Margin = new MarginPadding { Top = 100 }; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + } + } + } +} diff --git a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs b/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs index 2ab395eb6c..8e48db0b4d 100644 --- a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs @@ -16,16 +16,18 @@ using osuTK.Graphics; namespace osu.Game.Skinning.Components { - public sealed class ArgonJudgmentCounter : VisibilityContainer + public sealed partial class ArgonJudgmentCounter : VisibilityContainer { public ArgonCounterTextComponent TextComponent; private OsuColour colours = null!; public readonly JudgementCount JudgementCounter; public BindableInt DisplayedValue = new BindableInt(); + public string JudgementName = null!; public ArgonJudgmentCounter(JudgementCount judgementCounter) { - this.JudgementCounter = judgementCounter; + JudgementCounter = judgementCounter; + JudgementName = judgementCounter.DisplayName.ToString().ToUpper(); AutoSizeAxes = Axes.Both; AddInternal(TextComponent = new ArgonCounterTextComponent(Anchor.TopRight, judgementCounter.DisplayName.ToUpper())); diff --git a/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs b/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs index dc705b0981..73d823671c 100644 --- a/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs +++ b/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs @@ -45,13 +45,13 @@ namespace osu.Game.Skinning.Components [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.FlowDirection))] public Bindable FlowDirection { get; } = new Bindable(); - private FillFlowContainer counterFlow = null!; + protected FillFlowContainer CounterFlow = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) { AutoSizeAxes = Axes.Both; - InternalChild = counterFlow = new FillFlowContainer + InternalChild = CounterFlow = new FillFlowContainer { Direction = getFillDirection(FlowDirection.Value), Spacing = new Vector2(16), @@ -64,7 +64,7 @@ namespace osu.Game.Skinning.Components counterComponent.TextComponent.WireframeOpacity.BindTo(WireframeOpacity); counterComponent.TextComponent.ShowLabel.BindTo(ShowLabel); counterComponent.DisplayedValue.BindTo(counter.ResultCount); - counterFlow.Add(counterComponent); + CounterFlow.Add(counterComponent); } } @@ -73,14 +73,14 @@ namespace osu.Game.Skinning.Components base.LoadComplete(); Mode.BindValueChanged(_ => updateVisibility(), true); ShowMaxJudgement.BindValueChanged(_ => updateVisibility(), true); - FlowDirection.BindValueChanged(d => counterFlow.Direction = getFillDirection(d.NewValue)); + FlowDirection.BindValueChanged(d => CounterFlow.Direction = getFillDirection(d.NewValue)); } private void updateVisibility() { - for (int i = 0; i < counterFlow.Children.Count; i++) + for (int i = 0; i < CounterFlow.Children.Count; i++) { - ArgonJudgmentCounter counter = counterFlow.Children[i]; + ArgonJudgmentCounter counter = CounterFlow.Children[i]; if (shouldBeVisible(i, counter)) counter.Show(); From bb588566e63504b9c6d38ac2a175179dd0ed68fd Mon Sep 17 00:00:00 2001 From: LukynkaCZE Date: Sat, 8 Mar 2025 21:38:45 +0100 Subject: [PATCH 003/267] fix ToString --- osu.Game/Skinning/Components/ArgonJudgmentCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs b/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs index 8e48db0b4d..5d827a9931 100644 --- a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs @@ -27,7 +27,7 @@ namespace osu.Game.Skinning.Components public ArgonJudgmentCounter(JudgementCount judgementCounter) { JudgementCounter = judgementCounter; - JudgementName = judgementCounter.DisplayName.ToString().ToUpper(); + JudgementName = judgementCounter.DisplayName.ToUpper().ToString(); AutoSizeAxes = Axes.Both; AddInternal(TextComponent = new ArgonCounterTextComponent(Anchor.TopRight, judgementCounter.DisplayName.ToUpper())); From cc7e60daab8fdf84941200724711c5ef1c70a968 Mon Sep 17 00:00:00 2001 From: LukynkaCZE Date: Sat, 8 Mar 2025 21:46:17 +0100 Subject: [PATCH 004/267] forgot remove initializer vlaue --- osu.Game/Skinning/Components/ArgonJudgmentCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs b/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs index 5d827a9931..8fbd472e54 100644 --- a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs @@ -22,7 +22,7 @@ namespace osu.Game.Skinning.Components private OsuColour colours = null!; public readonly JudgementCount JudgementCounter; public BindableInt DisplayedValue = new BindableInt(); - public string JudgementName = null!; + public string JudgementName; public ArgonJudgmentCounter(JudgementCount judgementCounter) { From 1e2468d2bbab5fd60f5843263866cf9a6c4ffb40 Mon Sep 17 00:00:00 2001 From: LukynkaCZE Date: Sat, 8 Mar 2025 23:02:57 +0100 Subject: [PATCH 005/267] Fix SkinDeserialisationTest failing --- .../Archives/modified-argon-20250308.osk | Bin 0 -> 1716 bytes .../Skins/SkinDeserialisationTest.cs | 2 ++ .../TestSceneArgonJudgementCounter.cs | 30 +++++++++--------- ...entCounter.cs => ArgonJudgementCounter.cs} | 4 +-- ...lay.cs => ArgonJudgementCounterDisplay.cs} | 12 +++---- 5 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20250308.osk rename osu.Game/Skinning/Components/{ArgonJudgmentCounter.cs => ArgonJudgementCounter.cs} (94%) rename osu.Game/Skinning/Components/{ArgonJudgmentCounterDisplay.cs => ArgonJudgementCounterDisplay.cs} (91%) diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250308.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250308.osk new file mode 100644 index 0000000000000000000000000000000000000000..cbaacd3f4b3cb99ece8079983114a10c91bbde3e GIT binary patch literal 1716 zcmZ{kdpHw%7{`AzYeqZfu8?$$IFYbo6{Bp}Fw3PxZn?~5qnVmIoF$S=O}PuDHX&*v zm)1E_TJAd(Dm~O`>w@Ewo#R^Pspp)YljrIC*ZaJ;=Y8Mjc|T+#R8|!LAP2OX!hMLH z$)yzlfc7sQ02q-}nkkh=MQ8Hm+i)n@Qu5O2uqwGA%PdZBXRmYggCOblgkz0A>8a#ksF9!<`~BGdDBu_YghmaqA#;kz63=NK9^AtIx#@ ziVATy1J`2zq3*+M*N02WrL1=J85X+m47zZERm!3+kdvh&?7X+IurP@@M8X}nJ~;tq z;#339I}3apz_$a!Vz&2JK>?8H0iX^5a0#T+96k5$p-0EiX(6;Y#+OMa3yJh+IFxWI zcTop*_oNtpj%;*VwTQP>4dcW}+Z4%%A1@Bkn5?}Z+B|wU7>Y=BP(yAL$-u8aeOi%J z^r`#Er$x_7kB<mID`DWf+HTUQTKfAL(@?Jz&J&aF|sUhBP(Y@VEA)F`xnrQo#x8}D_pP;pR&uIws@k2Qb->Xm5lP`~H!g8gqBX`5hTHnYZ%w=9j z!*C6Y%87#9xo-YYyJ7?k{*I{Jzc&PKromR1PFO9eR)i7kIF8Dh>G}0lciQ>kOsB4l zq4@BkypAB5+`BYeZK4|%A9$T<_g9KGY6H%joEdNs>NmjmnYX@nBYW3#?EZ^#S)Y(B zhZv^4^te&Vviu_@<4xJtT|8ziA^5-{h(As<$zXq0#hHx0&2WFAWs~F*MfhylK=|ry zMe{Jyv!3v@ptXH0Pss|k!;0&6Ge=)ZbA{5AJEoW>z%#uPa!ML69PM;Wj2O8u2rZ~? zH)2G)eOME|!9QPa1CyJkzd^`bsKJh-hj05NL2urQ$R01c!oRaU(V~BoQ|inS6h^0D z#zO7hLjl7g#TBndv2?;@{1#i8@Nnl?#1(#VGkeG^tT-!7G+*ARzYbEy(@S7zH@OZ} zS=UAe=$Y7#~2|LFFrvM%$d-BbQ?^v7|r~ z+bLW3Q+Ev0Ze5caM$p_l=Mf*yEl z=!=NKsOPoxb)=uOQ?57H+pJLz>bJe%)!)R3sd|KpE5(Zsm7yKi3|#q2Rep-B~;XJ{b4njqndp$^q{Cvh*AUr{!0 z#i~E35sRx}fJ}tQsDkg~`m5NVt^eS?x@t8Szlzbrau Zn*>{ZB{C7V=5t&Y$buOFKn?73_CJm0!@K|h literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 5b343c80c5..77b001d772 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -75,6 +75,8 @@ namespace osu.Game.Tests.Skins "Archives/modified-argon-20250116.osk", // Covers player team flag "Archives/modified-argon-20250214.osk", + // Covers argon judgement counter + "Archives/modified-argon-20250308.osk", }; /// diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs index 45b58b60ca..64bb6497ad 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2); AddAssert("Check value added whilst hidden", () => hiddenCount() == 2); - AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.All); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All); } [Test] @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false); AddWaitStep("wait some", 2); - AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); + AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); AddStep("Show max judgement", () => counterDisplay.ShowMaxJudgement.Value = true); } @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Gameplay { ShowMaxJudgement = { Value = false } }); - AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); + AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); } [Test] @@ -143,19 +143,19 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); AddStep("Set max judgement to hide itself", () => counterDisplay.ShowMaxJudgement.Value = false); - AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.All); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All); AddWaitStep("wait some", 2); - AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); + AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); } [Test] public void TestNoDuplicates() { AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); - AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.All); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All); AddAssert("Check no duplicates", - () => counterDisplay.CounterFlow.ChildrenOfType().Count(), - () => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType().Select(c => c.JudgementName).Distinct().Count())); + () => counterDisplay.CounterFlow.ChildrenOfType().Count(), + () => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType().Select(c => c.JudgementName).Distinct().Count())); } [Test] @@ -163,13 +163,13 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay()); - AddStep("Show basic judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.Simple); + AddStep("Show basic judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.Simple); AddWaitStep("wait some", 2); - AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 0); - AddStep("Show normal judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.Normal); - AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgmentCounterDisplay.DisplayMode.All); + AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 0); + AddStep("Show normal judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.Normal); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All); AddWaitStep("wait some", 2); - AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 1); + AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 1); } private int hiddenCount() @@ -178,9 +178,9 @@ namespace osu.Game.Tests.Visual.Gameplay return num.JudgementCounter.ResultCount.Value; } - private partial class TestArgonJudgementCounterDisplay : ArgonJudgmentCounterDisplay + private partial class TestArgonJudgementCounterDisplay : ArgonJudgementCounterDisplay { - public new FillFlowContainer CounterFlow => base.CounterFlow; + public new FillFlowContainer CounterFlow => base.CounterFlow; public TestArgonJudgementCounterDisplay() { diff --git a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs similarity index 94% rename from osu.Game/Skinning/Components/ArgonJudgmentCounter.cs rename to osu.Game/Skinning/Components/ArgonJudgementCounter.cs index 8fbd472e54..a627a53c02 100644 --- a/osu.Game/Skinning/Components/ArgonJudgmentCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs @@ -16,7 +16,7 @@ using osuTK.Graphics; namespace osu.Game.Skinning.Components { - public sealed partial class ArgonJudgmentCounter : VisibilityContainer + public sealed partial class ArgonJudgementCounter : VisibilityContainer { public ArgonCounterTextComponent TextComponent; private OsuColour colours = null!; @@ -24,7 +24,7 @@ namespace osu.Game.Skinning.Components public BindableInt DisplayedValue = new BindableInt(); public string JudgementName; - public ArgonJudgmentCounter(JudgementCount judgementCounter) + public ArgonJudgementCounter(JudgementCount judgementCounter) { JudgementCounter = judgementCounter; JudgementName = judgementCounter.DisplayName.ToUpper().ToString(); diff --git a/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs b/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs similarity index 91% rename from osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs rename to osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs index 73d823671c..dcf12b3f42 100644 --- a/osu.Game/Skinning/Components/ArgonJudgmentCounterDisplay.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs @@ -20,7 +20,7 @@ using osuTK; namespace osu.Game.Skinning.Components { [UsedImplicitly] - public partial class ArgonJudgmentCounterDisplay : CompositeDrawable, ISerialisableDrawable + public partial class ArgonJudgementCounterDisplay : CompositeDrawable, ISerialisableDrawable { [Resolved] private JudgementCountController judgementCountController { get; set; } = null!; @@ -45,13 +45,13 @@ namespace osu.Game.Skinning.Components [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.FlowDirection))] public Bindable FlowDirection { get; } = new Bindable(); - protected FillFlowContainer CounterFlow = null!; + protected FillFlowContainer CounterFlow = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) { AutoSizeAxes = Axes.Both; - InternalChild = CounterFlow = new FillFlowContainer + InternalChild = CounterFlow = new FillFlowContainer { Direction = getFillDirection(FlowDirection.Value), Spacing = new Vector2(16), @@ -60,7 +60,7 @@ namespace osu.Game.Skinning.Components foreach (var counter in judgementCountController.Counters) { - ArgonJudgmentCounter counterComponent = new ArgonJudgmentCounter(counter); + ArgonJudgementCounter counterComponent = new ArgonJudgementCounter(counter); counterComponent.TextComponent.WireframeOpacity.BindTo(WireframeOpacity); counterComponent.TextComponent.ShowLabel.BindTo(ShowLabel); counterComponent.DisplayedValue.BindTo(counter.ResultCount); @@ -80,7 +80,7 @@ namespace osu.Game.Skinning.Components { for (int i = 0; i < CounterFlow.Children.Count; i++) { - ArgonJudgmentCounter counter = CounterFlow.Children[i]; + ArgonJudgementCounter counter = CounterFlow.Children[i]; if (shouldBeVisible(i, counter)) counter.Show(); @@ -89,7 +89,7 @@ namespace osu.Game.Skinning.Components } } - private bool shouldBeVisible(int index, ArgonJudgmentCounter counter) + private bool shouldBeVisible(int index, ArgonJudgementCounter counter) { if (index == 0 && !ShowMaxJudgement.Value) return false; From 62b4999184546e5026e8d31b7cb1baabbb44b326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Aug 2025 11:22:37 +0200 Subject: [PATCH 006/267] Add failing test case --- .../TestSceneMultiSpectatorScreen.cs | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index faf8f35a8e..b9c77e20c0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -28,6 +28,7 @@ using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -302,9 +303,69 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 10); } + [Test] + [Explicit("Test relies on timing of arriving frames to exercise assertions which doesn't work headless.")] + public void TestMaximisedUserIsAudioSource() + { + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); + loadSpectateScreen(); + + // With no frames, the synchronisation state will be TooFarAhead. + // In this state, all players should be muted. + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, true); + + // Send frames for both players. + sendFrames(PLAYER_1_ID, 20); + sendFrames(PLAYER_2_ID, 40); + + waitUntilRunning(PLAYER_1_ID); + AddStep("maximise player 1", () => + { + InputManager.MoveMouseTo(getInstance(PLAYER_1_ID)); + InputManager.Click(MouseButton.Left); + }); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); + + waitUntilPaused(PLAYER_1_ID); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); + + AddStep("minimise player 1", () => + { + InputManager.MoveMouseTo(getInstance(PLAYER_1_ID)); + InputManager.Click(MouseButton.Left); + }); + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, false); + + AddStep("maximise player 2", () => + { + InputManager.MoveMouseTo(getInstance(PLAYER_2_ID)); + InputManager.Click(MouseButton.Left); + }); + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, false); + + waitUntilPaused(PLAYER_2_ID); + sendFrames(PLAYER_1_ID, 60); + + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, false); + + AddStep("minimise player 2", () => + { + InputManager.MoveMouseTo(getInstance(PLAYER_2_ID)); + InputManager.Click(MouseButton.Left); + }); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); + } + [Test] [FlakyTest] - public void TestMostInSyncUserIsAudioSource() + public void TestMostInSyncUserIsAudioSourceIfNoneMaximised() { start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); From 083365f3320c96d9655d9e73c794017510410bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Aug 2025 12:03:35 +0200 Subject: [PATCH 007/267] Always use audio from maximised player if there is one in multiplayer spectator --- .../Spectate/MultiSpectatorScreen.cs | 32 +++++++++++++------ .../Multiplayer/Spectate/PlayerGrid.cs | 8 ++++- .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 8 +---- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 1f96f0d371..200e6a715d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -178,17 +178,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { base.Update(); - if (!isCandidateAudioSource(currentAudioSource?.SpectatorPlayerClock)) - { - currentAudioSource = instances.Where(i => isCandidateAudioSource(i.SpectatorPlayerClock)).MinBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime)); + checkAudioSource(); + } - // Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio. - if (currentAudioSource != null) - bindAudioAdjustments(currentAudioSource); + private void checkAudioSource() + { + // always use the maximised player instance as the current audio source if there is one + if (grid.MaximisedCell?.Content is PlayerArea maximisedPlayer && maximisedPlayer == currentAudioSource) + return; - foreach (var instance in instances) - instance.Mute = instance != currentAudioSource; - } + // if there is no maximised player instance and the previous audio source is still good to use, keep using it + if (grid.MaximisedCell == null && isCandidateAudioSource(currentAudioSource?.SpectatorPlayerClock)) + return; + + // at this point we're in one of the following scenarios: + // - the maximised player instance is not the current audio source => we want to switch to the maximised player instance + // - there is no maximised player instance, and the previous audio source is stopped => find another running audio source + currentAudioSource = grid.MaximisedCell?.Content as PlayerArea + ?? instances.Where(i => isCandidateAudioSource(i.SpectatorPlayerClock)).MinBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime)); + + // Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio. + if (currentAudioSource != null) + bindAudioAdjustments(currentAudioSource); + + foreach (var instance in instances) + instance.Mute = instance != currentAudioSource; } private void bindAudioAdjustments(PlayerArea first) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index 6e71c010e5..c3ad14dba2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -31,6 +31,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public Facade MaximisedFacade { get; } + /// + /// The currently-maximised cell. + /// + public Cell? MaximisedCell { get; private set; } + private readonly Container paddingContainer; private readonly FillFlowContainer facadeContainer; private readonly Container cellContainer; @@ -99,7 +104,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void toggleMaximisationState(Cell target) { // in the case the target is the already maximised cell (or there is only one cell), no cell should be maximised. - bool hasMaximised = !target.IsMaximised && cellContainer.Count > 1; + bool hasMaximised = target != MaximisedCell && cellContainer.Count > 1; + MaximisedCell = hasMaximised ? target : null; // Iterate through all cells to ensure only one is maximised at any time. foreach (var cell in cellContainer.ToList()) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index bc31299615..d1ba214117 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// A cell of the grid. Contains the content and tracks to the linked facade. /// - private partial class Cell : CompositeDrawable + public partial class Cell : CompositeDrawable { /// /// The index of the original facade of this cell. @@ -33,11 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public Action? ToggleMaximisationState; - /// - /// Whether this cell is currently maximised. - /// - public bool IsMaximised { get; private set; } - private Facade facade; private bool isAnimating; @@ -83,7 +78,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void SetFacade(Facade newFacade, bool isMaximised) { facade = newFacade; - IsMaximised = isMaximised; isAnimating = true; TweenEdgeEffectTo(new EdgeEffectParameters From a6f823e5bc32bfc0e59fa4a0df82e1c2fff263b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Aug 2025 14:18:13 +0200 Subject: [PATCH 008/267] Show pinned rooms on top of listing --- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 15 ++++++++++++--- osu.Game/Online/Rooms/Room.cs | 9 +++++++++ .../OnlinePlay/Lounge/Components/RoomListing.cs | 3 +-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index 037c5faae3..ce9ee3a011 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -81,6 +81,15 @@ namespace osu.Game.Tests.Visual.Multiplayer CurrentPlaylistItem = item1 }), createLoungeRoom(new Room + { + Name = "Pinned room", + Pinned = true, + EndDate = DateTimeOffset.Now.AddDays(1), + Type = MatchType.HeadToHead, + Playlist = [item1], + CurrentPlaylistItem = item1 + }), + createLoungeRoom(new Room { Name = "Private room", Password = "*", @@ -140,13 +149,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for panel load", () => panel.ChildrenOfType().Any()); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); AddStep("set password", () => room.Password = "password"); - AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().Single().Alpha)); AddStep("unset password", () => room.Password = string.Empty); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); } [Test] diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index e965f9c187..4200fed0dd 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -263,6 +263,12 @@ namespace osu.Game.Online.Rooms set => SetField(ref availability, value); } + public bool Pinned + { + get => pinned; + set => SetField(ref pinned, value); + } + [JsonProperty("id")] private long? roomId; @@ -339,6 +345,9 @@ namespace osu.Game.Online.Rooms [JsonConverter(typeof(SnakeCaseStringEnumConverter))] private RoomStatus status; + [JsonProperty("pinned")] + private bool pinned; + // Not yet serialised (not implemented). private RoomAvailability availability; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 14edd13ec5..b93d26880d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -194,8 +194,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components roomFlow.Add(drawableRoom); - // Always show spotlight playlists at the top of the listing. - roomFlow.SetLayoutPosition(drawableRoom, room.Category > RoomCategory.Normal ? float.MinValue : -(room.RoomID ?? 0)); + roomFlow.SetLayoutPosition(drawableRoom, room.Pinned ? float.MinValue : -(room.RoomID ?? 0)); } applyFilterCriteria(Filter.Value); From 20b316d32d3cd4a575e31df5608209a0491588da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Aug 2025 14:18:47 +0200 Subject: [PATCH 009/267] Add indicator for pinned rooms in upper right of room panel --- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 6 +- .../OnlinePlay/Lounge/Components/RoomPanel.cs | 62 ++++++++++++++----- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index ce9ee3a011..d1b9005e63 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -149,13 +149,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for panel load", () => panel.ChildrenOfType().Any()); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().First().Alpha)); AddStep("set password", () => room.Password = "password"); - AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().First().Alpha)); AddStep("unset password", () => room.Password = string.Empty); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().First().Alpha)); } [Test] diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index 3610995b2c..258c9c3a97 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -59,7 +59,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private DrawableRoomParticipantsList? drawableRoomParticipantsList; private RoomSpecialCategoryPill? specialCategoryPill; - private PasswordProtectedIcon? passwordIcon; + private CornerIcon? passwordIcon; + private CornerIcon? pinnedIcon; private EndDateInfo? endDateInfo; private RoomNameLine? roomName; private DelayedLoadWrapper wrapper = null!; @@ -88,7 +89,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { ButtonsContainer = new Container { @@ -104,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Background5, + Colour = colourProvider.Background5, }, CreateBackground().With(d => { @@ -128,7 +129,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Background5, + Colour = colourProvider.Background5, Width = 0.2f, }, new Box @@ -136,7 +137,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)), + Colour = ColourInfo.GradientHorizontal(colourProvider.Background5, colourProvider.Background5.Opacity(0.3f)), Width = 0.8f, }, new GridContainer @@ -254,7 +255,28 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } }, - passwordIcon = new PasswordProtectedIcon { Alpha = 0 } + passwordIcon = new CornerIcon + { + Alpha = 0, + Background = { Colour = colours.Gray8, }, + Icon = + { + Icon = FontAwesome.Solid.Lock, + Colour = colours.Gray3, + Rotation = 45, + }, + }, + pinnedIcon = new CornerIcon + { + Alpha = 0, + Background = { Colour = colours.Orange2 }, + Icon = + { + Icon = FontAwesome.Solid.Thumbtack, + Colour = colours.Gray3, + Rotation = 45, + }, + } }, }, }, 0) @@ -283,6 +305,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components updateRoomCategory(); updateRoomType(); updateRoomHasPassword(); + updateRoomPinned(); }; SelectedItem.BindValueChanged(onSelectedItemChanged, true); @@ -311,6 +334,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components case nameof(Room.HasPassword): updateRoomHasPassword(); break; + + case nameof(Room.Pinned): + updateRoomPinned(); + break; } } @@ -371,6 +398,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components passwordIcon.Alpha = Room.HasPassword ? 1 : 0; } + private void updateRoomPinned() + { + if (pinnedIcon != null) + pinnedIcon.Alpha = Room.Pinned ? 1 : 0; + } + private int numberOfAvatars = 7; public int NumberOfAvatars @@ -534,10 +567,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - public partial class PasswordProtectedIcon : CompositeDrawable + public partial class CornerIcon : CompositeDrawable { - [BackgroundDependencyLoader] - private void load(OsuColour colours) + public SpriteIcon Icon { get; } + public Box Background { get; } + + public CornerIcon() { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; @@ -546,20 +581,19 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components InternalChildren = new Drawable[] { - new Box + Background = new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopCentre, - Colour = colours.Gray5, Rotation = 45, RelativeSizeAxes = Axes.Both, Width = 2, }, - new SpriteIcon + Icon = new SpriteIcon { - Icon = FontAwesome.Solid.Lock, Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, + Origin = Anchor.Centre, + Position = new Vector2(-13, 13), Margin = new MarginPadding(6), Size = new Vector2(14), } From 03e7e2b0d856f3164a872fc9d72bb298418c6535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Aug 2025 11:26:48 +0200 Subject: [PATCH 010/267] Update tests --- .../Visual/Multiplayer/TestSceneRoomListing.cs | 14 +++++++------- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 6 +++--- .../Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 58473f5fa2..7f6fb97e0c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -52,25 +52,25 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBasicListChanges() { - AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withSpotlightRooms: true))); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withPinnedRooms: true))); AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5); - AddAssert("all spotlights at top", () => container.DrawableRooms - .SkipWhile(r => r.Room.Category == RoomCategory.Spotlight) - .All(r => r.Room.Category == RoomCategory.Normal)); + AddAssert("all pinned at top", () => container.DrawableRooms + .SkipWhile(r => r.Room.Pinned) + .All(r => !r.Room.Pinned)); AddStep("remove first room", () => rooms.RemoveAt(0)); AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4); AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0)); AddStep("select first room", () => container.DrawableRooms.First().TriggerClick()); - AddAssert("first spotlight selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddAssert("first pinned room selected", () => checkRoomSelected(rooms.First(r => r.Pinned))); AddStep("remove last room", () => rooms.RemoveAt(rooms.Count - 1)); - AddAssert("first spotlight still selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddAssert("first pinned room selected", () => checkRoomSelected(rooms.First(r => r.Pinned))); - AddStep("remove spotlight room", () => rooms.RemoveAll(r => r.Category == RoomCategory.Spotlight)); + AddStep("remove pinned rooms", () => rooms.RemoveAll(r => r.Pinned)); AddAssert("selection vacated", () => checkRoomSelected(null)); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index d1b9005e63..58eb0f1ea1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -130,9 +130,9 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); - AddUntilStep("wait for panel load", () => rooms.Count == 7); - AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2); - AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 5); + AddUntilStep("wait for panel load", () => rooms.Count, () => Is.EqualTo(8)); + AddUntilStep("\"currently playing\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(3)); + AddUntilStep("\"ready to play\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)), () => Is.EqualTo(4)); } [Test] diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 914d187864..c687815270 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies(); - protected Room[] GenerateRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) + protected Room[] GenerateRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withPinnedRooms = false) { Room[] rooms = new Room[count]; @@ -110,10 +110,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay Name = $@"Room {currentRoomId}", Host = new APIUser { Username = @"Host" }, Duration = TimeSpan.FromSeconds(10), - Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, Password = withPassword ? @"password" : null, PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, - Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] + Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }], + Pinned = withPinnedRooms && i % 2 == 0, }; } From 68677200f3a758379fdefd01655bdf035504ecca Mon Sep 17 00:00:00 2001 From: Binwalker Date: Sat, 23 Aug 2025 16:43:36 +0900 Subject: [PATCH 011/267] feat(ManiaFilterCriteria): add long note ratio filter for mania --- .../ManiaFilterCriteria.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 9b2700c6e8..3b5736ad9f 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.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.Collections.Generic; using System.Linq; using osu.Framework.Bindables; @@ -19,12 +20,16 @@ namespace osu.Game.Rulesets.Mania public class ManiaFilterCriteria : IRulesetFilterCriteria { private readonly HashSet includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet(); + private FilterCriteria.OptionalRange longNoteRatio; public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); - return includedKeyCounts.Contains(keyCount); + bool keyCountMatch = includedKeyCounts.Contains(keyCount); + bool longNoteRatioMatch = !longNoteRatio.HasFilter || longNoteRatio.IsInRange(calculatelongNoteRatio(beatmapInfo)); + + return keyCountMatch && longNoteRatioMatch; } public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues) @@ -84,11 +89,24 @@ namespace osu.Game.Rulesets.Mania return false; } } + + case "ln": + case "lns": + return FilterQueryParser.TryUpdateCriteriaRange(ref longNoteRatio, op, strValues); } return false; } + private static float calculatelongNoteRatio(BeatmapInfo beatmapInfo) + { + int holdNotes = beatmapInfo.EndTimeObjectCount; + int totalNotes = beatmapInfo.TotalObjectCount; + int sum = Math.Max(1, totalNotes); + + return holdNotes / (float)sum * 100; + } + public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT) From f7b0e114a9f33e1bedd30508d5ce9bac455ed04c Mon Sep 17 00:00:00 2001 From: Binwalker Date: Sat, 23 Aug 2025 16:43:46 +0900 Subject: [PATCH 012/267] test(ManiaFilterCriteriaTest): add some testcase --- .../ManiaFilterCriteriaTest.cs | 224 +++++++++++++++++- 1 file changed, 223 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs index 24da447482..a7686c7320 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Tests } [TestCase] - public void TestInvalidFilters() + public void TestInvalidKeysFilters() { var criteria = new ManiaFilterCriteria(); @@ -183,5 +183,227 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text")); Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6")); } + + [TestCase] + public void TestLnsEqualSingleValue() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "50"); + BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); + BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 0, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo2, new FilterCriteria())); + + BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo3, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1"); + BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100"); + BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 100 + }; + Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1"); + BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 1000, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + } + + [TestCase] + public void TestLnsNotEqual() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); + BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.False(criteria.Matches(beatmapInfo1, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); + BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 0, + EndTimeObjectCount = 0 + }; + Assert.False(criteria.Matches(beatmapInfo2, new FilterCriteria())); + + BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.False(criteria.Matches(beatmapInfo3, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "1"); + BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 1 + }; + Assert.False(criteria.Matches(beatmapInfo4, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "100"); + BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 100 + }; + Assert.False(criteria.Matches(beatmapInfo5, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0.1"); + BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 1000, + EndTimeObjectCount = 1 + }; + Assert.False(criteria.Matches(beatmapInfo6, new FilterCriteria())); + } + + [TestCase] + public void TestLnsGreaterOrEqual() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "50"); + BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); + BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 0, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo2, new FilterCriteria())); + + BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo3, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1"); + BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100"); + BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 100 + }; + Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1"); + BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 1000, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + } + + [TestCase] + public void TestLnsGreater() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "49"); + BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); + BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 0, + EndTimeObjectCount = 0 + }; + Assert.False(criteria.Matches(beatmapInfo2, new FilterCriteria())); + + BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.False(criteria.Matches(beatmapInfo3, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.5"); + BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "99"); + BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 100 + }; + Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.01"); + BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 1000, + EndTimeObjectCount = 1 + }; + Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + } + + [TestCase] + public void TestInvalidLnsFilters() + { + var criteria = new ManiaFilterCriteria(); + + Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text")); + Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50,some text")); + Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text")); + } } } From 556c2469bf8b0660732f05d86695e8f9d9fc047b Mon Sep 17 00:00:00 2001 From: Binwalker Date: Mon, 25 Aug 2025 22:16:06 +0900 Subject: [PATCH 013/267] fix(ManiaFilterCriteria): converted beatmaps are not included --- .../ManiaFilterCriteria.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 3b5736ad9f..60dd8e1dae 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); bool keyCountMatch = includedKeyCounts.Contains(keyCount); - bool longNoteRatioMatch = !longNoteRatio.HasFilter || longNoteRatio.IsInRange(calculatelongNoteRatio(beatmapInfo)); + bool longNoteRatioMatch = !longNoteRatio.HasFilter || (!isConvertedBeatMap(beatmapInfo, criteria) && longNoteRatio.IsInRange(calculateLongNoteRatio(beatmapInfo))); return keyCountMatch && longNoteRatioMatch; } @@ -98,15 +98,6 @@ namespace osu.Game.Rulesets.Mania return false; } - private static float calculatelongNoteRatio(BeatmapInfo beatmapInfo) - { - int holdNotes = beatmapInfo.EndTimeObjectCount; - int totalNotes = beatmapInfo.TotalObjectCount; - int sum = Math.Max(1, totalNotes); - - return holdNotes / (float)sum * 100; - } - public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT) @@ -121,5 +112,19 @@ namespace osu.Game.Rulesets.Mania return false; } + + private static bool isConvertedBeatMap(BeatmapInfo beatmapInfo, FilterCriteria criteria) + { + return criteria.Ruleset == null || beatmapInfo.Ruleset.ShortName != criteria.Ruleset!.ShortName; + } + + private static float calculateLongNoteRatio(BeatmapInfo beatmapInfo) + { + int holdNotes = beatmapInfo.EndTimeObjectCount; + int totalNotes = beatmapInfo.TotalObjectCount; + int sum = Math.Max(1, totalNotes); + + return holdNotes / (float)sum * 100; + } } } From 65253708d8e939f237eb667d7797ee2c114df9f8 Mon Sep 17 00:00:00 2001 From: Binwalker Date: Mon, 25 Aug 2025 22:16:30 +0900 Subject: [PATCH 014/267] test(ManiaFilterCriteriaTest): fix some test case for ln filter --- .../ManiaFilterCriteriaTest.cs | 94 ++++++++++++++----- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs index a7686c7320..885390a052 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Mania.Tests } [TestCase] - public void TestFilterIntersection() + public void TestKeysFilterIntersection() { var criteria = new ManiaFilterCriteria(); criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4"); @@ -185,9 +185,13 @@ namespace osu.Game.Rulesets.Mania.Tests } [TestCase] - public void TestLnsEqualSingleValue() + public void TestLnsEqual() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "mania" } + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "50"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -195,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -203,14 +207,15 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 0, EndTimeObjectCount = 0 }; - Assert.True(criteria.Matches(beatmapInfo2, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo2, filterCriteria)); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, EndTimeObjectCount = 0 }; - Assert.True(criteria.Matches(beatmapInfo3, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo3, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1"); BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -218,7 +223,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -226,7 +231,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 100 }; - Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1"); BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -234,13 +239,17 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); } [TestCase] public void TestLnsNotEqual() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "mania" } + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -248,7 +257,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.False(criteria.Matches(beatmapInfo1, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -256,14 +265,15 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 0, EndTimeObjectCount = 0 }; - Assert.False(criteria.Matches(beatmapInfo2, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo2, filterCriteria)); + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, EndTimeObjectCount = 0 }; - Assert.False(criteria.Matches(beatmapInfo3, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo3, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "1"); BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -271,7 +281,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 1 }; - Assert.False(criteria.Matches(beatmapInfo4, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo4, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "100"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -279,7 +289,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 100 }; - Assert.False(criteria.Matches(beatmapInfo5, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo5, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0.1"); BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -287,13 +297,25 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.False(criteria.Matches(beatmapInfo6, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo6, filterCriteria)); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); + BeatmapInfo beatmapInfo7 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 0 + }; + Assert.True(criteria.Matches(beatmapInfo7, filterCriteria)); } [TestCase] public void TestLnsGreaterOrEqual() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "mania" } + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "50"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -301,7 +323,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -309,14 +331,15 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 0, EndTimeObjectCount = 0 }; - Assert.True(criteria.Matches(beatmapInfo2, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo2, filterCriteria)); + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, EndTimeObjectCount = 0 }; - Assert.True(criteria.Matches(beatmapInfo3, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo3, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1"); BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -324,7 +347,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -332,7 +355,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 100 }; - Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1"); BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -340,13 +363,17 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); } [TestCase] public void TestLnsGreater() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "mania" } + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "49"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -354,7 +381,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.True(criteria.Matches(beatmapInfo1, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -362,14 +389,15 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 0, EndTimeObjectCount = 0 }; - Assert.False(criteria.Matches(beatmapInfo2, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo2, filterCriteria)); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, EndTimeObjectCount = 0 }; - Assert.False(criteria.Matches(beatmapInfo3, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo3, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.5"); BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -377,7 +405,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo4, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "99"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -385,7 +413,7 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 100, EndTimeObjectCount = 100 }; - Assert.True(criteria.Matches(beatmapInfo5, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.01"); BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) @@ -393,7 +421,21 @@ namespace osu.Game.Rulesets.Mania.Tests TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo6, new FilterCriteria())); + Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); + } + + [TestCase] + public void TestLnsNotManiaRuleset() + { + var criteria = new ManiaFilterCriteria(); + + criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100"); + BeatmapInfo beatmapInfo = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + { + TotalObjectCount = 100, + EndTimeObjectCount = 50 + }; + Assert.False(criteria.Matches(beatmapInfo, new FilterCriteria())); } [TestCase] From 6a82b7331fd0da4208d3f3c760df44717b83ee7c Mon Sep 17 00:00:00 2001 From: Binwalker Date: Tue, 26 Aug 2025 21:27:57 +0900 Subject: [PATCH 015/267] refactor(ManiaFilterCriteria): exclude converted beatmaps from long note filter --- .../ManiaFilterCriteria.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 60dd8e1dae..3f7a018dd1 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -20,16 +20,16 @@ namespace osu.Game.Rulesets.Mania public class ManiaFilterCriteria : IRulesetFilterCriteria { private readonly HashSet includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet(); - private FilterCriteria.OptionalRange longNoteRatio; + private FilterCriteria.OptionalRange longNotePercentage; public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); bool keyCountMatch = includedKeyCounts.Contains(keyCount); - bool longNoteRatioMatch = !longNoteRatio.HasFilter || (!isConvertedBeatMap(beatmapInfo, criteria) && longNoteRatio.IsInRange(calculateLongNoteRatio(beatmapInfo))); + bool longNotePercentageMatch = !longNotePercentage.HasFilter || (!isConvertedBeatmap(beatmapInfo) && longNotePercentage.IsInRange(calculateLongNotePercentage(beatmapInfo))); - return keyCountMatch && longNoteRatioMatch; + return keyCountMatch && longNotePercentageMatch; } public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues) @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania case "ln": case "lns": - return FilterQueryParser.TryUpdateCriteriaRange(ref longNoteRatio, op, strValues); + return FilterQueryParser.TryUpdateCriteriaRange(ref longNotePercentage, op, strValues); } return false; @@ -113,18 +113,17 @@ namespace osu.Game.Rulesets.Mania return false; } - private static bool isConvertedBeatMap(BeatmapInfo beatmapInfo, FilterCriteria criteria) + private static bool isConvertedBeatmap(BeatmapInfo beatmapInfo) { - return criteria.Ruleset == null || beatmapInfo.Ruleset.ShortName != criteria.Ruleset!.ShortName; + return !beatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo); } - private static float calculateLongNoteRatio(BeatmapInfo beatmapInfo) + private static float calculateLongNotePercentage(BeatmapInfo beatmapInfo) { int holdNotes = beatmapInfo.EndTimeObjectCount; - int totalNotes = beatmapInfo.TotalObjectCount; - int sum = Math.Max(1, totalNotes); + int totalNotes = Math.Max(1, beatmapInfo.TotalObjectCount); - return holdNotes / (float)sum * 100; + return holdNotes / (float)totalNotes * 100; } } } From 149f18c3f549e38ffbb7cf1c18c50f829c513b40 Mon Sep 17 00:00:00 2001 From: Binwalker Date: Tue, 26 Aug 2025 21:28:45 +0900 Subject: [PATCH 016/267] test(ManiaFilterCriteriaTest): simplify test case --- .../ManiaFilterCriteriaTest.cs | 185 +++--------------- 1 file changed, 24 insertions(+), 161 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs index 885390a052..ad3cf4e05f 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -190,30 +190,30 @@ namespace osu.Game.Rulesets.Mania.Tests var criteria = new ManiaFilterCriteria(); var filterCriteria = new FilterCriteria { - Ruleset = new RulesetInfo { ShortName = "mania" } + Ruleset = new ManiaRuleset().RulesetInfo }; - criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "50"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 100, - EndTimeObjectCount = 50 + TotalObjectCount = 0, + EndTimeObjectCount = 0 }; Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 0, + TotalObjectCount = 100, EndTimeObjectCount = 0 }; Assert.True(criteria.Matches(beatmapInfo2, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, - EndTimeObjectCount = 0 + EndTimeObjectCount = 100 }; Assert.True(criteria.Matches(beatmapInfo3, filterCriteria)); @@ -225,87 +225,13 @@ namespace osu.Game.Rulesets.Mania.Tests }; Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 100, - EndTimeObjectCount = 100 + TotalObjectCount = 1000, + EndTimeObjectCount = 1 }; Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1"); - BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 1000, - EndTimeObjectCount = 1 - }; - Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); - } - - [TestCase] - public void TestLnsNotEqual() - { - var criteria = new ManiaFilterCriteria(); - var filterCriteria = new FilterCriteria - { - Ruleset = new RulesetInfo { ShortName = "mania" } - }; - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); - BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 50 - }; - Assert.False(criteria.Matches(beatmapInfo1, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); - BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 0, - EndTimeObjectCount = 0 - }; - Assert.False(criteria.Matches(beatmapInfo2, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0"); - BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 0 - }; - Assert.False(criteria.Matches(beatmapInfo3, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "1"); - BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 1 - }; - Assert.False(criteria.Matches(beatmapInfo4, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "100"); - BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 100 - }; - Assert.False(criteria.Matches(beatmapInfo5, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "0.1"); - BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 1000, - EndTimeObjectCount = 1 - }; - Assert.False(criteria.Matches(beatmapInfo6, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50"); - BeatmapInfo beatmapInfo7 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 0 - }; - Assert.True(criteria.Matches(beatmapInfo7, filterCriteria)); } [TestCase] @@ -314,30 +240,30 @@ namespace osu.Game.Rulesets.Mania.Tests var criteria = new ManiaFilterCriteria(); var filterCriteria = new FilterCriteria { - Ruleset = new RulesetInfo { ShortName = "mania" } + Ruleset = new ManiaRuleset().RulesetInfo }; - criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "50"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 100, - EndTimeObjectCount = 50 + TotalObjectCount = 0, + EndTimeObjectCount = 0 }; Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { - TotalObjectCount = 0, + TotalObjectCount = 100, EndTimeObjectCount = 0 }; Assert.True(criteria.Matches(beatmapInfo2, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0"); + criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100"); BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 100, - EndTimeObjectCount = 0 + EndTimeObjectCount = 100 }; Assert.True(criteria.Matches(beatmapInfo3, filterCriteria)); @@ -349,93 +275,31 @@ namespace osu.Game.Rulesets.Mania.Tests }; Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100"); - BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 100 - }; - Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); - criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1"); - BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 1000, - EndTimeObjectCount = 1 - }; - Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); - } - - [TestCase] - public void TestLnsGreater() - { - var criteria = new ManiaFilterCriteria(); - var filterCriteria = new FilterCriteria - { - Ruleset = new RulesetInfo { ShortName = "mania" } - }; - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "49"); - BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 50 - }; - Assert.True(criteria.Matches(beatmapInfo1, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); - BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 0, - EndTimeObjectCount = 0 - }; - Assert.False(criteria.Matches(beatmapInfo2, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0"); - BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 0 - }; - Assert.False(criteria.Matches(beatmapInfo3, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.5"); - BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 1 - }; - Assert.True(criteria.Matches(beatmapInfo4, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "99"); BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) - { - TotalObjectCount = 100, - EndTimeObjectCount = 100 - }; - Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); - - criteria.TryParseCustomKeywordCriteria("lns", Operator.Greater, "0.01"); - BeatmapInfo beatmapInfo6 = new BeatmapInfo(new ManiaRuleset().RulesetInfo) { TotalObjectCount = 1000, EndTimeObjectCount = 1 }; - Assert.True(criteria.Matches(beatmapInfo6, filterCriteria)); + Assert.True(criteria.Matches(beatmapInfo5, filterCriteria)); } [TestCase] public void TestLnsNotManiaRuleset() { var criteria = new ManiaFilterCriteria(); + var filterCriteria = new FilterCriteria + { + Ruleset = new ManiaRuleset().RulesetInfo + }; criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100"); - BeatmapInfo beatmapInfo = new BeatmapInfo(new ManiaRuleset().RulesetInfo) + BeatmapInfo beatmapInfo = new BeatmapInfo { TotalObjectCount = 100, EndTimeObjectCount = 50 }; - Assert.False(criteria.Matches(beatmapInfo, new FilterCriteria())); + Assert.False(criteria.Matches(beatmapInfo, filterCriteria)); } [TestCase] @@ -444,7 +308,6 @@ namespace osu.Game.Rulesets.Mania.Tests var criteria = new ManiaFilterCriteria(); Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text")); - Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.NotEqual, "50,some text")); Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text")); } } From 043235fed25bf00eadc42caa0d2611dfbc86cdd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Aug 2025 18:03:37 +0900 Subject: [PATCH 017/267] Add test coverage ensuring filtering does not occur on unnecessary updates --- .../TestSceneBeatmapCarouselUpdateHandling.cs | 81 +++++++++++++++++-- osu.Game/Graphics/Carousel/Carousel.cs | 2 +- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index eb610a40f1..3638c8eeec 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -22,12 +22,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { private BeatmapSetInfo baseTestBeatmap = null!; + private const int initial_filter_count = 3; + [SetUpSteps] public void SetUpSteps() { RemoveAllBeatmaps(); CreateCarousel(); + WaitForFiltering(); AddBeatmaps(1, 3); + WaitForFiltering(); AddStep("generate and add test beatmap", () => { baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3); @@ -42,8 +46,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 b.Metadata = metadata; BeatmapSets.Add(baseTestBeatmap); }); - WaitForFiltering(); + + AddAssert("filter count correct", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count)); } [Test] @@ -81,12 +86,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep("is scrolled to end", () => Carousel.ChildrenOfType().Single().IsScrolledToEnd()); - updateBeatmap(b => b.Metadata = new BeatmapMetadata + updateBeatmap(b => { - Artist = "updated test", - Title = $"beatmap {RNG.Next().ToString()}" + // hash will be updated when important metadata changes, such as title, difficulty, author etc. + b.Hash = "new hash"; + b.Metadata = new BeatmapMetadata + { + Artist = "updated test", + Title = $"beatmap {RNG.Next().ToString()}" + }; }); + assertDidFilter(); WaitForFiltering(); AddAssert("scroll is still at end", () => Carousel.ChildrenOfType().Single().IsScrolledToEnd()); @@ -113,8 +124,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("find panel", () => panel = Carousel.ChildrenOfType().Single(p => p.ChildrenOfType().Any(t => t.Text.ToString() == "beatmap"))); - updateBeatmap(b => b.Metadata = metadata); + updateBeatmap(b => + { + b.Metadata = metadata; + // hash will be updated when important metadata changes, such as title, difficulty, author etc. + b.Hash = "new hash"; + }); + assertDidFilter(); WaitForFiltering(); AddAssert("drawables unchanged", () => Carousel.ChildrenOfType(), () => Is.EqualTo(originalDrawables)); @@ -123,7 +140,41 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - public void TestSelectionHeld() + public void TestOnlineStatusUpdated() + { + List originalDrawables = new List(); + + AddStep("store drawable references", () => + { + originalDrawables.Clear(); + originalDrawables.AddRange(Carousel.ChildrenOfType().ToList()); + }); + + updateBeatmap(b => b.Status = BeatmapOnlineStatus.Graveyard); + + assertDidFilter(); + WaitForFiltering(); + + AddAssert("drawables unchanged", () => Carousel.ChildrenOfType(), () => Is.EqualTo(originalDrawables)); + } + + [Test] + public void TestNoUpdateTriggeredOnUserTagsChange() + { + var metadata = new BeatmapMetadata + { + Artist = "updated test", + Title = "new beatmap title", + UserTags = { "hi" } + }; + + updateBeatmap(b => b.Metadata = metadata); + assertDidNotFilter(); + } + + [TestCase(false)] + [TestCase(true)] + public void TestSelectionHeld(bool hashChanged) { SelectNextSet(); @@ -131,7 +182,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - updateBeatmap(); + updateBeatmap(b => + { + if (hashChanged) + b.Hash = "new hash"; + }); + + if (hashChanged) + assertDidFilter(); + else + assertDidNotFilter(); + WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -148,6 +209,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.DifficultyName = "new name"); + assertDidFilter(); WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -164,6 +226,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.OnlineID = b.OnlineID + 1); + assertDidFilter(); WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -339,6 +402,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder)); } + private void assertDidFilter() => AddAssert("did filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count + 1)); + + private void assertDidNotFilter() => AddAssert("did not filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count)); + private void updateBeatmap(Action? updateBeatmap = null, Action? updateSet = null) { AddStep("update beatmap with different reference", () => diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 7b5aea08b6..c9a3c7f723 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -79,7 +79,7 @@ namespace osu.Game.Graphics.Carousel /// /// The number of times filter operations have been triggered. /// - internal int FilterCount { get; private set; } + public int FilterCount { get; private set; } /// /// The number of displayable items currently being tracked (before filtering). From 0e57ee9ba68f0fb7b58b8f11efd4611ff4e9d936 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Aug 2025 18:04:19 +0900 Subject: [PATCH 018/267] Avoid triggering changes when add operations are empty Only seems to happen in tests. I think. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index da841aa361..10ce578562 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -143,6 +143,9 @@ namespace osu.Game.Screens.SelectV2 switch (changed.Action) { case NotifyCollectionChangedAction.Add: + if (!newItems!.Any()) + return; + Items.AddRange(newItems!.SelectMany(s => s.Beatmaps)); break; From be6fb9aa776f83e1b9560329f6a5033a5b1476b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Aug 2025 18:04:58 +0900 Subject: [PATCH 019/267] Fix beatmap carousel re-filtering when it doesn't need to Local rules ensure we only handle callbacks when we need to. --- osu.Game/Graphics/Carousel/Carousel.cs | 13 +++++- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 46 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index c9a3c7f723..b81df0a7eb 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using System.Threading; @@ -210,6 +211,12 @@ namespace osu.Game.Graphics.Carousel return filterTask; } + /// + /// Called when changes in any way. + /// + /// Whether a re-filter is required. + protected virtual bool HandleItemsChanged(NotifyCollectionChangedEventArgs args) => true; + /// /// Fired after a filter operation completed. /// @@ -301,7 +308,11 @@ namespace osu.Game.Graphics.Carousel RelativeSizeAxes = Axes.Both, }; - Items.BindCollectionChanged((_, _) => filterAfterItemsChanged.Invalidate()); + Items.BindCollectionChanged((_, args) => + { + if (HandleItemsChanged(args)) + filterAfterItemsChanged.Invalidate(); + }); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 10ce578562..ad691d34c0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -356,6 +356,52 @@ namespace osu.Game.Screens.SelectV2 } } + protected override bool HandleItemsChanged(NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + case NotifyCollectionChangedAction.Remove: + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Reset: + return true; + + case NotifyCollectionChangedAction.Replace: + var oldBeatmaps = args.OldItems!.OfType().ToList(); + var newBeatmaps = args.NewItems!.OfType().ToList(); + + for (int i = 0; i < oldBeatmaps.Count; i++) + { + var oldBeatmap = oldBeatmaps[i]; + var newBeatmap = newBeatmaps[i]; + + // Ignore changes which don't concern us. + // + // Here are some examples of things that can go wrong: + // - Background difficulty calculation runs and causes a realm update. + // We use `BeatmapDifficultyCache` and don't want to know about these. + // - Background user tag population runs and causes a realm update. + // We don't display user tags so want to ignore this. + if ( + // covers metadata changes + oldBeatmap.Hash == newBeatmap.Hash && + // displayed + oldBeatmap.Status == newBeatmap.Status && + // displayed + oldBeatmap.DifficultyName == newBeatmap.DifficultyName && + // sanity + oldBeatmap.OnlineID == newBeatmap.OnlineID + ) + return false; + } + + return true; + + default: + throw new ArgumentOutOfRangeException(); + } + } + protected override void HandleFilterCompleted() { base.HandleFilterCompleted(); From 40303832761831e74ac31175205151257bf9030c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Aug 2025 14:45:20 +0200 Subject: [PATCH 020/267] Allow grouping modes that apply max aggregate to split beatmap sets apart --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index f17281db2f..f0ec3ae3ab 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -43,7 +43,8 @@ namespace osu.Game.Screens.SelectV2 private readonly Func> getCollections; private readonly Func> getLocalUserTopRanks; - public BeatmapCarouselFilterGrouping(Func getCriteria, Func> getCollections, Func> getLocalUserTopRanks) + public BeatmapCarouselFilterGrouping(Func getCriteria, Func> getCollections, + Func> getLocalUserTopRanks) { this.getCriteria = getCriteria; this.getCollections = getCollections; @@ -189,9 +190,6 @@ namespace osu.Game.Screens.SelectV2 { var date = b.LastPlayed; - if (BeatmapSetsGroupedTogether) - date = aggregateMax(b, static b => b.LastPlayed ?? DateTimeOffset.MinValue); - if (date == null || date == DateTimeOffset.MinValue) return new GroupDefinition(int.MaxValue, "Never"); @@ -202,29 +200,13 @@ namespace osu.Game.Screens.SelectV2 return getGroupsBy(b => defineGroupByStatus(b.BeatmapSet!.Status), items); case GroupMode.BPM: - return getGroupsBy(b => - { - double bpm = FormatUtils.RoundBPM(b.BPM); - - if (BeatmapSetsGroupedTogether) - bpm = aggregateMax(b, bb => FormatUtils.RoundBPM(bb.BPM)); - - return defineGroupByBPM(bpm); - }, items); + return getGroupsBy(b => defineGroupByBPM(FormatUtils.RoundBPM(b.BPM)), items); case GroupMode.Difficulty: return getGroupsBy(b => defineGroupByStars(b.StarRating), items); case GroupMode.Length: - return getGroupsBy(b => - { - double length = b.Length; - - if (BeatmapSetsGroupedTogether) - length = aggregateMax(b, bb => bb.Length); - - return defineGroupByLength(length); - }, items); + return getGroupsBy(b => defineGroupByLength(b.Length), items); case GroupMode.Source: return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items); @@ -433,12 +415,6 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(int.MaxValue, "Unplayed"); } - private static T? aggregateMax(BeatmapInfo b, Func func) - { - var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden); - return beatmaps.Max(func); - } - private record GroupMapping(GroupDefinition? Group, List ItemsInGroup); } } From 8a6c8577192e61aa475be2e461a0529d2501fbfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Aug 2025 02:33:18 +0900 Subject: [PATCH 021/267] Fix hidden beatmap state not being reflected immediately --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index ad691d34c0..5b0ad1ae29 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -382,16 +382,19 @@ namespace osu.Game.Screens.SelectV2 // We use `BeatmapDifficultyCache` and don't want to know about these. // - Background user tag population runs and causes a realm update. // We don't display user tags so want to ignore this. - if ( + bool equalForDisplayPurposes = // covers metadata changes oldBeatmap.Hash == newBeatmap.Hash && - // displayed + // sanity check + oldBeatmap.OnlineID == newBeatmap.OnlineID && + // displayed on panel oldBeatmap.Status == newBeatmap.Status && - // displayed + // displayed on panel oldBeatmap.DifficultyName == newBeatmap.DifficultyName && - // sanity - oldBeatmap.OnlineID == newBeatmap.OnlineID - ) + // hidden changed, needs re-filter + oldBeatmap.Hidden == newBeatmap.Hidden; + + if (equalForDisplayPurposes) return false; } From e831d1b6fa3110740bb4faa3113ac813b9bfb54f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Aug 2025 13:06:14 +0900 Subject: [PATCH 022/267] Preserve pre-post notification completion target --- osu.Game/Overlays/NotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 18a487a312..f56e5e6ac3 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -180,7 +180,7 @@ namespace osu.Game.Overlays notification.Closed += () => notificationClosed(notification); if (notification is IHasCompletionTarget hasCompletionTarget) - hasCompletionTarget.CompletionTarget = Post; + hasCompletionTarget.CompletionTarget ??= Post; playDebouncedSample(notification.PopInSampleName); From b95573f97df469dafe331631fc73b04f98844d80 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Aug 2025 13:20:02 +0900 Subject: [PATCH 023/267] Fix potential loss of room events during join --- .../Online/Multiplayer/MultiplayerClient.cs | 181 ++++++++++-------- 1 file changed, 99 insertions(+), 82 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 986bc26716..14bc0bad82 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -172,6 +172,8 @@ namespace osu.Game.Online.Multiplayer protected Room? APIRoom { get; private set; } + private readonly Queue pendingRequests = new Queue(); + [BackgroundDependencyLoader] private void load() { @@ -266,6 +268,9 @@ namespace osu.Game.Online.Multiplayer updateLocalRoomSettings(joinedRoom.Settings); + while (pendingRequests.TryDequeue(out Action? action)) + action(); + postServerShuttingDownNotification(); OnRoomJoined(); @@ -300,10 +305,23 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); }); - return joinOrLeaveTaskChain.Add(async () => + return Task.Run(async () => { - await scheduledReset.ConfigureAwait(false); - await LeaveRoomInternal().ConfigureAwait(false); + try + { + await joinOrLeaveTaskChain.Add(async () => + { + await scheduledReset.ConfigureAwait(false); + await LeaveRoomInternal().ConfigureAwait(false); + }); + } + finally + { + await runOnUpdateThreadAsync(() => + { + pendingRequests.Clear(); + }); + } }); } @@ -449,11 +467,9 @@ namespace osu.Game.Online.Multiplayer Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Room.State = state; @@ -476,7 +492,7 @@ namespace osu.Game.Online.Multiplayer } RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } @@ -485,10 +501,9 @@ namespace osu.Game.Online.Multiplayer { await PopulateUsers([user]).ConfigureAwait(false); - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; + Debug.Assert(Room != null); // for sanity, ensure that there can be no duplicate users in the room user list. if (Room.Users.Any(existing => existing.UserID == user.UserID)) @@ -500,18 +515,18 @@ namespace osu.Game.Online.Multiplayer UserJoined?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); } Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) { - Scheduler.Add(() => handleUserLeft(user, UserLeft), false); + handleRoomRequest(() => handleUserLeft(user, UserLeft)); return Task.CompletedTask; } Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user) { - Scheduler.Add(() => + handleRoomRequest(() => { if (LocalUser == null) return; @@ -520,7 +535,7 @@ namespace osu.Game.Online.Multiplayer LeaveRoom(); handleUserLeft(user, UserKicked); - }, false); + }); return Task.CompletedTask; } @@ -528,9 +543,7 @@ namespace osu.Game.Online.Multiplayer private void handleUserLeft(MultiplayerRoomUser user, Action? callback) { Debug.Assert(ThreadSafety.IsUpdateThread); - - if (Room == null) - return; + Debug.Assert(Room != null); Room.Users.Remove(user); PlayingUserIds.Remove(user.UserID); @@ -587,11 +600,9 @@ namespace osu.Game.Online.Multiplayer Task IMultiplayerClient.HostChanged(int userId) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); var user = Room.Users.FirstOrDefault(u => u.UserID == userId); @@ -601,22 +612,24 @@ namespace osu.Game.Online.Multiplayer HostChanged?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) { - Scheduler.Add(() => updateLocalRoomSettings(newSettings)); + handleRoomRequest(() => updateLocalRoomSettings(newSettings)); return Task.CompletedTask; } Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // TODO: user should NEVER be null here, see https://github.com/ppy/osu/issues/17713. if (user == null) @@ -626,16 +639,18 @@ namespace osu.Game.Online.Multiplayer updateUserPlayingState(userId, state); RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.MatchUserStateChanged(int userId, MatchUserState state) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // TODO: user should NEVER be null here, see https://github.com/ppy/osu/issues/17713. if (user == null) @@ -643,31 +658,29 @@ namespace osu.Game.Online.Multiplayer user.MatchState = state; RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.MatchRoomStateChanged(MatchRoomState state) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; + Debug.Assert(Room != null); Room.MatchState = state; RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } public Task MatchEvent(MatchServerEvent e) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; + Debug.Assert(Room != null); switch (e) { @@ -691,7 +704,7 @@ namespace osu.Game.Online.Multiplayer } RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } @@ -708,9 +721,11 @@ namespace osu.Game.Online.Multiplayer Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // errors here are not critical - beatmap availability state is mostly for display. if (user == null) @@ -719,16 +734,18 @@ namespace osu.Game.Online.Multiplayer user.BeatmapAvailability = beatmapAvailability; RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.UserStyleChanged(int userId, int? beatmapId, int? rulesetId) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // errors here are not critical - user style is mostly for display. if (user == null) @@ -739,16 +756,18 @@ namespace osu.Game.Online.Multiplayer UserStyleChanged?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.UserModsChanged(int userId, IEnumerable mods) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // errors here are not critical - user mods are mostly for display. if (user == null) @@ -758,70 +777,60 @@ namespace osu.Game.Online.Multiplayer UserModsChanged?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.LoadRequested() { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); LoadRequested?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.GameplayAborted(GameplayAbortReason reason) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); GameplayAborted?.Invoke(reason); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.GameplayStarted() { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); GameplayStarted?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.ResultsReady() { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); ResultsReady?.Invoke(); - }, false); + }); return Task.CompletedTask; } public Task PlaylistItemAdded(MultiplayerPlaylistItem item) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Room.Playlist.Add(item); @@ -836,11 +845,9 @@ namespace osu.Game.Online.Multiplayer public Task PlaylistItemRemoved(long playlistItemId) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId)); @@ -857,11 +864,9 @@ namespace osu.Game.Online.Multiplayer public Task PlaylistItemChanged(MultiplayerPlaylistItem item) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Room.Playlist[Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID))] = item; @@ -908,9 +913,7 @@ namespace osu.Game.Online.Multiplayer /// The new to update from. private void updateLocalRoomSettings(MultiplayerRoomSettings settings) { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); // Update a few properties of the room instantaneously. @@ -972,6 +975,20 @@ namespace osu.Game.Online.Multiplayer return tcs.Task; } + private void handleRoomRequest(Action request) + { + Scheduler.Add(() => + { + if (Room == null) + { + pendingRequests.Enqueue(request); + return; + } + + request(); + }); + } + Task IStatefulUserHubClient.DisconnectRequested() { Schedule(() => From 9ae6e509b73ca265d1fac781a428b2e9a564fe2d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Aug 2025 13:47:55 +0900 Subject: [PATCH 024/267] Configure await calls --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 14bc0bad82..1dfa3c0cfb 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -313,14 +313,14 @@ namespace osu.Game.Online.Multiplayer { await scheduledReset.ConfigureAwait(false); await LeaveRoomInternal().ConfigureAwait(false); - }); + }).ConfigureAwait(false); } finally { await runOnUpdateThreadAsync(() => { pendingRequests.Clear(); - }); + }).ConfigureAwait(false); } }); } From 311c75aa533979521981aa5ed70494d3c57a4f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 10:26:31 +0200 Subject: [PATCH 025/267] Adjust test after allowing grouping modes to split beatmap sets apart --- .../BeatmapCarouselFilterGroupingTest.cs | 107 +++++++++--------- 1 file changed, 54 insertions(+), 53 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index c8f1c1e017..592994f2f0 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -74,11 +74,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyBeatmap('_'), beatmapSets, out var underscoreBeatmap); var results = await runGrouping(mode, beatmapSets); - assertGroup(results, 0, "0-9", new[] { fiveBeatmap, fourBeatmap }, ref total); - assertGroup(results, 1, "A", new[] { aBeatmap }, ref total); - assertGroup(results, 2, "F", new[] { fBeatmap }, ref total); - assertGroup(results, 3, "Z", new[] { zBeatmap }, ref total); - assertGroup(results, 4, "Other", new[] { dashBeatmap, underscoreBeatmap }, ref total); + assertGroup(results, 0, "0-9", fiveBeatmap.Beatmaps.Concat(fourBeatmap.Beatmaps), ref total); + assertGroup(results, 1, "A", aBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "F", fBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Z", zBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "Other", dashBeatmap.Beatmaps.Concat(underscoreBeatmap.Beatmaps), ref total); assertTotal(results, total); } @@ -115,12 +115,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddMonths(-2).AddDays(-3), beatmapSets, out var twoMonthsAgoBeatmap); var results = await runGrouping(GroupMode.DateAdded, beatmapSets); - assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total); - assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total); - assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total); - assertGroup(results, 3, "Last month", new[] { lastMonthBeatmap }, ref total); - assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total); - assertGroup(results, 5, "2 months ago", new[] { twoMonthsAgoBeatmap }, ref total); + assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "2 months ago", twoMonthsAgoBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -139,13 +139,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyLastPlayed(null), beatmapSets, out var neverBeatmap); var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); - assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total); - assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total); - assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total); - assertGroup(results, 3, "Last month", new[] { lastMonthBeatmap }, ref total); - assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total); - assertGroup(results, 5, "2 months ago", new[] { twoMonthsBeatmap }, ref total); - assertGroup(results, 6, "Never", new[] { neverBeatmap }, ref total); + assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "2 months ago", twoMonthsBeatmap.Beatmaps, ref total); + assertGroup(results, 6, "Never", neverBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -162,7 +162,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); int total = 0; - assertGroup(results, 0, "Today", new[] { set }, ref total); + assertGroup(results, 0, "Today", [set.Beatmaps[2]], ref total); + assertGroup(results, 1, "Never", [set.Beatmaps[0], set.Beatmaps[1]], ref total); assertTotal(results, total); } @@ -176,8 +177,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); int total = 0; - assertGroup(results, 0, "Over 5 months ago", new[] { overFiveMonthsBeatmap }, ref total); - assertGroup(results, 1, "Never", new[] { neverBeatmap }, ref total); + assertGroup(results, 0, "Over 5 months ago", overFiveMonthsBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Never", neverBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -207,14 +208,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.Status = BeatmapOnlineStatus.LocallyModified, beatmapSets, out var localBeatmap); var results = await runGrouping(GroupMode.RankedStatus, beatmapSets); - assertGroup(results, 0, "Ranked", new[] { rankedBeatmap, approvedBeatmap }, ref total); - assertGroup(results, 1, "Qualified", new[] { qualifiedBeatmap }, ref total); - assertGroup(results, 2, "WIP", new[] { wipBeatmap }, ref total); - assertGroup(results, 3, "Pending", new[] { pendingBeatmap }, ref total); - assertGroup(results, 4, "Graveyard", new[] { graveyardBeatmap }, ref total); - assertGroup(results, 5, "Local", new[] { localBeatmap }, ref total); - assertGroup(results, 6, "Unknown", new[] { noneBeatmap }, ref total); - assertGroup(results, 7, "Loved", new[] { lovedBeatmap }, ref total); + assertGroup(results, 0, "Ranked", rankedBeatmap.Beatmaps.Concat(approvedBeatmap.Beatmaps), ref total); + assertGroup(results, 1, "Qualified", qualifiedBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "WIP", wipBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Pending", pendingBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "Graveyard", graveyardBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "Local", localBeatmap.Beatmaps, ref total); + assertGroup(results, 6, "Unknown", noneBeatmap.Beatmaps, ref total); + assertGroup(results, 7, "Loved", lovedBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -240,12 +241,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyBPM(330), beatmapSets, out var beatmap330); var results = await runGrouping(GroupMode.BPM, beatmapSets); - assertGroup(results, 0, "Under 60 BPM", new[] { beatmap30 }, ref total); - assertGroup(results, 1, "60 - 70 BPM", new[] { beatmap59, beatmap60 }, ref total); - assertGroup(results, 2, "90 - 100 BPM", new[] { beatmap90, beatmap95 }, ref total); - assertGroup(results, 3, "270 - 280 BPM", new[] { beatmap269, beatmap270 }, ref total); - assertGroup(results, 4, "290 - 300 BPM", new[] { beatmap299 }, ref total); - assertGroup(results, 5, "Over 300 BPM", new[] { beatmap300, beatmap330 }, ref total); + assertGroup(results, 0, "Under 60 BPM", beatmap30.Beatmaps, ref total); + assertGroup(results, 1, "60 - 70 BPM", (beatmap59.Beatmaps.Concat(beatmap60.Beatmaps)), ref total); + assertGroup(results, 2, "90 - 100 BPM", (beatmap90.Beatmaps.Concat(beatmap95.Beatmaps)), ref total); + assertGroup(results, 3, "270 - 280 BPM", (beatmap269.Beatmaps.Concat(beatmap270.Beatmaps)), ref total); + assertGroup(results, 4, "290 - 300 BPM", beatmap299.Beatmaps, ref total); + assertGroup(results, 5, "Over 300 BPM", (beatmap300.Beatmaps.Concat(beatmap330.Beatmaps)), ref total); assertTotal(results, total); } @@ -272,10 +273,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyStars(7), beatmapSets, out var beatmap7); var results = await runGrouping(GroupMode.Difficulty, beatmapSets); - assertGroup(results, 0, "Below 1 Star", new[] { beatmapBelow1 }, ref total); - assertGroup(results, 1, "1 Star", new[] { beatmapAbove1, beatmapAlmost2 }, ref total); - assertGroup(results, 2, "2 Stars", new[] { beatmap2, beatmapAbove2 }, ref total); - assertGroup(results, 3, "7 Stars", new[] { beatmap7 }, ref total); + assertGroup(results, 0, "Below 1 Star", beatmapBelow1.Beatmaps, ref total); + assertGroup(results, 1, "1 Star", (beatmapAbove1.Beatmaps.Concat(beatmapAlmost2.Beatmaps)), ref total); + assertGroup(results, 2, "2 Stars", (beatmap2.Beatmaps.Concat(beatmapAbove2.Beatmaps)), ref total); + assertGroup(results, 3, "7 Stars", beatmap7.Beatmaps, ref total); assertTotal(results, total); } @@ -304,11 +305,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(applyLength(630_000), beatmapSets, out var beatmap10Min30Sec); var results = await runGrouping(GroupMode.Length, beatmapSets); - assertGroup(results, 0, "1 minute or less", new[] { beatmap30Sec, beatmap1Min }, ref total); - assertGroup(results, 1, "2 minutes or less", new[] { beatmap1Min30Sec, beatmap2Min }, ref total); - assertGroup(results, 2, "5 minutes or less", new[] { beatmap5Min }, ref total); - assertGroup(results, 3, "10 minutes or less", new[] { beatmap6Min, beatmap10Min }, ref total); - assertGroup(results, 4, "Over 10 minutes", new[] { beatmap10Min30Sec }, ref total); + assertGroup(results, 0, "1 minute or less", (beatmap30Sec.Beatmaps.Concat(beatmap1Min.Beatmaps)), ref total); + assertGroup(results, 1, "2 minutes or less", (beatmap1Min30Sec.Beatmaps.Concat(beatmap2Min.Beatmaps)), ref total); + assertGroup(results, 2, "5 minutes or less", beatmap5Min.Beatmaps, ref total); + assertGroup(results, 3, "10 minutes or less", (beatmap6Min.Beatmaps.Concat(beatmap10Min.Beatmaps)), ref total); + assertGroup(results, 4, "Over 10 minutes", beatmap10Min30Sec.Beatmaps, ref total); assertTotal(results, total); } @@ -334,10 +335,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.DateRanked = null, beatmapSets, out var beatmapUnranked); var results = await runGrouping(GroupMode.DateRanked, beatmapSets); - assertGroup(results, 0, "2025", new[] { beatmap2025 }, ref total); - assertGroup(results, 1, "2010", new[] { beatmap2010 }, ref total); - assertGroup(results, 2, "2007", new[] { beatmapOct2007, beatmapDec2007 }, ref total); - assertGroup(results, 3, "Unranked", new[] { beatmapUnranked }, ref total); + assertGroup(results, 0, "2025", beatmap2025.Beatmaps, ref total); + assertGroup(results, 1, "2010", beatmap2010.Beatmaps, ref total); + assertGroup(results, 2, "2007", (beatmapOct2007.Beatmaps.Concat(beatmapDec2007.Beatmaps)), ref total); + assertGroup(results, 3, "Unranked", beatmapUnranked.Beatmaps, ref total); assertTotal(results, total); } @@ -357,9 +358,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced); var results = await runGrouping(GroupMode.Source, beatmapSets); - assertGroup(results, 0, "Cool Game", new[] { beatmapCoolGame, beatmapCoolGameB }, ref total); - assertGroup(results, 1, "Nice Movie", new[] { beatmapNiceMovie }, ref total); - assertGroup(results, 2, "Unsourced", new[] { beatmapUnsourced }, ref total); + assertGroup(results, 0, "Cool Game", (beatmapCoolGame.Beatmaps.Concat(beatmapCoolGameB.Beatmaps)), ref total); + assertGroup(results, 1, "Nice Movie", beatmapNiceMovie.Beatmaps, ref total); + assertGroup(results, 2, "Unsourced", beatmapUnsourced.Beatmaps, ref total); assertTotal(results, total); } @@ -375,7 +376,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); } - private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmapSets, ref int totalItems) + private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmaps, ref int totalItems) { var groupItem = items.Where(i => i.Model is GroupDefinition).ElementAtOrDefault(index); @@ -390,7 +391,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var groupModel = (GroupDefinition)groupItem.Model; Assert.That(groupModel.Title, Is.EqualTo(expectedTitle)); - Assert.That(itemsInGroup.Select(i => i.Model).OfType(), Is.EquivalentTo(expectedBeatmapSets.SelectMany(bs => bs.Beatmaps))); + Assert.That(itemsInGroup.Select(i => i.Model).OfType(), Is.EquivalentTo(expectedBeatmaps)); totalItems += itemsInGroup.Count() + 1; } From 47164c61b4889a8e1af7c871ffb7c3b751ed425d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Aug 2025 14:45:32 +0200 Subject: [PATCH 026/267] Add failing test coverage of splitting beatmap sets apart --- .../TestSceneBeatmapCarouselSetsSplitApart.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs new file mode 100644 index 0000000000..fa635f9bde --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs @@ -0,0 +1,120 @@ +// 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 System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselSetsSplitApart : BeatmapCarouselTestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + + SortAndGroupBy(SortMode.Title, GroupMode.Length); + } + + [Test] + public void TestSetTraversal() + { + AddBeatmaps(3, splitApart: true); + AddBeatmaps(3, splitApart: false); + WaitForDrawablePanels(); + + SelectNextSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectNextSet(); + WaitForSetSelection(set: 1, diff: 0); + + SelectPrevSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectPrevSet(); + WaitForSetSelection(set: 5, diff: 0); + + SelectPrevSet(); + SelectPrevSet(); + SelectPrevSet(); + WaitForSetSelection(set: 2, diff: 4); + AddAssert("only two beatmap panels visible", () => GetVisiblePanels().Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestBeatmapTraversal() + { + AddBeatmaps(3, splitApart: true); + AddBeatmaps(3, splitApart: false); + WaitForDrawablePanels(); + + SelectNextSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectNextPanel(); + WaitForSetSelection(set: 0, diff: 1); + + SelectNextPanel(); // header of set 1 in group 0 + Select(); + WaitForSetSelection(set: 1, diff: 0); + + SelectPrevPanel(); // header of set 1 in group 0 + SelectPrevPanel(); // header of set 0 in group 0 + Select(); + WaitForSetSelection(set: 0, diff: 0); + + SelectPrevPanel(); // header of set 0 in group 0 + SelectPrevPanel(); // header of group 0 + SelectPrevPanel(); // header of group 2 + Select(); + SelectNextPanel(); // header of set 0 in group 2 + Select(); + WaitForSetSelection(set: 0, diff: 4); + } + + [Test] + public void TestRandomStaysInGroup() + { + AddBeatmaps(2, splitApart: false); + AddBeatmaps(1, splitApart: true); + WaitForDrawablePanels(); + + SelectPrevSet(); + SelectPrevSet(); + WaitForSetSelection(set: 1); + WaitForExpandedGroup(2); + + AddStep("select next random", () => Carousel.NextRandom()); + WaitForExpandedGroup(2); + AddStep("select next random", () => Carousel.NextRandom()); + WaitForExpandedGroup(2); + } + + protected void AddBeatmaps(int count, bool splitApart) => AddStep($"add {count} beatmaps ({(splitApart ? "" : "not ")}split apart)", () => + { + var beatmapSets = new List(); + + for (int i = 0; i < count; i++) + { + var beatmapSet = CreateTestBeatmapSetInfo(6, false); + + for (int j = 0; j < beatmapSet.Beatmaps.Count; j++) + { + beatmapSet.Beatmaps[j].Length = splitApart ? 30_000 * (j + 1) : 180_000; + } + + beatmapSets.Add(beatmapSet); + } + + BeatmapSets.AddRange(beatmapSets); + }); + } +} From 7e109add9618bf3a59d47e999fa864fc23ed11a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Aug 2025 19:10:20 +0900 Subject: [PATCH 027/267] Ensure filtering also runs after local gameplay `LastPlayed` changes --- .../SongSelectV2/TestSceneSongSelect.cs | 24 +++++++++++++++++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 69bdb97617..895f148965 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -23,7 +23,9 @@ using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; using osuTK.Input; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods; using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions; using FooterButtonRandom = osu.Game.Screens.SelectV2.FooterButtonRandom; @@ -302,6 +304,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + /// + /// Last played and rank achieved may have changed, so we want to make sure filtering runs on resume to song select. + /// + [Test] + public void TestFilteringRunsAfterReturningFromGameplay() + { + AddStep("import actual beatmap", () => Beatmaps.Import(TestResources.GetQuickTestBeatmapForImport())); + LoadSongSelect(); + + AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType().Single().FilterCount, () => Is.EqualTo(1)); + + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); + AddUntilStep("wait for fail", () => ((Player)Stack.CurrentScreen).GameplayState.HasFailed); + + AddStep("exit gameplay", () => InputManager.Key(Key.Escape)); + AddStep("exit gameplay", () => InputManager.Key(Key.Escape)); + + AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType().Single().FilterCount, () => Is.EqualTo(2)); + } + [Test] public void TestAutoplayShortcut() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 5b0ad1ae29..a4e957a1bf 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -392,7 +392,9 @@ namespace osu.Game.Screens.SelectV2 // displayed on panel oldBeatmap.DifficultyName == newBeatmap.DifficultyName && // hidden changed, needs re-filter - oldBeatmap.Hidden == newBeatmap.Hidden; + oldBeatmap.Hidden == newBeatmap.Hidden && + // might be used for grouping, returning from gameplay + oldBeatmap.LastPlayed == newBeatmap.LastPlayed; if (equalForDisplayPurposes) return false; From 8dd131f17ee57e8cbe005b8a8abb8e0ff3a0c4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 09:40:22 +0200 Subject: [PATCH 028/267] Support beatmap sets being split apart by the active group mode in beatmap carousel --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 70 +++++++++++-------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 15 ++-- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 19 +++-- 4 files changed, 60 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index bc507fbffa..64084d76f1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo; public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet; - public new BeatmapSetInfo? ExpandedBeatmapSet => base.ExpandedBeatmapSet; + public new BeatmapSetUnderGrouping? ExpandedBeatmapSet => base.ExpandedBeatmapSet; public new GroupDefinition? ExpandedGroup => base.ExpandedGroup; public TestBeatmapCarousel() diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index da841aa361..95fb26c6dd 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -69,11 +70,11 @@ namespace osu.Game.Screens.SelectV2 if (grouping.BeatmapSetsGroupedTogether) { // Give some space around the expanded beatmap set, at the top.. - if (bottom.Model is BeatmapSetInfo && bottom.IsExpanded) + if (bottom.Model is BeatmapSetUnderGrouping && bottom.IsExpanded) return SPACING * 2; // ..and the bottom. - if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetInfo) + if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetUnderGrouping) return SPACING * 2; // Beatmap difficulty panels do not overlap with themselves or any other panel. @@ -206,12 +207,12 @@ namespace osu.Game.Screens.SelectV2 return true; } - if (item.Model is BeatmapSetInfo beatmapSetInfo) + if (item.Model is BeatmapSetUnderGrouping setUnderGrouping) { - if (oldItems.Contains(beatmapSetInfo)) + if (oldItems.Contains(setUnderGrouping.BeatmapSet)) return false; - RequestRecommendedSelection(beatmapSetInfo.Beatmaps); + RequestRecommendedSelection(setUnderGrouping.BeatmapSet.Beatmaps); return true; } } @@ -282,7 +283,7 @@ namespace osu.Game.Screens.SelectV2 protected GroupDefinition? ExpandedGroup { get; private set; } - protected BeatmapSetInfo? ExpandedBeatmapSet { get; private set; } + protected BeatmapSetUnderGrouping? ExpandedBeatmapSet { get; private set; } protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo; @@ -310,8 +311,8 @@ namespace osu.Game.Screens.SelectV2 return; - case BeatmapSetInfo setInfo: - selectRecommendedDifficultyForBeatmapSet(setInfo); + case BeatmapSetUnderGrouping setUnderGrouping: + selectRecommendedDifficultyForBeatmapSet(setUnderGrouping); return; case BeatmapInfo beatmapInfo: @@ -337,7 +338,7 @@ namespace osu.Game.Screens.SelectV2 switch (model) { - case BeatmapSetInfo: + case BeatmapSetUnderGrouping: case GroupDefinition: throw new InvalidOperationException("Groups should never become selected"); @@ -348,7 +349,7 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(containingGroup); if (grouping.BeatmapSetsGroupedTogether) - setExpandedSet(beatmapInfo); + setExpandedSet(new BeatmapSetUnderGrouping(containingGroup, beatmapInfo.BeatmapSet!)); break; } } @@ -372,10 +373,10 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(groupForReselection); } - private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetInfo beatmapSet) + private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetUnderGrouping setUnderGrouping) { // Selecting a set isn't valid – let's re-select the first visible difficulty. - if (grouping.SetItems.TryGetValue(beatmapSet, out var items)) + if (grouping.SetItems.TryGetValue(setUnderGrouping, out var items)) { var beatmaps = items.Select(i => i.Model).OfType(); RequestRecommendedSelection(beatmaps); @@ -423,7 +424,7 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { - case BeatmapSetInfo: + case BeatmapSetUnderGrouping: return true; case BeatmapInfo: @@ -462,11 +463,11 @@ namespace osu.Game.Screens.SelectV2 i.IsExpanded = true; break; - case BeatmapSetInfo set: + case BeatmapSetUnderGrouping setUnderGrouping: // Case where there are set headers, header should be visible // and items should use the set's expanded state. i.IsVisible = true; - setExpansionStateOfSetItems(set, i.IsExpanded); + setExpansionStateOfSetItems(setUnderGrouping, i.IsExpanded); break; default: @@ -496,21 +497,21 @@ namespace osu.Game.Screens.SelectV2 } } - private void setExpandedSet(BeatmapInfo beatmapInfo) + private void setExpandedSet(BeatmapSetUnderGrouping setUnderGrouping) { if (ExpandedBeatmapSet != null) setExpansionStateOfSetItems(ExpandedBeatmapSet, false); - ExpandedBeatmapSet = beatmapInfo.BeatmapSet!; + ExpandedBeatmapSet = setUnderGrouping; setExpansionStateOfSetItems(ExpandedBeatmapSet, true); } - private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded) + private void setExpansionStateOfSetItems(BeatmapSetUnderGrouping set, bool expanded) { if (grouping.SetItems.TryGetValue(set, out var items)) { foreach (var i in items) { - if (i.Model is BeatmapSetInfo) + if (i.Model is BeatmapSetUnderGrouping) i.IsExpanded = expanded; else i.IsVisible = expanded; @@ -548,7 +549,7 @@ namespace osu.Game.Screens.SelectV2 sampleToggleGroup?.Play(); return; - case BeatmapSetInfo: + case BeatmapSetUnderGrouping: sampleChangeSet?.Play(); return; @@ -687,8 +688,8 @@ namespace osu.Game.Screens.SelectV2 // it is doing a Replace operation on the list. If it is, then check the local handling in beatmapSetsChanged // before changing matching requirements here. - if (x is BeatmapSetInfo beatmapSetX && y is BeatmapSetInfo beatmapSetY) - return beatmapSetX.Equals(beatmapSetY); + if (x is BeatmapSetUnderGrouping setUnderGroupingX && y is BeatmapSetUnderGrouping setUnderGroupingY) + return setUnderGroupingX.Equals(setUnderGroupingY); if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY) return beatmapX.Equals(beatmapY); @@ -718,7 +719,7 @@ namespace osu.Game.Screens.SelectV2 return beatmapPanelPool.Get(); - case BeatmapSetInfo: + case BeatmapSetUnderGrouping: return setPanelPool.Get(); } @@ -828,30 +829,31 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomSet() { - ICollection visibleSets = ExpandedGroup != null + ICollection visibleSetsUnderGrouping = ExpandedGroup != null // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // // If this becomes an issue, we could either store a mapping, or run the random algorithm many times // using the `SetItems` method until we get a group HIT. - ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() // This is the fastest way to retrieve sets for randomisation. : grouping.SetItems.Keys; - BeatmapSetInfo set; + BeatmapSetUnderGrouping set; switch (randomAlgorithm.Value) { case RandomSelectAlgorithm.RandomPermutation: { - ICollection notYetVisitedSets = visibleSets.Except(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!)).ToList(); + ICollection notYetVisitedSets = + visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), setUnderGrouping => setUnderGrouping.BeatmapSet).ToList(); if (!notYetVisitedSets.Any()) { - previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSets.Contains(b.BeatmapSet!)); - notYetVisitedSets = visibleSets; + previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(setUnderGrouping => setUnderGrouping.BeatmapSet.Equals(b.BeatmapSet!))); + notYetVisitedSets = visibleSetsUnderGrouping; if (CurrentSelection is BeatmapInfo beatmapInfo) - notYetVisitedSets = notYetVisitedSets.Except([beatmapInfo.BeatmapSet!]).ToList(); + notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], setUnderGrouping => setUnderGrouping.BeatmapSet).ToList(); } if (notYetVisitedSets.Count == 0) @@ -862,7 +864,7 @@ namespace osu.Game.Screens.SelectV2 } case RandomSelectAlgorithm.Random: - set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); + set = visibleSetsUnderGrouping.ElementAt(RNG.Next(visibleSetsUnderGrouping.Count)); break; default: @@ -959,4 +961,10 @@ namespace osu.Game.Screens.SelectV2 /// Defines a grouping header for a set of carousel items grouped by star difficulty. /// public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); + + /// + /// Used to represent a portion of a under a . + /// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it. + /// + public record BeatmapSetUnderGrouping([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index f0ec3ae3ab..63bc94b087 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -29,14 +29,14 @@ namespace osu.Game.Screens.SelectV2 /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// - public IDictionary> SetItems => setMap; + public IDictionary> SetItems => setMap; /// /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. /// public IDictionary> GroupItems => groupMap; - private Dictionary> setMap = new Dictionary>(); + private Dictionary> setMap = new Dictionary>(); private Dictionary> groupMap = new Dictionary>(); private readonly Func getCriteria; @@ -56,7 +56,7 @@ namespace osu.Game.Screens.SelectV2 return await Task.Run(() => { // preallocate space for the new mappings using last known estimates - var newSetMap = new Dictionary>(setMap.Count); + var newSetMap = new Dictionary>(setMap.Count); var newGroupMap = new Dictionary>(groupMap.Count); var criteria = getCriteria(); @@ -94,11 +94,12 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)item.Model; bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; + var beatmapSetUnderGrouping = new BeatmapSetUnderGrouping(group, beatmap.BeatmapSet!); if (newBeatmapSet) { - if (!newSetMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) - newSetMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + if (!newSetMap.TryGetValue(beatmapSetUnderGrouping, out currentSetItems)) + newSetMap[beatmapSetUnderGrouping] = currentSetItems = new HashSet(); } if (BeatmapSetsGroupedTogether) @@ -108,7 +109,7 @@ namespace osu.Game.Screens.SelectV2 if (groupItem != null) groupItem.NestedItemCount++; - addItem(new CarouselItem(beatmap.BeatmapSet!) + addItem(new CarouselItem(beatmapSetUnderGrouping) { DrawHeight = PanelBeatmapSet.HEIGHT, DepthLayer = -1 @@ -135,7 +136,7 @@ namespace osu.Game.Screens.SelectV2 currentGroupItems?.Add(i); currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || !BeatmapSetsGroupedTogether)); + i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetUnderGrouping || !BeatmapSetsGroupedTogether)); } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index d776ab1ffb..7b07076975 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -67,6 +67,15 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable ruleset { get; set; } = null!; + private BeatmapSetUnderGrouping beatmapSetUnderGrouping + { + get + { + Debug.Assert(Item != null); + return (BeatmapSetUnderGrouping)Item!.Model; + } + } + public PanelBeatmapSet() { PanelXOffset = 20f; @@ -179,9 +188,7 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - Debug.Assert(Item != null); - - var beatmapSet = (BeatmapSetInfo)Item.Model; + var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). setBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); @@ -215,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return Array.Empty(); - var beatmapSet = (BeatmapSetInfo)Item.Model; + var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; List items = new List(); @@ -268,9 +275,7 @@ namespace osu.Game.Screens.SelectV2 private MenuItem createCollectionMenuItem(BeatmapCollection collection) { - var beatmapSet = (BeatmapSetInfo)Item!.Model; - - Debug.Assert(beatmapSet != null); + var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; TernaryState state; From 6ba72fa481462932cf770781a2ac28263fd3e4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 10:35:31 +0200 Subject: [PATCH 029/267] Adjust tests to new beatmap set model usage in carousel --- .../Visual/Navigation/TestScenePresentBeatmap.cs | 2 +- .../BeatmapCarouselFilterGroupingTest.cs | 2 +- .../SongSelectV2/BeatmapCarouselTestScene.cs | 2 +- .../TestSceneBeatmapCarouselFiltering.cs | 16 ++++++++++++---- .../Visual/SongSelectV2/TestScenePanelSet.cs | 8 ++++---- .../SongSelectV2/TestSceneSongSelectGrouping.cs | 2 +- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index e7172cacbf..6092bdde3a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("beatmap in song select", () => { var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen; - return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetInfo bsi && bsi.MatchesOnlineID(getImport())); + return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetUnderGrouping bsug && bsug.BeatmapSet.MatchesOnlineID(getImport())); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 592994f2f0..efd4eb7b03 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ]; var results = await runGrouping(GroupMode.None, beatmapSets); - Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(beatmapSets)); + Assert.That(results.Select(r => r.Model).OfType().Select(setUnderGrouping => setUnderGrouping.BeatmapSet), Is.EquivalentTo(beatmapSets)); Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(allBeatmaps)); assertTotal(results, beatmapSets.Count + allBeatmaps.Length); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 64084d76f1..2664062fc2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Using groupingFilter.SetItems.Count alone doesn't work. // When sorting by difficulty, there can be more than one set panel for the same set displayed. - return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo)); + return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetUnderGrouping)); }, () => Is.EqualTo(expected)); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 78c12e2730..d599c07f27 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -396,7 +396,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectNextPanel(); - AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); + AddAssert("keyboard selected is first set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.First())); } [Test] @@ -413,7 +415,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectPrevPanel(); - AddAssert("keyboard selected is last set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); + AddAssert("keyboard selected is last set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.Last())); } [Test] @@ -428,7 +432,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); SelectPrevPanel(); - AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); + AddAssert("keyboard selected is first set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.First())); } [Test] @@ -444,7 +450,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Single result is automatically selected for us, so we iterate once backwards to the set header. SelectPrevPanel(); - AddAssert("keyboard selected is second set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); + AddAssert("keyboard selected is second set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.Last())); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs index 1723185b1f..6a212381a8 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs @@ -75,21 +75,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet) + Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)) }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), KeyboardSelected = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), Expanded = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), KeyboardSelected = { Value = true }, Expanded = { Value = true } }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs index 0f7c42946d..be7f705532 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("no-collection group present", () => { var group = grouping.GroupItems.Single(g => g.Key.Title == "Not in collection"); - return group.Value.Select(i => i.Model).OfType().Single().Equals(beatmapSet); + return group.Value.Select(i => i.Model).OfType().Single().BeatmapSet.Equals(beatmapSet); }); AddStep("add beatmap to collection", () => From bb9f9e4d358461d471c682a6af1d83e028523a41 Mon Sep 17 00:00:00 2001 From: marvin Date: Thu, 28 Aug 2025 23:34:22 +0200 Subject: [PATCH 030/267] Fix operations in PooledDrawableWithLifetimeContainer.CheckChildrenLife being in wrong order Previously CompositeDrawable.CheckChildrenLife() would be run before lifetimeManager.Update() which lead to the new drawables being inserted into the container but not being made alive immediately, leading to the drawable not becoming visibile until the next update loop. --- .../Objects/Pooling/PooledDrawableWithLifetimeContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index efc10f26e1..e01df1428c 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -160,8 +160,8 @@ namespace osu.Game.Rulesets.Objects.Pooling if (!IsPresent) return false; - bool aliveChanged = base.CheckChildrenLife(); - aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + bool aliveChanged = lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + aliveChanged |= base.CheckChildrenLife(); return aliveChanged; } } From f2f5cf19a286821e46ff609f1394c66a485d879e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 14:08:06 +0900 Subject: [PATCH 031/267] Return early to avoid creating mod description strings unnecessarily --- osu.Game/Rulesets/Mods/Mod.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 727db913e2..628098c5b6 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -56,6 +56,9 @@ namespace osu.Game.Rulesets.Mods { var bindable = (IBindable)property.GetValue(this)!; + if (!bindable.IsDefault) + continue; + string valueText; switch (bindable) @@ -69,8 +72,7 @@ namespace osu.Game.Rulesets.Mods break; } - if (!bindable.IsDefault) - yield return (attr.Label, valueText); + yield return (attr.Label, valueText); } } } From e83f3d5e778397b5fbb6fb778209afb70521e70b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 14:08:18 +0900 Subject: [PATCH 032/267] Fix some mods showing tooltips when settings are default --- osu.Game/Rulesets/Mods/ModBarrelRoll.cs | 9 ++++++--- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 22d2f41b82..98a7999065 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -46,8 +46,10 @@ namespace osu.Game.Rulesets.Mods { get { - yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); - yield return ("Direction", Direction.Value.GetDescription()); + if (!SpinSpeed.IsDefault) + yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); + if (!Direction.IsDefault) + yield return ("Direction", Direction.Value.GetDescription()); } } @@ -55,7 +57,8 @@ namespace osu.Game.Rulesets.Mods public virtual void Update(Playfield playfield) { - playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); + playfieldAdjustmentContainer.Rotation = + CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 8dfe8444e8..049b8f9b7f 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Mods { get { - yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); + if (!InitialRate.IsDefault || !FinalRate.IsDefault) + yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); if (!AdjustPitch.IsDefault) yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off"); From 12832e9fef04daf0e87e6ec9cada56d0d056bfbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 14:35:42 +0900 Subject: [PATCH 033/267] Use switches for warmup/chat toggles in tournament interface As proposed in https://github.com/ppy/osu/discussions/32515. --- .../Screens/Gameplay/GameplayScreen.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index b2152eaf3d..2cf7ce1961 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; -using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays.Settings; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; @@ -24,7 +24,6 @@ namespace osu.Game.Tournament.Screens.Gameplay private readonly BindableBool warmup = new BindableBool(); public readonly Bindable State = new Bindable(); - private OsuButton warmupButton = null!; private MatchIPCInfo ipc = null!; [Resolved] @@ -40,6 +39,8 @@ namespace osu.Game.Tournament.Screens.Gameplay { this.ipc = ipc; + LabelledSwitchButton chatToggle; + AddRangeInternal(new Drawable[] { new TourneyVideo("gameplay") @@ -95,17 +96,14 @@ namespace osu.Game.Tournament.Screens.Gameplay { Children = new Drawable[] { - warmupButton = new TourneyButton + new LabelledSwitchButton { - RelativeSizeAxes = Axes.X, - Text = "Toggle warmup", - Action = () => warmup.Toggle() + Label = "Warmup", + Current = warmup, }, - new TourneyButton + chatToggle = new LabelledSwitchButton { - RelativeSizeAxes = Axes.X, - Text = "Toggle chat", - Action = () => { State.Value = State.Value == TourneyState.Idle ? TourneyState.Playing : TourneyState.Idle; } + Label = "Show chat", }, new SettingsSlider { @@ -123,13 +121,12 @@ namespace osu.Game.Tournament.Screens.Gameplay } }); + State.BindValueChanged(state => chatToggle.Current.Value = State.Value == TourneyState.Idle, true); + chatToggle.Current.BindValueChanged(v => State.Value = v.NewValue ? TourneyState.Idle : TourneyState.Playing); + LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); - warmup.BindValueChanged(w => - { - warmupButton.Alpha = !w.NewValue ? 0.5f : 1; - header.ShowScores = !w.NewValue; - }, true); + warmup.BindValueChanged(w => header.ShowScores = !w.NewValue, true); } protected override void LoadComplete() From 9e77a5b0507c7d71fad374c9d59169dbe0ece269 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 16:01:49 +0900 Subject: [PATCH 034/267] Fix obviously incorrect conditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 628098c5b6..477372b97d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mods { var bindable = (IBindable)property.GetValue(this)!; - if (!bindable.IsDefault) + if (bindable.IsDefault) continue; string valueText; From df6d6edaca6e69736ddd52ff78cf026717aae935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 09:29:47 +0200 Subject: [PATCH 035/267] Fix song select not performing online lookup on re-enter Closes https://github.com/ppy/osu/issues/34825. Root cause is https://github.com/ppy/osu/blob/24ec43b3b65fa3b164b7713341cd62b1e0dacc2e/osu.Game/Screens/SelectV2/SongSelect.cs#L345-L356 not specifying `(..., true)`, therefore the fetch doesn't happen on enter if song select doesn't change the global beatmap as a side effect of the enter, which is the case on re-entering. --- osu.Game/Screens/SelectV2/SongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 947b8f9c7c..ef00064ced 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -653,6 +653,7 @@ namespace osu.Game.Screens.SelectV2 ensurePlayingSelected(); updateBackgroundDim(); + fetchOnlineInfo(); } private void onLeavingScreen() From 526ee32268fd74a65ebe42fe53bcdb9cfe32fe12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 09:54:42 +0200 Subject: [PATCH 036/267] Apply suggested rename --- .../Navigation/TestScenePresentBeatmap.cs | 2 +- .../BeatmapCarouselFilterGroupingTest.cs | 2 +- .../SongSelectV2/BeatmapCarouselTestScene.cs | 4 +- .../TestSceneBeatmapCarouselFiltering.cs | 8 +-- .../Visual/SongSelectV2/TestScenePanelSet.cs | 8 +-- .../TestSceneSongSelectGrouping.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 62 +++++++++---------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 16 ++--- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 10 +-- 9 files changed, 57 insertions(+), 57 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 6092bdde3a..1dd39e5bf9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("beatmap in song select", () => { var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen; - return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetUnderGrouping bsug && bsug.BeatmapSet.MatchesOnlineID(getImport())); + return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is GroupedBeatmapSet gbs && gbs.BeatmapSet.MatchesOnlineID(getImport())); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index efd4eb7b03..32a7b89424 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ]; var results = await runGrouping(GroupMode.None, beatmapSets); - Assert.That(results.Select(r => r.Model).OfType().Select(setUnderGrouping => setUnderGrouping.BeatmapSet), Is.EquivalentTo(beatmapSets)); + Assert.That(results.Select(r => r.Model).OfType().Select(groupedSet => groupedSet.BeatmapSet), Is.EquivalentTo(beatmapSets)); Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(allBeatmaps)); assertTotal(results, beatmapSets.Count + allBeatmaps.Length); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 2664062fc2..f18e1e9b52 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Using groupingFilter.SetItems.Count alone doesn't work. // When sorting by difficulty, there can be more than one set panel for the same set displayed. - return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetUnderGrouping)); + return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is GroupedBeatmapSet)); }, () => Is.EqualTo(expected)); } @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo; public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet; - public new BeatmapSetUnderGrouping? ExpandedBeatmapSet => base.ExpandedBeatmapSet; + public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet; public new GroupDefinition? ExpandedGroup => base.ExpandedGroup; public TestBeatmapCarousel() diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index d599c07f27..687c4c23be 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -397,7 +397,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); AddAssert("keyboard selected is first set", - () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, () => Is.EqualTo(BeatmapSets.First())); } @@ -416,7 +416,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectPrevPanel(); AddAssert("keyboard selected is last set", - () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, () => Is.EqualTo(BeatmapSets.Last())); } @@ -433,7 +433,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectPrevPanel(); AddAssert("keyboard selected is first set", - () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, () => Is.EqualTo(BeatmapSets.First())); } @@ -451,7 +451,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Single result is automatically selected for us, so we iterate once backwards to the set header. SelectPrevPanel(); AddAssert("keyboard selected is second set", - () => (GetKeyboardSelectedPanel()?.Item?.Model as BeatmapSetUnderGrouping)?.BeatmapSet, + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, () => Is.EqualTo(BeatmapSets.Last())); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs index 6a212381a8..b574262d55 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs @@ -75,21 +75,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelBeatmapSet { - Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)) + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)) }, new PanelBeatmapSet { - Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), KeyboardSelected = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), Expanded = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(new BeatmapSetUnderGrouping(null, beatmapSet)), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), KeyboardSelected = { Value = true }, Expanded = { Value = true } }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs index be7f705532..0772607a57 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("no-collection group present", () => { var group = grouping.GroupItems.Single(g => g.Key.Title == "Not in collection"); - return group.Value.Select(i => i.Model).OfType().Single().BeatmapSet.Equals(beatmapSet); + return group.Value.Select(i => i.Model).OfType().Single().BeatmapSet.Equals(beatmapSet); }); AddStep("add beatmap to collection", () => diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 95fb26c6dd..22079ea91f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -70,11 +70,11 @@ namespace osu.Game.Screens.SelectV2 if (grouping.BeatmapSetsGroupedTogether) { // Give some space around the expanded beatmap set, at the top.. - if (bottom.Model is BeatmapSetUnderGrouping && bottom.IsExpanded) + if (bottom.Model is GroupedBeatmapSet && bottom.IsExpanded) return SPACING * 2; // ..and the bottom. - if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetUnderGrouping) + if (top.Model is BeatmapInfo && bottom.Model is GroupedBeatmapSet) return SPACING * 2; // Beatmap difficulty panels do not overlap with themselves or any other panel. @@ -207,12 +207,12 @@ namespace osu.Game.Screens.SelectV2 return true; } - if (item.Model is BeatmapSetUnderGrouping setUnderGrouping) + if (item.Model is GroupedBeatmapSet groupedSet) { - if (oldItems.Contains(setUnderGrouping.BeatmapSet)) + if (oldItems.Contains(groupedSet.BeatmapSet)) return false; - RequestRecommendedSelection(setUnderGrouping.BeatmapSet.Beatmaps); + RequestRecommendedSelection(groupedSet.BeatmapSet.Beatmaps); return true; } } @@ -283,7 +283,7 @@ namespace osu.Game.Screens.SelectV2 protected GroupDefinition? ExpandedGroup { get; private set; } - protected BeatmapSetUnderGrouping? ExpandedBeatmapSet { get; private set; } + protected GroupedBeatmapSet? ExpandedBeatmapSet { get; private set; } protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo; @@ -311,8 +311,8 @@ namespace osu.Game.Screens.SelectV2 return; - case BeatmapSetUnderGrouping setUnderGrouping: - selectRecommendedDifficultyForBeatmapSet(setUnderGrouping); + case GroupedBeatmapSet groupedSet: + selectRecommendedDifficultyForBeatmapSet(groupedSet); return; case BeatmapInfo beatmapInfo: @@ -338,7 +338,7 @@ namespace osu.Game.Screens.SelectV2 switch (model) { - case BeatmapSetUnderGrouping: + case GroupedBeatmapSet: case GroupDefinition: throw new InvalidOperationException("Groups should never become selected"); @@ -349,7 +349,7 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(containingGroup); if (grouping.BeatmapSetsGroupedTogether) - setExpandedSet(new BeatmapSetUnderGrouping(containingGroup, beatmapInfo.BeatmapSet!)); + setExpandedSet(new GroupedBeatmapSet(containingGroup, beatmapInfo.BeatmapSet!)); break; } } @@ -373,10 +373,10 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(groupForReselection); } - private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetUnderGrouping setUnderGrouping) + private void selectRecommendedDifficultyForBeatmapSet(GroupedBeatmapSet set) { // Selecting a set isn't valid – let's re-select the first visible difficulty. - if (grouping.SetItems.TryGetValue(setUnderGrouping, out var items)) + if (grouping.SetItems.TryGetValue(set, out var items)) { var beatmaps = items.Select(i => i.Model).OfType(); RequestRecommendedSelection(beatmaps); @@ -424,7 +424,7 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { - case BeatmapSetUnderGrouping: + case GroupedBeatmapSet: return true; case BeatmapInfo: @@ -463,11 +463,11 @@ namespace osu.Game.Screens.SelectV2 i.IsExpanded = true; break; - case BeatmapSetUnderGrouping setUnderGrouping: + case GroupedBeatmapSet groupedSet: // Case where there are set headers, header should be visible // and items should use the set's expanded state. i.IsVisible = true; - setExpansionStateOfSetItems(setUnderGrouping, i.IsExpanded); + setExpansionStateOfSetItems(groupedSet, i.IsExpanded); break; default: @@ -497,21 +497,21 @@ namespace osu.Game.Screens.SelectV2 } } - private void setExpandedSet(BeatmapSetUnderGrouping setUnderGrouping) + private void setExpandedSet(GroupedBeatmapSet set) { if (ExpandedBeatmapSet != null) setExpansionStateOfSetItems(ExpandedBeatmapSet, false); - ExpandedBeatmapSet = setUnderGrouping; + ExpandedBeatmapSet = set; setExpansionStateOfSetItems(ExpandedBeatmapSet, true); } - private void setExpansionStateOfSetItems(BeatmapSetUnderGrouping set, bool expanded) + private void setExpansionStateOfSetItems(GroupedBeatmapSet set, bool expanded) { if (grouping.SetItems.TryGetValue(set, out var items)) { foreach (var i in items) { - if (i.Model is BeatmapSetUnderGrouping) + if (i.Model is GroupedBeatmapSet) i.IsExpanded = expanded; else i.IsVisible = expanded; @@ -549,7 +549,7 @@ namespace osu.Game.Screens.SelectV2 sampleToggleGroup?.Play(); return; - case BeatmapSetUnderGrouping: + case GroupedBeatmapSet: sampleChangeSet?.Play(); return; @@ -688,8 +688,8 @@ namespace osu.Game.Screens.SelectV2 // it is doing a Replace operation on the list. If it is, then check the local handling in beatmapSetsChanged // before changing matching requirements here. - if (x is BeatmapSetUnderGrouping setUnderGroupingX && y is BeatmapSetUnderGrouping setUnderGroupingY) - return setUnderGroupingX.Equals(setUnderGroupingY); + if (x is GroupedBeatmapSet groupedSetX && y is GroupedBeatmapSet groupedSetY) + return groupedSetX.Equals(groupedSetY); if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY) return beatmapX.Equals(beatmapY); @@ -719,7 +719,7 @@ namespace osu.Game.Screens.SelectV2 return beatmapPanelPool.Get(); - case BeatmapSetUnderGrouping: + case GroupedBeatmapSet: return setPanelPool.Get(); } @@ -829,31 +829,31 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomSet() { - ICollection visibleSetsUnderGrouping = ExpandedGroup != null + ICollection visibleSetsUnderGrouping = ExpandedGroup != null // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // // If this becomes an issue, we could either store a mapping, or run the random algorithm many times // using the `SetItems` method until we get a group HIT. - ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() // This is the fastest way to retrieve sets for randomisation. : grouping.SetItems.Keys; - BeatmapSetUnderGrouping set; + GroupedBeatmapSet set; switch (randomAlgorithm.Value) { case RandomSelectAlgorithm.RandomPermutation: { - ICollection notYetVisitedSets = - visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), setUnderGrouping => setUnderGrouping.BeatmapSet).ToList(); + ICollection notYetVisitedSets = + visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), groupedSet => groupedSet.BeatmapSet).ToList(); if (!notYetVisitedSets.Any()) { - previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(setUnderGrouping => setUnderGrouping.BeatmapSet.Equals(b.BeatmapSet!))); + previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(groupedSet => groupedSet.BeatmapSet.Equals(b.BeatmapSet!))); notYetVisitedSets = visibleSetsUnderGrouping; if (CurrentSelection is BeatmapInfo beatmapInfo) - notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], setUnderGrouping => setUnderGrouping.BeatmapSet).ToList(); + notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], groupedSet => groupedSet.BeatmapSet).ToList(); } if (notYetVisitedSets.Count == 0) @@ -966,5 +966,5 @@ namespace osu.Game.Screens.SelectV2 /// Used to represent a portion of a under a . /// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it. /// - public record BeatmapSetUnderGrouping([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet); + public record GroupedBeatmapSet([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 63bc94b087..0d2489c304 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -29,14 +29,14 @@ namespace osu.Game.Screens.SelectV2 /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// - public IDictionary> SetItems => setMap; + public IDictionary> SetItems => setMap; /// /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. /// public IDictionary> GroupItems => groupMap; - private Dictionary> setMap = new Dictionary>(); + private Dictionary> setMap = new Dictionary>(); private Dictionary> groupMap = new Dictionary>(); private readonly Func getCriteria; @@ -56,7 +56,7 @@ namespace osu.Game.Screens.SelectV2 return await Task.Run(() => { // preallocate space for the new mappings using last known estimates - var newSetMap = new Dictionary>(setMap.Count); + var newSetMap = new Dictionary>(setMap.Count); var newGroupMap = new Dictionary>(groupMap.Count); var criteria = getCriteria(); @@ -94,12 +94,12 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)item.Model; bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; - var beatmapSetUnderGrouping = new BeatmapSetUnderGrouping(group, beatmap.BeatmapSet!); + var groupedBeatmapSet = new GroupedBeatmapSet(group, beatmap.BeatmapSet!); if (newBeatmapSet) { - if (!newSetMap.TryGetValue(beatmapSetUnderGrouping, out currentSetItems)) - newSetMap[beatmapSetUnderGrouping] = currentSetItems = new HashSet(); + if (!newSetMap.TryGetValue(groupedBeatmapSet, out currentSetItems)) + newSetMap[groupedBeatmapSet] = currentSetItems = new HashSet(); } if (BeatmapSetsGroupedTogether) @@ -109,7 +109,7 @@ namespace osu.Game.Screens.SelectV2 if (groupItem != null) groupItem.NestedItemCount++; - addItem(new CarouselItem(beatmapSetUnderGrouping) + addItem(new CarouselItem(groupedBeatmapSet) { DrawHeight = PanelBeatmapSet.HEIGHT, DepthLayer = -1 @@ -136,7 +136,7 @@ namespace osu.Game.Screens.SelectV2 currentGroupItems?.Add(i); currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetUnderGrouping || !BeatmapSetsGroupedTogether)); + i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is GroupedBeatmapSet || !BeatmapSetsGroupedTogether)); } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 7b07076975..1a6e886cb7 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -67,12 +67,12 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable ruleset { get; set; } = null!; - private BeatmapSetUnderGrouping beatmapSetUnderGrouping + private GroupedBeatmapSet groupedBeatmapSet { get { Debug.Assert(Item != null); - return (BeatmapSetUnderGrouping)Item!.Model; + return (GroupedBeatmapSet)Item!.Model; } } @@ -188,7 +188,7 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; + var beatmapSet = groupedBeatmapSet.BeatmapSet; // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). setBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return Array.Empty(); - var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; + var beatmapSet = groupedBeatmapSet.BeatmapSet; List items = new List(); @@ -275,7 +275,7 @@ namespace osu.Game.Screens.SelectV2 private MenuItem createCollectionMenuItem(BeatmapCollection collection) { - var beatmapSet = beatmapSetUnderGrouping.BeatmapSet; + var beatmapSet = groupedBeatmapSet.BeatmapSet; TernaryState state; From 0a408a3ac4c1eec91f662bd04a297a4683eeb525 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 17:11:47 +0900 Subject: [PATCH 037/267] Fix tournament test failure due to control change --- osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 31583bf8b7..eb9faa5930 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Screens.Gameplay; @@ -66,6 +67,6 @@ namespace osu.Game.Tournament.Tests.Screens () => this.ChildrenOfType().All(score => score.Alpha == (visible ? 1 : 0))); private void toggleWarmup() - => AddStep("toggle warmup", () => this.ChildrenOfType().First().TriggerClick()); + => AddStep("toggle warmup", () => this.ChildrenOfType().First().ChildrenOfType().First().TriggerClick()); } } From 04ba5aa57538aaea6cdff7631a04a18839dca4df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 29 Aug 2025 17:42:14 +0900 Subject: [PATCH 038/267] Move footer to ScreenTestScene --- .../SongSelectV2/SongSelectTestScene.cs | 50 ---------------- osu.Game/Tests/Visual/ScreenTestScene.cs | 60 +++++++++++++++++-- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index b1d1ed8c61..e3b02e5905 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -22,8 +22,6 @@ using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens; -using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -43,9 +41,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!; protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType().Single(); - [Cached] - protected readonly ScreenFooter Footer; - [Cached] private readonly OsuLogo logo; @@ -72,10 +67,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { State = { Value = Visibility.Visible }, }, - Footer = new ScreenFooter - { - BackButtonPressed = () => Stack.CurrentScreen.Exit(), - }, logo = new OsuLogo { Alpha = 0f, @@ -111,14 +102,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Add(beatmapStore); } - protected override void LoadComplete() - { - base.LoadComplete(); - - Stack.ScreenPushed += updateFooter; - Stack.ScreenExited += updateFooter; - } - public override void SetUpSteps() { base.SetUpSteps(); @@ -207,38 +190,5 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen()); - - private void updateFooter(IScreen? _, IScreen? newScreen) - { - if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter) - { - Footer.Show(); - - if (osuScreen.IsLoaded) - updateFooterButtons(); - else - { - // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). - Footer.SetButtons(Array.Empty()); - - osuScreen.OnLoadComplete += _ => updateFooterButtons(); - } - - void updateFooterButtons() - { - var buttons = osuScreen.CreateFooterButtons(); - - osuScreen.LoadComponentsAgainstScreenDependencies(buttons); - - Footer.SetButtons(buttons); - Footer.Show(); - } - } - else - { - Footer.Hide(); - Footer.SetButtons(Array.Empty()); - } - } } } diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index f780b1a8f8..42199faa4d 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -7,7 +7,9 @@ using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Logging; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Overlays; @@ -32,7 +34,7 @@ namespace osu.Game.Tests.Visual protected DialogOverlay DialogOverlay { get; private set; } [Cached] - private ScreenFooter footer; + protected ScreenFooter Footer { get; private set; } protected ScreenTestScene() { @@ -43,17 +45,32 @@ namespace osu.Game.Tests.Visual Name = nameof(ScreenTestScene), RelativeSizeAxes = Axes.Both }, - content = new Container { RelativeSizeAxes = Axes.Both }, + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + content = new Container { RelativeSizeAxes = Axes.Both }, + Footer = new ScreenFooter(), + } + }, overlayContent = new Container { RelativeSizeAxes = Axes.Both, Child = DialogOverlay = new DialogOverlay() }, - footer = new ScreenFooter(), }); - Stack.ScreenPushed += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); - Stack.ScreenExited += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); + Stack.ScreenPushed += (oldScreen, newScreen) => + { + updateFooter(oldScreen, newScreen); + Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); + }; + Stack.ScreenExited += (oldScreen, newScreen) => + { + updateFooter(oldScreen, newScreen); + Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); + }; } protected void LoadScreen(OsuScreen screen) => Stack.Push(screen); @@ -79,6 +96,39 @@ namespace osu.Game.Tests.Visual }); } + private void updateFooter(IScreen? _, IScreen? newScreen) + { + if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter) + { + Footer.Show(); + + if (osuScreen.IsLoaded) + updateFooterButtons(); + else + { + // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). + Footer.SetButtons(Array.Empty()); + + osuScreen.OnLoadComplete += _ => updateFooterButtons(); + } + + void updateFooterButtons() + { + var buttons = osuScreen.CreateFooterButtons(); + + osuScreen.LoadComponentsAgainstScreenDependencies(buttons); + + Footer.SetButtons(buttons); + Footer.Show(); + } + } + else + { + Footer.Hide(); + Footer.SetButtons(Array.Empty()); + } + } + #region IOverlayManager IBindable IOverlayManager.OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); From d304a31757956f11e1624ce054645bf4d5972626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 11:36:22 +0200 Subject: [PATCH 039/267] Make grouping by collections and rank achieved testable without involving realm --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 8 ++++++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index f18e1e9b52..7178dc014d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -16,11 +16,13 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Overlays; +using osu.Game.Scoring; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -443,6 +445,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet; public new GroupDefinition? ExpandedGroup => base.ExpandedGroup; + public Func> AllCollections { get; set; } = () => []; + public Func> BeatmapInfoGuidToTopRankMapping { get; set; } = _ => new Dictionary(); + public TestBeatmapCarousel() { RequestPresentBeatmap = _ => RequestPresentBeatmapCount++; @@ -464,6 +469,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 PostFilterBeatmaps = items.Select(i => i.Model).OfType(); return items; } + + protected override List GetAllCollections() => AllCollections.Invoke(); + protected override Dictionary GetBeatmapInfoGuidToTopRankMapping(FilterCriteria criteria) => BeatmapInfoGuidToTopRankMapping.Invoke(criteria); } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c2711ceef0..18cf005ed3 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -103,14 +103,14 @@ namespace osu.Game.Screens.SelectV2 { new BeatmapCarouselFilterMatching(() => Criteria!), new BeatmapCarouselFilterSorting(() => Criteria!), - grouping = new BeatmapCarouselFilterGrouping(() => Criteria!, getDetachedCollections, getTopRanksMapping) + grouping = new BeatmapCarouselFilterGrouping(() => Criteria!, GetAllCollections, GetBeatmapInfoGuidToTopRankMapping) }; AddInternal(loading = new LoadingLayer()); } [BackgroundDependencyLoader] - private void load(BeatmapStore beatmapStore, RealmAccess realm, AudioManager audio, OsuConfigManager config, CancellationToken? cancellationToken) + private void load(BeatmapStore beatmapStore, AudioManager audio, OsuConfigManager config, CancellationToken? cancellationToken) { setupPools(); detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); @@ -687,9 +687,9 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private RealmAccess realm { get; set; } = null!; - private List getDetachedCollections() => realm.Run(r => r.All().AsEnumerable().Detach()); + protected virtual List GetAllCollections() => realm.Run(r => r.All().AsEnumerable().Detach()); - private Dictionary getTopRanksMapping(FilterCriteria criteria) => realm.Run(r => + protected virtual Dictionary GetBeatmapInfoGuidToTopRankMapping(FilterCriteria criteria) => realm.Run(r => { var topRankMapping = new Dictionary(); From 2ed79d354c8827d2b7a3c3f3e521309bb821af5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 11:50:39 +0200 Subject: [PATCH 040/267] Add baseline test exercising desired duplicated display --- ...tSceneBeatmapCarouselCollectionGrouping.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselCollectionGrouping.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselCollectionGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselCollectionGrouping.cs new file mode 100644 index 0000000000..e410d66ce8 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselCollectionGrouping.cs @@ -0,0 +1,66 @@ +// 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 System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Collections; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselCollectionGrouping : BeatmapCarouselTestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + + AddBeatmaps(10, 3); + + AddStep("set up collections", () => + { + List collections = + [ + new BeatmapCollection("collection one", [ + ..BeatmapSets[0].Beatmaps.Select(b => b.MD5Hash), + ..BeatmapSets[1].Beatmaps.Select(b => b.MD5Hash), + ..BeatmapSets[2].Beatmaps.Select(b => b.MD5Hash), + BeatmapSets[5].Beatmaps[1].MD5Hash, + BeatmapSets[8].Beatmaps[0].MD5Hash, + ]), + new BeatmapCollection("collection two", [ + BeatmapSets[0].Beatmaps[0].MD5Hash, + ..BeatmapSets[1].Beatmaps.Select(b => b.MD5Hash), + ..BeatmapSets[2].Beatmaps.Select(b => b.MD5Hash), + BeatmapSets[6].Beatmaps[2].MD5Hash, + BeatmapSets[8].Beatmaps[2].MD5Hash, + ]), + new BeatmapCollection("collection one copy", [ + ..BeatmapSets[0].Beatmaps.Select(b => b.MD5Hash), + ..BeatmapSets[1].Beatmaps.Select(b => b.MD5Hash), + ..BeatmapSets[2].Beatmaps.Select(b => b.MD5Hash), + BeatmapSets[5].Beatmaps[1].MD5Hash, + BeatmapSets[8].Beatmaps[0].MD5Hash, + ]), + ]; + Carousel.AllCollections = () => collections; + }); + + SortAndGroupBy(SortMode.Title, GroupMode.Collections); + WaitForDrawablePanels(); + } + + [Test] + public void TestMultipleCopiesOfBeatmapsPresent() + { + CheckDisplayedGroupsCount(4); // one for each collection, plus no collections + // all three collections have beatmaps from 5 beatmap sets + // 7 beatmap sets have beatmaps which belong to no collection + CheckDisplayedBeatmapSetsCount(5 + 5 + 5 + 7); + } + } +} From a84c364e44d1e1f89c209da4f29e0ab524b3e2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 12:42:54 +0200 Subject: [PATCH 041/267] Introduce new model for "beatmaps under grouping" & allow beatmaps to appear in multiple groups This bypasses the immediate first issue of not being able to display multiple instances of a beatmap on the carousel because of model equality being baked into the structure. It inevitably poses a bunch of *other* problems, but it's a start. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 56 +++---- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 137 ++++++++++-------- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 18 ++- .../SelectV2/PanelBeatmapStandalone.cs | 19 ++- 4 files changed, 136 insertions(+), 94 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 18cf005ed3..36264223ea 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -74,11 +74,11 @@ namespace osu.Game.Screens.SelectV2 return SPACING * 2; // ..and the bottom. - if (top.Model is BeatmapInfo && bottom.Model is GroupedBeatmapSet) + if (top.Model is GroupedBeatmap && bottom.Model is GroupedBeatmapSet) return SPACING * 2; // Beatmap difficulty panels do not overlap with themselves or any other panel. - if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo) + if (top.Model is GroupedBeatmap || bottom.Model is GroupedBeatmap) return SPACING; } else @@ -200,13 +200,13 @@ namespace osu.Game.Screens.SelectV2 { if (CheckValidForSetSelection(item)) { - if (item.Model is BeatmapInfo beatmapInfo) + if (item.Model is GroupedBeatmap groupedBeatmap) { // check the new selection wasn't deleted above - if (!Items.Contains(beatmapInfo)) + if (!Items.Contains(groupedBeatmap.Beatmap)) return false; - RequestSelection(beatmapInfo); + RequestSelection(groupedBeatmap.Beatmap); return true; } @@ -289,7 +289,7 @@ namespace osu.Game.Screens.SelectV2 protected GroupedBeatmapSet? ExpandedBeatmapSet { get; private set; } protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => - grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo; + grouping.BeatmapSetsGroupedTogether && item.Model is GroupedBeatmap; protected override void HandleItemActivated(CarouselItem item) { @@ -318,14 +318,14 @@ namespace osu.Game.Screens.SelectV2 selectRecommendedDifficultyForBeatmapSet(groupedSet); return; - case BeatmapInfo beatmapInfo: - if (CurrentSelection != null && CheckModelEquality(CurrentSelection, beatmapInfo)) + case GroupedBeatmap groupedBeatmap: + if (CurrentSelection != null && CheckModelEquality(CurrentSelection, groupedBeatmap)) { - RequestPresentBeatmap?.Invoke(beatmapInfo); + RequestPresentBeatmap?.Invoke(groupedBeatmap.Beatmap); return; } - RequestSelection(beatmapInfo); + RequestSelection(groupedBeatmap.Beatmap); return; } } @@ -345,14 +345,11 @@ namespace osu.Game.Screens.SelectV2 case GroupDefinition: throw new InvalidOperationException("Groups should never become selected"); - case BeatmapInfo beatmapInfo: - // Find any containing group. There should never be too many groups so iterating is efficient enough. - GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => CheckModelEquality(i.Model, beatmapInfo))).Key; - - setExpandedGroup(containingGroup); + case GroupedBeatmap groupedBeatmap: + setExpandedGroup(groupedBeatmap.Group); if (grouping.BeatmapSetsGroupedTogether) - setExpandedSet(new GroupedBeatmapSet(containingGroup, beatmapInfo.BeatmapSet!)); + setExpandedSet(new GroupedBeatmapSet(groupedBeatmap.Group, groupedBeatmap.Beatmap.BeatmapSet!)); break; } } @@ -432,7 +429,7 @@ namespace osu.Game.Screens.SelectV2 // Selecting a set isn't valid – let's re-select the first visible difficulty. if (grouping.SetItems.TryGetValue(set, out var items)) { - var beatmaps = items.Select(i => i.Model).OfType(); + var beatmaps = items.Select(i => i.Model).OfType().Select(b => b.Beatmap); RequestRecommendedSelection(beatmaps); } } @@ -450,8 +447,10 @@ namespace osu.Game.Screens.SelectV2 foreach (var item in items) { - if (item.Model is BeatmapInfo beatmapInfo) + if (item.Model is GroupedBeatmap groupedBeatmap) { + var beatmapInfo = groupedBeatmap.Beatmap; + if (beatmapSetInfo == null) { beatmapSetInfo = beatmapInfo.BeatmapSet!; @@ -481,7 +480,7 @@ namespace osu.Game.Screens.SelectV2 case GroupedBeatmapSet: return true; - case BeatmapInfo: + case GroupedBeatmap: return !grouping.BeatmapSetsGroupedTogether; case GroupDefinition: @@ -492,7 +491,7 @@ namespace osu.Game.Screens.SelectV2 } } - private void setExpandedGroup(GroupDefinition group) + private void setExpandedGroup(GroupDefinition? group) { if (ExpandedGroup != null) setExpansionStateOfGroup(ExpandedGroup, false); @@ -500,7 +499,7 @@ namespace osu.Game.Screens.SelectV2 ExpandedGroup = group; if (ExpandedGroup != null) - setExpansionStateOfGroup(group, true); + setExpansionStateOfGroup(ExpandedGroup, true); } private void setExpansionStateOfGroup(GroupDefinition group, bool expanded) @@ -607,7 +606,7 @@ namespace osu.Game.Screens.SelectV2 sampleChangeSet?.Play(); return; - case BeatmapInfo: + case GroupedBeatmap: sampleChangeDifficulty?.Play(); return; } @@ -745,8 +744,8 @@ namespace osu.Game.Screens.SelectV2 if (x is GroupedBeatmapSet groupedSetX && y is GroupedBeatmapSet groupedSetY) return groupedSetX.Equals(groupedSetY); - if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY) - return beatmapX.Equals(beatmapY); + if (x is GroupedBeatmap groupedBeatmapX && y is GroupedBeatmap groupedBeatmapY) + return groupedBeatmapX.Equals(groupedBeatmapY); if (x is GroupDefinition groupX && y is GroupDefinition groupY) return groupX.Equals(groupY); @@ -767,7 +766,7 @@ namespace osu.Game.Screens.SelectV2 case GroupDefinition: return groupPanelPool.Get(); - case BeatmapInfo: + case GroupedBeatmap: if (!grouping.BeatmapSetsGroupedTogether) return standalonePanelPool.Get(); @@ -1021,4 +1020,11 @@ namespace osu.Game.Screens.SelectV2 /// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it. /// public record GroupedBeatmapSet([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet); + + /// + /// Used to represent a under a . + /// The purpose of this model is to support showing multiple copies of a beatmap, which can occur if a beatmap appears in multiple groups + /// (most prominently, collections group mode). + /// + public record GroupedBeatmap(GroupDefinition? Group, BeatmapInfo Beatmap); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 0d2489c304..9fa4e28e93 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Graphics.Carousel; @@ -124,7 +125,7 @@ namespace osu.Game.Screens.SelectV2 item.DrawHeight = PanelBeatmapStandalone.HEIGHT; } - addItem(item); + addItem(new CarouselItem(new GroupedBeatmap(group, beatmap))); lastBeatmap = beatmap; displayedBeatmapsCount++; } @@ -192,7 +193,7 @@ namespace osu.Game.Screens.SelectV2 var date = b.LastPlayed; if (date == null || date == DateTimeOffset.MinValue) - return new GroupDefinition(int.MaxValue, "Never"); + return new GroupDefinition(int.MaxValue, "Never").Yield(); return defineGroupByDate(date.Value); }, items); @@ -236,184 +237,204 @@ namespace osu.Game.Screens.SelectV2 } } - private List getGroupsBy(Func getGroup, List items) + private List getGroupsBy(Func> defineGroups, List items) { - return items.GroupBy(i => getGroup((BeatmapInfo)i.Model)) - .Where(g => g.Key != null) - .OrderBy(g => g.Key!.Order) - .ThenBy(g => g.Key!.Title) - .Select(g => new GroupMapping(g.Key, g.ToList())) - .ToList(); + var groups = new Dictionary(); + + foreach (var item in items) + { + foreach (var groupDefinition in defineGroups((BeatmapInfo)item.Model)) + { + if (!groups.TryGetValue(groupDefinition, out var group)) + group = groups[groupDefinition] = new GroupMapping(groupDefinition, []); + + group.ItemsInGroup.Add(item); + } + } + + return groups.Values + .OrderBy(g => g.Group!.Order) + .ThenBy(g => g.Group!.Title) + .ToList(); } - private GroupDefinition defineGroupAlphabetically(string name) + private IEnumerable defineGroupAlphabetically(string name) { char firstChar = name.FirstOrDefault(); if (char.IsAsciiDigit(firstChar)) - return new GroupDefinition(int.MinValue, "0-9"); + return new GroupDefinition(int.MinValue, "0-9").Yield(); if (char.IsAsciiLetter(firstChar)) - return new GroupDefinition(char.ToUpperInvariant(firstChar) - 'A', char.ToUpperInvariant(firstChar).ToString()); + return new GroupDefinition(char.ToUpperInvariant(firstChar) - 'A', char.ToUpperInvariant(firstChar).ToString()).Yield(); - return new GroupDefinition(int.MaxValue, "Other"); + return new GroupDefinition(int.MaxValue, "Other").Yield(); } - private GroupDefinition defineGroupByDate(DateTimeOffset date) + private IEnumerable defineGroupByDate(DateTimeOffset date) { var now = DateTimeOffset.Now; var elapsed = now - date; if (elapsed.TotalDays < 1) - return new GroupDefinition(0, "Today"); + return new GroupDefinition(0, "Today").Yield(); if (elapsed.TotalDays < 2) - return new GroupDefinition(1, "Yesterday"); + return new GroupDefinition(1, "Yesterday").Yield(); if (elapsed.TotalDays < 7) - return new GroupDefinition(2, "Last week"); + return new GroupDefinition(2, "Last week").Yield(); if (elapsed.TotalDays < 30) - return new GroupDefinition(3, "Last month"); + return new GroupDefinition(3, "Last month").Yield(); if (elapsed.TotalDays < 60) - return new GroupDefinition(4, "1 month ago"); + return new GroupDefinition(4, "1 month ago").Yield(); for (int i = 90; i <= 150; i += 30) { if (elapsed.TotalDays < i) - return new GroupDefinition(i, $"{i / 30 - 1} months ago"); + return new GroupDefinition(i, $"{i / 30 - 1} months ago").Yield(); } - return new GroupDefinition(151, "Over 5 months ago"); + return new GroupDefinition(151, "Over 5 months ago").Yield(); } - private GroupDefinition defineGroupByRankedDate(DateTimeOffset? date) + private IEnumerable defineGroupByRankedDate(DateTimeOffset? date) { if (date == null) - return new GroupDefinition(0, "Unranked"); + return new GroupDefinition(0, "Unranked").Yield(); - return new GroupDefinition(-date.Value.Year, $"{date.Value.Year}"); + return new GroupDefinition(-date.Value.Year, $"{date.Value.Year}").Yield(); } - private GroupDefinition defineGroupByStatus(BeatmapOnlineStatus status) + private IEnumerable defineGroupByStatus(BeatmapOnlineStatus status) { switch (status) { case BeatmapOnlineStatus.Ranked: case BeatmapOnlineStatus.Approved: - return new GroupDefinition(0, BeatmapOnlineStatus.Ranked.GetDescription()); + return new GroupDefinition(0, BeatmapOnlineStatus.Ranked.GetDescription()).Yield(); case BeatmapOnlineStatus.Qualified: - return new GroupDefinition(1, status.GetDescription()); + return new GroupDefinition(1, status.GetDescription()).Yield(); case BeatmapOnlineStatus.WIP: - return new GroupDefinition(2, status.GetDescription()); + return new GroupDefinition(2, status.GetDescription()).Yield(); case BeatmapOnlineStatus.Pending: - return new GroupDefinition(3, status.GetDescription()); + return new GroupDefinition(3, status.GetDescription()).Yield(); case BeatmapOnlineStatus.Graveyard: - return new GroupDefinition(4, status.GetDescription()); + return new GroupDefinition(4, status.GetDescription()).Yield(); case BeatmapOnlineStatus.LocallyModified: - return new GroupDefinition(5, status.GetDescription()); + return new GroupDefinition(5, status.GetDescription()).Yield(); case BeatmapOnlineStatus.None: - return new GroupDefinition(6, status.GetDescription()); + return new GroupDefinition(6, status.GetDescription()).Yield(); case BeatmapOnlineStatus.Loved: - return new GroupDefinition(7, status.GetDescription()); + return new GroupDefinition(7, status.GetDescription()).Yield(); default: throw new ArgumentOutOfRangeException(nameof(status), status, null); } } - private GroupDefinition defineGroupByBPM(double bpm) + private IEnumerable defineGroupByBPM(double bpm) { if (bpm < 60) - return new GroupDefinition(60, "Under 60 BPM"); + return new GroupDefinition(60, "Under 60 BPM").Yield(); for (int i = 70; i <= 300; i += 10) { if (bpm < i) - return new GroupDefinition(i, $"{i - 10} - {i} BPM"); + return new GroupDefinition(i, $"{i - 10} - {i} BPM").Yield(); } - return new GroupDefinition(301, "Over 300 BPM"); + return new GroupDefinition(301, "Over 300 BPM").Yield(); } - private GroupDefinition defineGroupByStars(double stars) + private IEnumerable defineGroupByStars(double stars) { // truncation is intentional - compare `FormatUtils.FormatStarRating()` int starInt = (int)stars; var starDifficulty = new StarDifficulty(starInt, 0); if (starInt == 0) - return new StarDifficultyGroupDefinition(0, "Below 1 Star", starDifficulty); + return new StarDifficultyGroupDefinition(0, "Below 1 Star", starDifficulty).Yield(); if (starInt == 1) - return new StarDifficultyGroupDefinition(1, "1 Star", starDifficulty); + return new StarDifficultyGroupDefinition(1, "1 Star", starDifficulty).Yield(); - return new StarDifficultyGroupDefinition(starInt, $"{starInt} Stars", starDifficulty); + return new StarDifficultyGroupDefinition(starInt, $"{starInt} Stars", starDifficulty).Yield(); } - private GroupDefinition defineGroupByLength(double length) + private IEnumerable defineGroupByLength(double length) { for (int i = 1; i < 6; i++) { if (length <= i * 60_000) { if (i == 1) - return new GroupDefinition(1, "1 minute or less"); + return new GroupDefinition(1, "1 minute or less").Yield(); - return new GroupDefinition(i, $"{i} minutes or less"); + return new GroupDefinition(i, $"{i} minutes or less").Yield(); } } if (length <= 10 * 60_000) - return new GroupDefinition(10, "10 minutes or less"); + return new GroupDefinition(10, "10 minutes or less").Yield(); - return new GroupDefinition(11, "Over 10 minutes"); + return new GroupDefinition(11, "Over 10 minutes").Yield(); } - private GroupDefinition defineGroupBySource(string source) + private IEnumerable defineGroupBySource(string source) { if (string.IsNullOrEmpty(source)) - return new GroupDefinition(1, "Unsourced"); + return new GroupDefinition(1, "Unsourced").Yield(); - return new GroupDefinition(0, source); + return new GroupDefinition(0, source).Yield(); } - private GroupDefinition defineGroupByCollection(BeatmapInfo beatmap, IEnumerable collections) + private IEnumerable defineGroupByCollection(BeatmapInfo beatmap, IEnumerable collections) { + bool anyCollections = false; + foreach (var collection in collections) { if (collection.BeatmapMD5Hashes.Contains(beatmap.MD5Hash)) - return new GroupDefinition(0, collection.Name); + { + yield return new GroupDefinition(0, collection.Name); + + anyCollections = true; + } } - return new GroupDefinition(1, "Not in collection"); + if (anyCollections) + yield break; + + yield return new GroupDefinition(1, "Not in collection"); } - private GroupDefinition? defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername) + private IEnumerable defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername) { var author = beatmap.BeatmapSet!.Metadata.Author; if (author.OnlineID == localUserId || (author.OnlineID <= 1 && author.Username == localUserUsername)) - return new GroupDefinition(0, "My maps"); + return new GroupDefinition(0, "My maps").Yield(); // discard beatmaps not owned by the user. - return null; + return []; } - private GroupDefinition defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary topRankMapping) + private IEnumerable defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary topRankMapping) { if (topRankMapping.TryGetValue(beatmap.ID, out var rank)) - return new GroupDefinition(-(int)rank, rank.GetDescription()); + return new GroupDefinition(-(int)rank, rank.GetDescription()).Yield(); - return new GroupDefinition(int.MaxValue, "Unplayed"); + return new GroupDefinition(int.MaxValue, "Unplayed").Yield(); } private record GroupMapping(GroupDefinition? Group, List ItemsInGroup); diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 106b911606..545439684b 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -72,6 +72,15 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private ISongSelect? songSelect { get; set; } + private GroupedBeatmap groupedBeatmap + { + get + { + Debug.Assert(Item != null); + return (GroupedBeatmap)Item!.Model; + } + } + public PanelBeatmap() { PanelXOffset = 60; @@ -207,8 +216,7 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - Debug.Assert(Item != null); - var beatmap = (BeatmapInfo)Item.Model; + var beatmap = groupedBeatmap.Beatmap; difficultyIcon.Icon = getRulesetIcon(beatmap.Ruleset); @@ -248,7 +256,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return; - var beatmap = (BeatmapInfo)Item.Model; + var beatmap = groupedBeatmap.Beatmap; starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE); starDifficultyBindable.BindValueChanged(starDifficulty => @@ -293,7 +301,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return; - var beatmap = (BeatmapInfo)Item.Model; + var beatmap = groupedBeatmap.Beatmap; if (ruleset.Value.OnlineID == 3) { @@ -319,7 +327,7 @@ namespace osu.Game.Screens.SelectV2 List items = new List(); if (songSelect != null) - items.AddRange(songSelect.GetForwardActions((BeatmapInfo)Item.Model)); + items.AddRange(songSelect.GetForwardActions(groupedBeatmap.Beatmap)); return items.ToArray(); } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 87a35facbd..226a1b1d06 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -73,6 +73,15 @@ namespace osu.Game.Screens.SelectV2 private Box backgroundBorder = null!; + private GroupedBeatmap groupedBeatmap + { + get + { + Debug.Assert(Item != null); + return (GroupedBeatmap)Item!.Model; + } + } + public PanelBeatmapStandalone() { PanelXOffset = 20; @@ -219,9 +228,7 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - Debug.Assert(Item != null); - - var beatmap = (BeatmapInfo)Item.Model; + var beatmap = groupedBeatmap.Beatmap; var beatmapSet = beatmap.BeatmapSet!; beatmapBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmap); @@ -262,7 +269,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return; - var beatmap = (BeatmapInfo)Item.Model; + var beatmap = groupedBeatmap.Beatmap; starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE); starDifficultyBindable.BindValueChanged(starDifficulty => @@ -300,7 +307,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return; - var beatmap = (BeatmapInfo)Item.Model; + var beatmap = groupedBeatmap.Beatmap; if (ruleset.Value.OnlineID == 3) { @@ -326,7 +333,7 @@ namespace osu.Game.Screens.SelectV2 List items = new List(); if (songSelect != null) - items.AddRange(songSelect.GetForwardActions((BeatmapInfo)Item.Model)); + items.AddRange(songSelect.GetForwardActions(groupedBeatmap.Beatmap)); return items.ToArray(); } From e98579d3afbb4eb7df72f6e41cad4f57470b93e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 12:50:47 +0200 Subject: [PATCH 042/267] Apply most trivial adjustments to tests after beatmap model replacement --- .../BeatmapCarouselFilterGroupingTest.cs | 4 ++-- .../SongSelectV2/BeatmapCarouselTestScene.cs | 8 +++---- .../TestSceneBeatmapCarouselArtistGrouping.cs | 5 ++--- ...tSceneBeatmapCarouselDifficultyGrouping.cs | 5 ++--- .../TestSceneBeatmapCarouselFiltering.cs | 22 +++++++++---------- .../TestSceneBeatmapCarouselNoGrouping.cs | 3 +-- .../SongSelectV2/TestScenePanelBeatmap.cs | 8 +++---- .../TestScenePanelBeatmapStandalone.cs | 8 +++---- .../TestSceneSongSelectGrouping.cs | 2 +- 9 files changed, 31 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 32a7b89424..5f3cd26d55 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var results = await runGrouping(GroupMode.None, beatmapSets); Assert.That(results.Select(r => r.Model).OfType().Select(groupedSet => groupedSet.BeatmapSet), Is.EquivalentTo(beatmapSets)); - Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(allBeatmaps)); + Assert.That(results.Select(r => r.Model).OfType().Select(groupedBeatmap => groupedBeatmap.Beatmap), Is.EquivalentTo(allBeatmaps)); assertTotal(results, beatmapSets.Count + allBeatmaps.Length); } @@ -391,7 +391,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var groupModel = (GroupDefinition)groupItem.Model; Assert.That(groupModel.Title, Is.EqualTo(expectedTitle)); - Assert.That(itemsInGroup.Select(i => i.Model).OfType(), Is.EquivalentTo(expectedBeatmaps)); + Assert.That(itemsInGroup.Select(i => i.Model).OfType().Select(gb => gb.Beatmap), Is.EquivalentTo(expectedBeatmaps)); totalItems += itemsInGroup.Count() + 1; } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 7178dc014d..06d2a42e0d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -283,8 +283,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // offset by one because the group itself is included in the items list. CarouselItem item = groupingFilter.GroupItems[groupDefinition].ElementAt(panel + 1); - return (Carousel.CurrentSelection as BeatmapInfo)? - .Equals(item.Model as BeatmapInfo) == true; + return (Carousel.CurrentSelection as GroupedBeatmap)? + .Equals(item.Model as GroupedBeatmap) == true; }); } @@ -293,7 +293,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 if (diff != null) { AddUntilStep($"selected is set{set} diff{diff.Value}", - () => (Carousel.CurrentSelection as BeatmapInfo), + () => (Carousel.CurrentSelection as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(BeatmapSets[set].Beatmaps[diff.Value])); } else @@ -466,7 +466,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 if (FilterDelay != 0) await Task.Delay(FilterDelay).ConfigureAwait(true); - PostFilterBeatmaps = items.Select(i => i.Model).OfType(); + PostFilterBeatmaps = items.Select(i => i.Model).OfType().Select(i => i.Beatmap); return items; } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 521221f0c7..78b6985fdb 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -92,7 +91,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckHasSelection(); AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); - AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); @@ -132,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForBeatmapSelection(0, 1); // Expanding a group will move keyboard selection to the selected beatmap if contained. AddAssert("keyboard selected panel is expanded", () => groupPanel?.Expanded.Value, () => Is.True); - AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf); + AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 2ab0eda172..c28860e368 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -82,7 +81,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckHasSelection(); AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); - AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); @@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForBeatmapSelection(0, 0); // Expanding a group will move keyboard selection to the selected beatmap if contained. AddAssert("keyboard selected panel is expanded", () => groupPanel?.Expanded.Value, () => Is.True); - AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf); + AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 687c4c23be..70af48069a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -130,14 +130,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); Select(); - AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID); + AddStep("record selection", () => selectedID = ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID); for (int i = 0; i < 5; i++) { ApplyToFilterAndWaitForFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); - AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID == selectedID); ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); - AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID == selectedID); } } @@ -177,14 +177,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); - AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID); + AddStep("record selection", () => selectedID = ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID); for (int i = 0; i < 5; i++) { ApplyToFilterAndWaitForFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); - AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID == selectedID); ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); - AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID == selectedID); } } @@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var visibleBeatmapPanels = GetVisiblePanels(); return visibleBeatmapPanels.Count() == 1 - && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1; + && visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 0) == 1; }); ApplyToFilterAndWaitForFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1)); @@ -249,8 +249,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var visibleBeatmapPanels = GetVisiblePanels(); return visibleBeatmapPanels.Count() == 2 - && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1 - && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1; + && visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 1) == 1; }); ApplyToFilterAndWaitForFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2)); @@ -260,8 +260,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var visibleBeatmapPanels = GetVisiblePanels(); return visibleBeatmapPanels.Count() == 2 - && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1 - && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 2) == 1; + && visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 2) == 1; }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 648f531a6e..0b0f93b3bc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; @@ -101,7 +100,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckHasSelection(); AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); - AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs index 09f8c68951..618b9e0d48 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs @@ -104,21 +104,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelBeatmap { - Item = new CarouselItem(beatmap) + Item = new CarouselItem(new GroupedBeatmap(null, beatmap)) }, new PanelBeatmap { - Item = new CarouselItem(beatmap), + Item = new CarouselItem(new GroupedBeatmap(null, beatmap)), KeyboardSelected = { Value = true } }, new PanelBeatmap { - Item = new CarouselItem(beatmap), + Item = new CarouselItem(new GroupedBeatmap(null, beatmap)), Selected = { Value = true } }, new PanelBeatmap { - Item = new CarouselItem(beatmap), + Item = new CarouselItem(new GroupedBeatmap(null, beatmap)), KeyboardSelected = { Value = true }, Selected = { Value = true } }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs index e9361b3d7f..67a9f54f1a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs @@ -104,21 +104,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelBeatmapStandalone { - Item = new CarouselItem(beatmap) + Item = new CarouselItem(new GroupedBeatmap(null, beatmap)) }, new PanelBeatmapStandalone { - Item = new CarouselItem(beatmap), + Item = new CarouselItem(new GroupedBeatmap(null, beatmap)), KeyboardSelected = { Value = true } }, new PanelBeatmapStandalone { - Item = new CarouselItem(beatmap), + Item = new CarouselItem(new GroupedBeatmap(null, beatmap)), Selected = { Value = true } }, new PanelBeatmapStandalone { - Item = new CarouselItem(beatmap), + Item = new CarouselItem(new GroupedBeatmap(null, beatmap)), KeyboardSelected = { Value = true }, Selected = { Value = true } }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs index 0772607a57..aa80321033 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs @@ -317,7 +317,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert($"\"{name}\" present", () => { var group = grouping.GroupItems.Single(g => g.Key.Title == name); - var actualBeatmaps = group.Value.Select(i => i.Model).OfType().OrderBy(b => b.ID); + var actualBeatmaps = group.Value.Select(i => i.Model).OfType().Select(gb => gb.Beatmap).OrderBy(b => b.ID); var expectedBeatmaps = getBeatmaps().SelectMany(s => s.Beatmaps).OrderBy(b => b.ID); return actualBeatmaps.SequenceEqual(expectedBeatmaps); }); From d4b357dfa0133d24083aa08372644f86c799a14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Aug 2025 13:08:31 +0200 Subject: [PATCH 043/267] Fix carousel selection not working Basically, `BeatmapCarousel.CurrentSelection`, which is magic-object-typed, can no longer use `BeatmapInfo` directly, it now must also use `GroupedBeatmap`. This spills out all the way into song select because of beatmap selection flows that require hookup from song select. --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 12 +-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 74 ++++++++++--------- osu.Game/Screens/SelectV2/SongSelect.cs | 20 +++-- 3 files changed, 58 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 06d2a42e0d..c60ee55110 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -117,13 +117,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2 NewItemsPresented = _ => NewItemsPresentedInvocationCount++, RequestSelection = b => { - BeatmapRequestedSelections.Push(b); + BeatmapRequestedSelections.Push(b.Beatmap); Carousel.CurrentSelection = b; }, - RequestRecommendedSelection = beatmaps => + RequestRecommendedSelection = groupedBeatmaps => { - BeatmapSetRequestedSelections.Push(beatmaps.First().BeatmapSet!); - Carousel.CurrentSelection = BeatmapRecommendationFunction?.Invoke(beatmaps) ?? beatmaps.First(); + var recommendedBeatmap = BeatmapRecommendationFunction?.Invoke(groupedBeatmaps.Select(gb => gb.Beatmap)) ?? groupedBeatmaps.First().Beatmap; + var recommendedGroupedBeatmap = groupedBeatmaps.First(gb => gb.Beatmap.Equals(recommendedBeatmap)); + BeatmapSetRequestedSelections.Push(recommendedBeatmap.BeatmapSet!); + Carousel.CurrentSelection = recommendedGroupedBeatmap; }, BleedTop = 50, BleedBottom = 50, @@ -439,7 +441,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public IEnumerable PostFilterBeatmaps = null!; - public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo; + public BeatmapInfo? SelectedBeatmapInfo => (CurrentSelection as GroupedBeatmap)?.Beatmap; public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet; public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 36264223ea..46ea61ca9d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -41,12 +41,12 @@ namespace osu.Game.Screens.SelectV2 /// /// From the provided beatmaps, select the most appropriate one for the user's skill. /// - public required Action> RequestRecommendedSelection { private get; init; } + public required Action> RequestRecommendedSelection { private get; init; } /// /// Selection requested for the provided beatmap. /// - public required Action RequestSelection { private get; init; } + public required Action RequestSelection { private get; init; } public const float SPACING = 3f; @@ -158,7 +158,7 @@ namespace osu.Game.Screens.SelectV2 foreach (var beatmap in set.Beatmaps) { Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); - selectedSetDeleted |= CheckModelEquality(CurrentSelection, beatmap); + selectedSetDeleted |= CheckModelEquality((CurrentSelection as GroupedBeatmap)?.Beatmap, beatmap); } } @@ -206,7 +206,7 @@ namespace osu.Game.Screens.SelectV2 if (!Items.Contains(groupedBeatmap.Beatmap)) return false; - RequestSelection(groupedBeatmap.Beatmap); + RequestSelection(groupedBeatmap); return true; } @@ -215,7 +215,7 @@ namespace osu.Game.Screens.SelectV2 if (oldItems.Contains(groupedSet.BeatmapSet)) return false; - RequestRecommendedSelection(groupedSet.BeatmapSet.Beatmaps); + selectRecommendedDifficultyForBeatmapSet(groupedSet); return true; } } @@ -256,8 +256,12 @@ namespace osu.Game.Screens.SelectV2 { // TODO: should this exist in song select instead of here? // we need to ensure the global beatmap is also updated alongside changes. - if (CurrentSelection != null && CheckModelEquality(beatmap, CurrentSelection)) - RequestSelection(matchingNewBeatmap); + if (CurrentSelection is GroupedBeatmap currentBeatmapUnderGrouping) + { + var candidateSelection = currentBeatmapUnderGrouping with { Beatmap = beatmap }; + if (CheckModelEquality(candidateSelection, CurrentSelection)) + RequestSelection(candidateSelection); + } Items.ReplaceRange(previousIndex, 1, [matchingNewBeatmap]); newSetBeatmaps.Remove(matchingNewBeatmap); @@ -309,8 +313,8 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(group); // If the active selection is within this group, it should get keyboard focus immediately. - if (CurrentSelectionItem?.IsVisible == true && CurrentSelection is BeatmapInfo info) - RequestSelection(info); + if (CurrentSelectionItem?.IsVisible == true && CurrentSelection is GroupedBeatmap gb) + RequestSelection(gb); return; @@ -325,7 +329,7 @@ namespace osu.Game.Screens.SelectV2 return; } - RequestSelection(groupedBeatmap.Beatmap); + RequestSelection(groupedBeatmap); return; } } @@ -429,7 +433,7 @@ namespace osu.Game.Screens.SelectV2 // Selecting a set isn't valid – let's re-select the first visible difficulty. if (grouping.SetItems.TryGetValue(set, out var items)) { - var beatmaps = items.Select(i => i.Model).OfType().Select(b => b.Beatmap); + var beatmaps = items.Select(i => i.Model).OfType(); RequestRecommendedSelection(beatmaps); } } @@ -463,9 +467,9 @@ namespace osu.Game.Screens.SelectV2 } } - var beatmaps = items.Select(i => i.Model).OfType(); + var beatmaps = items.Select(i => i.Model).OfType(); - if (beatmaps.Any(b => b.Equals(CurrentSelection as BeatmapInfo))) + if (beatmaps.Any(b => b.Equals(CurrentSelection as GroupedBeatmap))) return; RequestRecommendedSelection(beatmaps); @@ -784,8 +788,8 @@ namespace osu.Game.Screens.SelectV2 #region Random selection handling private readonly Bindable randomAlgorithm = new Bindable(); - private readonly List previouslyVisitedRandomBeatmaps = new List(); - private readonly List randomHistory = new List(); + private readonly HashSet previouslyVisitedRandomBeatmaps = new HashSet(); + private readonly List randomHistory = new List(); private Sample? spinSample; private Sample? randomSelectSample; @@ -798,7 +802,7 @@ namespace osu.Game.Screens.SelectV2 return false; var selectionBefore = CurrentSelectionItem; - var beatmapBefore = selectionBefore?.Model as BeatmapInfo; + var beatmapBefore = selectionBefore?.Model as GroupedBeatmap; bool success; @@ -808,7 +812,7 @@ namespace osu.Game.Screens.SelectV2 randomHistory.Add(beatmapBefore); // keep track of visited beatmaps for "RandomPermutation" random tracking. // note that this is reset when we run out of beatmaps, while `randomHistory` is not. - previouslyVisitedRandomBeatmaps.Add(beatmapBefore); + previouslyVisitedRandomBeatmaps.Add(beatmapBefore.Beatmap); } if (grouping.BeatmapSetsGroupedTogether) @@ -836,29 +840,29 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomBeatmap() { - ICollection visibleBeatmaps = ExpandedGroup != null + ICollection visibleBeatmaps = ExpandedGroup != null // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // // If this becomes an issue, we could either store a mapping, or run the random algorithm many times // using the `SetItems` method until we get a group HIT. - ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() - : GetCarouselItems()!.Select(i => i.Model).OfType().ToArray(); + ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + : GetCarouselItems()!.Select(i => i.Model).OfType().ToArray(); - BeatmapInfo beatmap; + GroupedBeatmap beatmap; switch (randomAlgorithm.Value) { case RandomSelectAlgorithm.RandomPermutation: { - ICollection notYetVisitedBeatmaps = visibleBeatmaps.Except(previouslyVisitedRandomBeatmaps).ToList(); + ICollection notYetVisitedBeatmaps = visibleBeatmaps.ExceptBy(previouslyVisitedRandomBeatmaps, gb => gb.Beatmap).ToList(); if (!notYetVisitedBeatmaps.Any()) { - previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleBeatmaps.Contains(b)); + previouslyVisitedRandomBeatmaps.ExceptWith(visibleBeatmaps.Select(b => b.Beatmap)); notYetVisitedBeatmaps = visibleBeatmaps; - if (CurrentSelection is BeatmapInfo beatmapInfo) - notYetVisitedBeatmaps = notYetVisitedBeatmaps.Except([beatmapInfo]).ToList(); + if (CurrentSelection is GroupedBeatmap groupedBeatmap) + notYetVisitedBeatmaps = notYetVisitedBeatmaps.Except([groupedBeatmap]).ToList(); } if (notYetVisitedBeatmaps.Count == 0) @@ -882,7 +886,7 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomSet() { - ICollection visibleSetsUnderGrouping = ExpandedGroup != null + ICollection visibleGroupedSets = ExpandedGroup != null // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // @@ -899,14 +903,14 @@ namespace osu.Game.Screens.SelectV2 case RandomSelectAlgorithm.RandomPermutation: { ICollection notYetVisitedSets = - visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), groupedSet => groupedSet.BeatmapSet).ToList(); + visibleGroupedSets.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), groupedSet => groupedSet.BeatmapSet).ToList(); if (!notYetVisitedSets.Any()) { - previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(groupedSet => groupedSet.BeatmapSet.Equals(b.BeatmapSet!))); - notYetVisitedSets = visibleSetsUnderGrouping; - if (CurrentSelection is BeatmapInfo beatmapInfo) - notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], groupedSet => groupedSet.BeatmapSet).ToList(); + previouslyVisitedRandomBeatmaps.ExceptWith(visibleGroupedSets.SelectMany(setUnderGrouping => setUnderGrouping.BeatmapSet.Beatmaps)); + notYetVisitedSets = visibleGroupedSets; + if (CurrentSelection is GroupedBeatmap groupedBeatmap) + notYetVisitedSets = notYetVisitedSets.ExceptBy([groupedBeatmap.Beatmap.BeatmapSet!], groupedSet => groupedSet.BeatmapSet).ToList(); } if (notYetVisitedSets.Count == 0) @@ -917,7 +921,7 @@ namespace osu.Game.Screens.SelectV2 } case RandomSelectAlgorithm.Random: - set = visibleSetsUnderGrouping.ElementAt(RNG.Next(visibleSetsUnderGrouping.Count)); + set = visibleGroupedSets.ElementAt(RNG.Next(visibleGroupedSets.Count)); break; default: @@ -940,15 +944,15 @@ namespace osu.Game.Screens.SelectV2 var previousBeatmap = randomHistory[^1]; randomHistory.RemoveAt(randomHistory.Count - 1); - var previousBeatmapItem = carouselItems.FirstOrDefault(i => i.Model is BeatmapInfo b && b.Equals(previousBeatmap)); + var previousBeatmapItem = carouselItems.FirstOrDefault(i => i.Model is GroupedBeatmap gb && gb.Equals(previousBeatmap)); if (previousBeatmapItem == null) return false; - if (CurrentSelection is BeatmapInfo beatmapInfo) + if (CurrentSelection is GroupedBeatmap groupedBeatmap) { if (randomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation) - previouslyVisitedRandomBeatmaps.Remove(beatmapInfo); + previouslyVisitedRandomBeatmaps.Remove(groupedBeatmap.Beatmap); if (CurrentSelectionItem == null) playSpinSample(0); diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 947b8f9c7c..5cca9467c2 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -289,9 +289,10 @@ namespace osu.Game.Screens.SelectV2 }); } - private void requestRecommendedSelection(IEnumerable b) + private void requestRecommendedSelection(IEnumerable groupedBeatmaps) { - queueBeatmapSelection(difficultyRecommender?.GetRecommendedBeatmap(b) ?? b.First()); + var recommendedBeatmap = difficultyRecommender?.GetRecommendedBeatmap(groupedBeatmaps.Select(gb => gb.Beatmap)) ?? groupedBeatmaps.First().Beatmap; + queueBeatmapSelection(groupedBeatmaps.First(bug => bug.Beatmap.Equals(recommendedBeatmap))); } /// @@ -472,22 +473,24 @@ namespace osu.Game.Screens.SelectV2 /// - After , update the global beatmap. This in turn causes song select visuals (title, details, leaderboard) to update. /// This debounce is intended to avoid high overheads from churning lookups while a user is changing selection via rapid keyboard operations. /// - /// The beatmap to be selected. - private void queueBeatmapSelection(BeatmapInfo beatmap) + /// The beatmap to be selected. + private void queueBeatmapSelection(GroupedBeatmap groupedBeatmap) { if (!this.IsCurrentScreen()) return; - carousel.CurrentSelection = beatmap; + carousel.CurrentSelection = groupedBeatmap; // Debounce consideration is to avoid beatmap churn on key repeat selection. selectionDebounce?.Cancel(); selectionDebounce = Scheduler.AddDelayed(() => { - if (Beatmap.Value.BeatmapInfo.Equals(beatmap)) + var beatmapInfo = groupedBeatmap.Beatmap; + + if (Beatmap.Value.BeatmapInfo.Equals(beatmapInfo)) return; - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); }, SELECTION_DEBOUNCE); } @@ -532,7 +535,8 @@ namespace osu.Game.Screens.SelectV2 if (validBeatmaps.Any()) { - requestRecommendedSelection(validBeatmaps); + // TODO: this needs a primitive that tells the carousel "I need this beatmap to be selected, you figure out the grouping". + //requestRecommendedSelection(validBeatmaps); return true; } } From 3f637db39162f7f1d6693a9ef22434cf64c4f5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 10:40:36 +0200 Subject: [PATCH 044/267] Fix obvious test failures from using `GroupedBeatmap` in `BeatmapCarousel.CurrentSelection` --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 2 +- .../TestSceneBeatmapCarouselFiltering.cs | 6 +-- .../TestSceneBeatmapCarouselRandom.cs | 18 ++++----- .../TestSceneBeatmapCarouselScrolling.cs | 10 ++--- .../TestSceneBeatmapCarouselUpdateHandling.cs | 40 +++++++++---------- ...neSongSelectCurrentSelectionInvalidated.cs | 2 +- 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index c60ee55110..b616055157 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -300,7 +300,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } else { - AddUntilStep($"selected is set{set}", () => BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection)); + AddUntilStep($"selected is set{set}", () => BeatmapSets[set].Beatmaps.Contains(((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap)); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 70af48069a..b232d12e46 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -201,13 +201,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 int diff = i; AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]); - AddUntilStep("selection changed", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + AddUntilStep("selection changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(chosenBeatmap)); SortBy(SortMode.Difficulty); - AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + AddAssert("selection retained", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(chosenBeatmap)); SortBy(SortMode.Title); - AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + AddAssert("selection retained", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(chosenBeatmap)); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index ed694c9e3d..0e35f5e45d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -50,12 +50,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 nextRandom(); ensureRandomDidNotRepeat(); - AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!); + AddStep("store selection", () => originalSelected = ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap); SortAndGroupBy(SortMode.Artist, GroupMode.Difficulty); WaitForFiltering(); - AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(originalSelected)); + AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(originalSelected)); storeExpandedGroup(); @@ -253,12 +253,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(10, 3, true); WaitForDrawablePanels(); - BeatmapInfo? originalSelected = null; + GroupedBeatmap? originalSelected = null; nextRandom(); CheckHasSelection(); - AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!); + AddStep("store selection", () => originalSelected = ((GroupedBeatmap)Carousel.CurrentSelection!)); AddStep("random then rewind", () => { @@ -275,20 +275,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(10, 3, true); WaitForDrawablePanels(); - BeatmapInfo? originalSelected = null; - BeatmapInfo? postRandomSelection = null; + GroupedBeatmap? originalSelected = null; + GroupedBeatmap? postRandomSelection = null; nextRandom(); CheckHasSelection(); - AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!); + AddStep("store selection", () => originalSelected = (GroupedBeatmap)Carousel.CurrentSelection!); nextRandom(); - AddStep("store selection", () => postRandomSelection = (BeatmapInfo)Carousel.CurrentSelection!); + AddStep("store selection", () => postRandomSelection = (GroupedBeatmap)Carousel.CurrentSelection!); AddAssert("selection changed", () => originalSelected, () => Is.Not.SameAs(postRandomSelection)); - AddStep("delete previous selection beatmaps", () => BeatmapSets.Remove(originalSelected!.BeatmapSet!)); + AddStep("delete previous selection beatmaps", () => BeatmapSets.Remove(originalSelected!.Beatmap.BeatmapSet!)); WaitForFiltering(); AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection)); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index 29aa976fe3..d05c874641 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Quad positionBefore = default; - AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); + AddStep("select middle beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First())); WaitForScrolling(); @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Quad positionBefore = default; - AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); + AddStep("select middle beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First())); WaitForScrolling(); AddStep("override scroll with user scroll", () => @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last()); + AddStep("select last beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.Last().Beatmaps.Last())); WaitForScrolling(); @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Quad positionBefore = default; - AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First()); + AddStep("select first beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.First().Beatmaps.First())); WaitForScrolling(); @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Quad positionBefore = default; - AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First()); + AddStep("select first beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.First().Beatmaps.First())); WaitForScrolling(); AddStep("override scroll with user scroll", () => diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index 3638c8eeec..a331879684 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -179,8 +179,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => { @@ -195,8 +195,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } [Test] // Checks that we keep selection based on online ID where possible. @@ -205,15 +205,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.DifficultyName = "new name"); assertDidFilter(); WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } [Test] // Checks that we fallback to keeping selection based on difficulty name. @@ -222,15 +222,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.OnlineID = b.OnlineID + 1); assertDidFilter(); WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } [Test] // Checks that we don't crash if there exists a difficulty with the same online ID as the selected difficulty. @@ -239,8 +239,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); // Add another difficulty with same online ID. updateBeatmap(null, bs => @@ -252,8 +252,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } [Test] // Checks that we don't crash if there exists a difficulty with the same name as the selected difficulty. @@ -262,8 +262,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); // Remove original selected difficulty, and add two difficulties with same name as selection. updateBeatmap(null, bs => @@ -284,8 +284,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } /// diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs index 0736925584..c480d6ca7e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 /// public partial class TestSceneSongSelectCurrentSelectionInvalidated : SongSelectTestScene { - private BeatmapInfo? selectedBeatmap => (BeatmapInfo?)Carousel.CurrentSelection; + private BeatmapInfo? selectedBeatmap => (Carousel.CurrentSelection as GroupedBeatmap)?.Beatmap; private BeatmapSetInfo? selectedBeatmapSet => selectedBeatmap?.BeatmapSet; [SetUpSteps] From 107e103825cc64c7ebc66fa65e415f93639a9c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 10:59:04 +0200 Subject: [PATCH 045/267] Fix changing group mode causing `CurrentSelection` to retain a stale `GroupDefinition` --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 46ea61ca9d..f58d0bbf1d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -418,9 +418,21 @@ namespace osu.Game.Screens.SelectV2 // Store selected group before handling selection (it may implicitly change the expanded group). var groupForReselection = ExpandedGroup; - // Ensure correct post-selection logic is handled on the new items list. - // This will update the visual state of the selected item. - HandleItemSelected(CurrentSelection); + var currentGroupedBeatmap = CurrentSelection as GroupedBeatmap; + + // The filter might have changed the set of available groups, which means that the current selection may point to a stale group. + // Check whether the current selection's group still exists. + if (currentGroupedBeatmap?.Group != null && grouping.GroupItems.ContainsKey(currentGroupedBeatmap.Group)) + { + // If the group still exists, then only update the visual state of the selected item. + HandleItemSelected(currentGroupedBeatmap); + } + else if (currentGroupedBeatmap != null) + { + // If the group no longer exists, grab an arbitrary other instance of the beatmap under the first group encountered. + var newSelection = GetCarouselItems()?.Select(i => i.Model).OfType().FirstOrDefault(gb => gb.Beatmap.Equals(currentGroupedBeatmap.Beatmap)); + CurrentSelection = newSelection; + } // If a group was selected that is not the one containing the selection, attempt to reselect it. // If the original group was not found, ExpandedGroup will already have been updated to a valid value in `HandleItemSelected` above. From 3cf0a9b9c02621118720087942ecc4cf674dab53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 11:25:15 +0200 Subject: [PATCH 046/267] Fix standalone beatmap panels not having the correct height --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 9fa4e28e93..69f5596578 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -121,11 +121,12 @@ namespace osu.Game.Screens.SelectV2 { if (groupItem != null) groupItem.NestedItemCount++; - - item.DrawHeight = PanelBeatmapStandalone.HEIGHT; } - addItem(new CarouselItem(new GroupedBeatmap(group, beatmap))); + addItem(new CarouselItem(new GroupedBeatmap(group, beatmap)) + { + DrawHeight = BeatmapSetsGroupedTogether ? PanelBeatmap.HEIGHT : PanelBeatmapStandalone.HEIGHT, + }); lastBeatmap = beatmap; displayedBeatmapsCount++; } From dfed564bda7408ec4fd1c82ab15f90f410cdd444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 11:45:48 +0200 Subject: [PATCH 047/267] Allow `BeatmapCarousel.CurrentSelection` to accept raw `BeatmapInfo`s (and redirect to appropriate grouped beatmap) This is probably where things get a little controversial. There are some song select flows wherein song select just wants to ensure sanity by authoritatively setting the global beatmap. The goal is to change the beatmap immediately and instantly. Therefore it should kind of be the carousel's job to figure out its grouping complications. To that end, `CurrentSelection` is made virtual, and overridden in `BeatmapCarousel` to perform a sort of reconciliation logic. If an external component sets `CurrentSelection` to a `BeatmapInfo`, one of the two following things happen: - Nothing, if the current `GroupedBeatmap` is already a copy of the beatmap that needs to be selected, or - The carousel looks at its items, finds any first copy which matches the beatmap that the external consumer wanted selected, and changes selection to that instead. --- osu.Game/Graphics/Carousel/Carousel.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 20 ++++++++++++++++++++ osu.Game/Screens/SelectV2/SongSelect.cs | 3 +-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index b81df0a7eb..5adc37ea40 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -107,7 +107,7 @@ namespace osu.Game.Graphics.Carousel /// The selection is never reset due to not existing. It can be set to anything. /// If no matching carousel item exists, there will be no visually selected item while waiting for potential new item which matches. /// - public object? CurrentSelection + public virtual object? CurrentSelection { get => currentSelection.Model; set diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index f58d0bbf1d..55751718b1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -295,6 +295,26 @@ namespace osu.Game.Screens.SelectV2 protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => grouping.BeatmapSetsGroupedTogether && item.Model is GroupedBeatmap; + public override object? CurrentSelection + { + get => base.CurrentSelection; + set + { + // this is a special pathway for external consumers who only care about showing some particular copy of a beatmap + // (there could be multiple panels for one beatmap due to grouping). + // in this pathway we basically figure out what group to use internally, and continue working with `GroupedBeatmap` all the way after that. + if (value is BeatmapInfo beatmapInfo) + { + if (CurrentSelection is GroupedBeatmap groupedBeatmap && beatmapInfo.Equals(groupedBeatmap.Beatmap)) + return; + + value = GetCarouselItems()?.Select(item => item.Model).OfType().FirstOrDefault(gb => gb.Beatmap.Equals(beatmapInfo)); + } + + base.CurrentSelection = value; + } + } + protected override void HandleItemActivated(CarouselItem item) { try diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 5cca9467c2..64c85dd31a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -535,8 +535,7 @@ namespace osu.Game.Screens.SelectV2 if (validBeatmaps.Any()) { - // TODO: this needs a primitive that tells the carousel "I need this beatmap to be selected, you figure out the grouping". - //requestRecommendedSelection(validBeatmaps); + carousel.CurrentSelection = difficultyRecommender?.GetRecommendedBeatmap(validBeatmaps) ?? validBeatmaps.First(); return true; } } From 1bb24c923dca4943e7d19cdd0ffde9959896500d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 12:23:51 +0200 Subject: [PATCH 048/267] Fix stale group refresh logic inadvertently losing selection entirely sometimes --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 55751718b1..b5f2a3aa25 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -451,7 +451,10 @@ namespace osu.Game.Screens.SelectV2 { // If the group no longer exists, grab an arbitrary other instance of the beatmap under the first group encountered. var newSelection = GetCarouselItems()?.Select(i => i.Model).OfType().FirstOrDefault(gb => gb.Beatmap.Equals(currentGroupedBeatmap.Beatmap)); - CurrentSelection = newSelection; + // Only change the selection if we actually got a positive hit. + // This is necessary so that selection isn't lost if the panel reappears later due to e.g. unapplying some filter criteria that made it disappear in the first place. + if (newSelection != null) + CurrentSelection = newSelection; } // If a group was selected that is not the one containing the selection, attempt to reselect it. From 89492cbd81e5e211286ca4b06c7de35d58e3c446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 12:48:28 +0200 Subject: [PATCH 049/267] Do not attempt to refresh group in current selection if grouping is not relevant --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index b5f2a3aa25..20a3516613 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -441,10 +441,13 @@ namespace osu.Game.Screens.SelectV2 var currentGroupedBeatmap = CurrentSelection as GroupedBeatmap; // The filter might have changed the set of available groups, which means that the current selection may point to a stale group. - // Check whether the current selection's group still exists. - if (currentGroupedBeatmap?.Group != null && grouping.GroupItems.ContainsKey(currentGroupedBeatmap.Group)) + // Check whether that is the case. + bool groupingRemainsOff = currentGroupedBeatmap?.Group == null && grouping.GroupItems.Count == 0; + bool groupStillExists = currentGroupedBeatmap?.Group != null && grouping.GroupItems.ContainsKey(currentGroupedBeatmap.Group); + + if (groupingRemainsOff || groupStillExists) { - // If the group still exists, then only update the visual state of the selected item. + // Only update the visual state of the selected item. HandleItemSelected(currentGroupedBeatmap); } else if (currentGroupedBeatmap != null) From 2b52c1de0b123a7e7699de913a0f7d7d0bae03d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 13:45:27 +0200 Subject: [PATCH 050/267] Fix presenting individual beatmaps from main menu breaking In this case `CurrentSelection` is being set on the song select screen's `OnEntering()`, at which point grouping is not yet known. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 20a3516613..1cd9396e5a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -308,7 +308,12 @@ namespace osu.Game.Screens.SelectV2 if (CurrentSelection is GroupedBeatmap groupedBeatmap && beatmapInfo.Equals(groupedBeatmap.Beatmap)) return; - value = GetCarouselItems()?.Select(item => item.Model).OfType().FirstOrDefault(gb => gb.Beatmap.Equals(beatmapInfo)); + // it is not universally guaranteed that the carousel items will be materialised at the time this is set. + // therefore, in cases where it is known that they will not be, default to a null group. + // even if grouping is active, this will be rectified to a correct group on the next invocation of `HandleFilterCompleted()`. + value = IsLoaded && !IsFiltering + ? GetCarouselItems()?.Select(item => item.Model).OfType().FirstOrDefault(gb => gb.Beatmap.Equals(beatmapInfo)) + : new GroupedBeatmap(null, beatmapInfo); } base.CurrentSelection = value; From a27fef243780ab4b4915633b1b3cac15123be70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 14:06:53 +0200 Subject: [PATCH 051/267] Add failing test for rewind not working over grouping mode changes --- .../TestSceneBeatmapCarouselRandom.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index 0e35f5e45d..9f31c875b6 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -247,6 +247,30 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } } + [Test] + public void TestRewindOverGroupingModeChange() + { + const int local_set_count = 3; + + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); + AddBeatmaps(local_set_count, 3); + WaitForDrawablePanels(); + + SelectNextSet(); + + for (int i = 0; i < local_set_count; i++) + nextRandom(); + + SortAndGroupBy(SortMode.Title, GroupMode.LastPlayed); + WaitForDrawablePanels(); + + for (int i = 0; i < local_set_count; i++) + { + prevRandomSet(); + checkRewindCorrectSet(); + } + } + [Test] public void TestRandomThenRewindSameFrame() { From 41b8033ebdae5249e86d8b3e5e0fd02d24563b28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 21:21:47 +0900 Subject: [PATCH 052/267] Adjust interpolation workaround to catch-up slightly smoother --- osu.Android.props | 2 +- osu.Game/Beatmaps/FramedBeatmapClock.cs | 5 ++++- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 40a9b454ce..46d558354e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 7545031cf3..3768550c21 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4ffc262073804e5fdad0e62a86832e81618635a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 14:11:40 +0200 Subject: [PATCH 053/267] Fix rewind not working over grouping mode changes --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 1cd9396e5a..6c98630274 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -987,7 +987,11 @@ namespace osu.Game.Screens.SelectV2 var previousBeatmap = randomHistory[^1]; randomHistory.RemoveAt(randomHistory.Count - 1); - var previousBeatmapItem = carouselItems.FirstOrDefault(i => i.Model is GroupedBeatmap gb && gb.Equals(previousBeatmap)); + // when going back through rewind history, we may no longer be in the same grouping mode. + // the user wants to go back to the beatmap first and foremost, so the most important thing is to find a panel that corresponds to the beatmap. + // going back to the same group is a nice-to-have, but a secondary concern. + var previousBeatmapItem = carouselItems.Where(i => i.Model is GroupedBeatmap gb && gb.Beatmap.Equals(previousBeatmap.Beatmap)) + .MaxBy(i => ((GroupedBeatmap)i.Model).Group == previousBeatmap.Group); if (previousBeatmapItem == null) return false; @@ -1003,7 +1007,7 @@ namespace osu.Game.Screens.SelectV2 playSpinSample(visiblePanelCountBetweenItems(previousBeatmapItem, CurrentSelectionItem)); } - RequestSelection(previousBeatmap); + RequestSelection((GroupedBeatmap)previousBeatmapItem.Model); return true; } From d6b4c2958dd28f13bee911c8236c254a0359004a Mon Sep 17 00:00:00 2001 From: marvin Date: Sat, 30 Aug 2025 18:48:46 +0200 Subject: [PATCH 054/267] Fix crash when marking previous objects as hit --- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 02eb38ffa6..9d9202d597 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -14,7 +14,6 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -55,12 +54,18 @@ namespace osu.Game.Screens.Edit.GameplayTest return masterGameplayClockContainer; } + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + + preventMissOnPreviousHitObjects(); + } + protected override void LoadComplete() { base.LoadComplete(); markPreviousObjectsHit(); - markVisibleDrawableObjectsHit(); ScoreProcessor.HasCompleted.BindValueChanged(completed => { @@ -111,38 +116,39 @@ namespace osu.Game.Screens.Edit.GameplayTest } } - private void markVisibleDrawableObjectsHit() + private void preventMissOnPreviousHitObjects() { - if (!DrawableRuleset.Playfield.IsLoaded) + void preventMiss(HitObject hitObject) { - Schedule(markVisibleDrawableObjectsHit); - return; - } + if (hitObject.StartTime > editorState.Time) + return; - foreach (var drawableObject in enumerateDrawableObjects(DrawableRuleset.Playfield.AllHitObjects, editorState.Time)) - { - if (drawableObject.Entry == null) - continue; + var drawableObject = DrawableRuleset.Playfield.HitObjectContainer + .AliveObjects + .LastOrDefault(it => it.HitObject == hitObject); + + if (drawableObject?.Entry == null) + return; var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); result.Type = result.Judgement.MaxResult; drawableObject.Entry.Result = result; } - static IEnumerable enumerateDrawableObjects(IEnumerable drawableObjects, double cutoffTime) + void removeListener() { - foreach (var drawableObject in drawableObjects) + if (!DrawableRuleset.Playfield.IsLoaded) { - foreach (var nested in enumerateDrawableObjects(drawableObject.NestedHitObjects, cutoffTime)) - { - if (nested.HitObject.GetEndTime() < cutoffTime) - yield return nested; - } - - if (drawableObject.HitObject.GetEndTime() < cutoffTime) - yield return drawableObject; + Schedule(removeListener); + return; } + + DrawableRuleset.Playfield.HitObjectUsageBegan -= preventMiss; } + + DrawableRuleset.Playfield.HitObjectUsageBegan += preventMiss; + + Schedule(removeListener); } protected override void PrepareReplay() From 2fb481e2eeba9d84302c9444ca0d3379c978b71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Ptach-=C5=BBurakowski?= Date: Sat, 30 Aug 2025 20:35:51 +0200 Subject: [PATCH 055/267] Add Secondary Keys for Mania --- .../DualStageVariantGenerator.cs | 23 +++++++++++++++++++ .../SingleStageVariantGenerator.cs | 9 ++++++++ .../VariantMappingGenerator.cs | 21 ++++++++++++++--- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs index 6a7634da01..345657cc58 100644 --- a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs @@ -14,6 +14,11 @@ namespace osu.Game.Rulesets.Mania private readonly InputKey[] stage1RightKeys; private readonly InputKey[] stage2LeftKeys; private readonly InputKey[] stage2RightKeys; + private readonly InputKey[] stage1SecondaryLeftKeys; + private readonly InputKey[] stage1SecondaryRightKeys; + private readonly InputKey[] stage2SecondaryLeftKeys; + private readonly InputKey[] stage2SecondaryRightKeys; + public DualStageVariantGenerator(int singleStageVariant) { @@ -27,6 +32,12 @@ namespace osu.Game.Rulesets.Mania stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G, InputKey.B }; stage2RightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; + + stage1SecondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; + stage1SecondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; + + stage2SecondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; + stage2SecondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; } else { @@ -35,6 +46,12 @@ namespace osu.Game.Rulesets.Mania stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G }; stage2RightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; + + stage1SecondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; + stage1SecondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; + + stage2SecondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; + stage2SecondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; } } @@ -44,14 +61,20 @@ namespace osu.Game.Rulesets.Mania { LeftKeys = stage1LeftKeys, RightKeys = stage1RightKeys, + SecondaryLeftKeys = stage1SecondaryLeftKeys, + SecondaryRightKeys = stage1SecondaryRightKeys, SpecialKey = InputKey.V, + SecondarySpecialKey = InputKey.Space }.GenerateKeyBindingsFor(singleStageVariant); var stage2Bindings = new VariantMappingGenerator { LeftKeys = stage2LeftKeys, RightKeys = stage2RightKeys, + SecondaryLeftKeys = stage2SecondaryLeftKeys, + SecondaryRightKeys = stage2SecondaryRightKeys, SpecialKey = InputKey.B, + SecondarySpecialKey = InputKey.Enter, ActionStart = (ManiaAction)singleStageVariant, }.GenerateKeyBindingsFor(singleStageVariant); diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs index c642da6dc4..06b51dca76 100644 --- a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs @@ -10,7 +10,9 @@ namespace osu.Game.Rulesets.Mania { private readonly int variant; private readonly InputKey[] leftKeys; + private readonly InputKey[] secondaryLeftKeys; private readonly InputKey[] rightKeys; + private readonly InputKey[] secondaryRightKeys; public SingleStageVariantGenerator(int variant) { @@ -21,19 +23,26 @@ namespace osu.Game.Rulesets.Mania { leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V }; rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; + secondaryLeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.G }; + secondaryRightKeys = new[] { InputKey.H, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft }; } else { leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F }; rightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; + secondaryLeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R }; + secondaryRightKeys = new[] { InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft }; } } public IEnumerable GenerateMappings() => new VariantMappingGenerator { LeftKeys = leftKeys, + SecondaryLeftKeys = secondaryLeftKeys, RightKeys = rightKeys, + SecondaryRightKeys = secondaryRightKeys, SpecialKey = InputKey.Space, + SecondarySpecialKey = InputKey.Enter }.GenerateKeyBindingsFor(variant); } } diff --git a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs index 2195c9e1b9..a8146497c1 100644 --- a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs +++ b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs @@ -15,16 +15,22 @@ namespace osu.Game.Rulesets.Mania /// public InputKey[] LeftKeys; + public InputKey[] SecondaryLeftKeys; + /// /// All the s available to the right hand. /// public InputKey[] RightKeys; + public InputKey[] SecondaryRightKeys; + /// /// The for the special key. /// public InputKey SpecialKey; + public InputKey SecondarySpecialKey; + /// /// The at which the columns should begin. /// @@ -42,13 +48,22 @@ namespace osu.Game.Rulesets.Mania var bindings = new List(); for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++) - bindings.Add(new KeyBinding(LeftKeys[i], currentAction++)); + { + bindings.Add(new KeyBinding(LeftKeys[i], currentAction)); + bindings.Add(new KeyBinding(SecondaryLeftKeys[i], currentAction++)); + } if (columns % 2 == 1) - bindings.Add(new KeyBinding(SpecialKey, currentAction++)); + { + bindings.Add(new KeyBinding(SpecialKey, currentAction)); + bindings.Add(new KeyBinding(SecondarySpecialKey, currentAction++)); + } for (int i = 0; i < columns / 2; i++) - bindings.Add(new KeyBinding(RightKeys[i], currentAction++)); + { + bindings.Add(new KeyBinding(RightKeys[i], currentAction)); + bindings.Add(new KeyBinding(SecondaryRightKeys[i], currentAction++)); + } return bindings; } From 08ad27459e4626efd248bcf2f0ddd2b70f01e509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Ptach-=C5=BBurakowski?= Date: Sat, 30 Aug 2025 20:36:29 +0200 Subject: [PATCH 056/267] Code quality --- osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs index 345657cc58..763f9f288b 100644 --- a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs @@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Mania private readonly InputKey[] stage2SecondaryLeftKeys; private readonly InputKey[] stage2SecondaryRightKeys; - public DualStageVariantGenerator(int singleStageVariant) { this.singleStageVariant = singleStageVariant; From b02093505db132e089b269b413db932f99cd849d Mon Sep 17 00:00:00 2001 From: NiyazBiyaz Date: Sun, 31 Aug 2025 17:17:17 +0500 Subject: [PATCH 057/267] Add `OperationInProgress` checking --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 6 +++++- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index ae8ad2c01b..5c5b6edcc3 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); ScheduleAfterChildren(() => angleInput.TakeFocus()); - angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); + angleInput.Current.BindValueChanged(angle => + { + if (rotationHandler.OperationInProgress.Value) + rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }; + }); rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => { diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ac6d9fbb19..86114f1dca 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -140,7 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); ScheduleAfterChildren(() => scaleInput.TakeFocus()); - scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); + scaleInput.Current.BindValueChanged(scale => + { + if (scaleHandler.OperationInProgress.Value) + scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }; + }); xCheckBox.Current.BindValueChanged(_ => { From c7f1210281988e85060021d6f3cf121547280a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Ptach-=C5=BBurakowski?= Date: Sun, 31 Aug 2025 21:19:26 +0200 Subject: [PATCH 058/267] Better secondary keybinds for 10K --- osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs index 06b51dca76..d5c0c16f64 100644 --- a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs @@ -23,8 +23,8 @@ namespace osu.Game.Rulesets.Mania { leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V }; rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; - secondaryLeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.G }; - secondaryRightKeys = new[] { InputKey.H, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft }; + secondaryLeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.B }; + secondaryRightKeys = new[] { InputKey.M, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft }; } else { From 12430ce464326d468e13c0564e623854687ab61d Mon Sep 17 00:00:00 2001 From: NiyazBiyaz Date: Mon, 1 Sep 2025 13:55:29 +0500 Subject: [PATCH 059/267] Move guard to `scale/rotationInfo` --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 10 +++++----- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 5c5b6edcc3..ba67bf1f2d 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -97,11 +97,7 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); ScheduleAfterChildren(() => angleInput.TakeFocus()); - angleInput.Current.BindValueChanged(angle => - { - if (rotationHandler.OperationInProgress.Value) - rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }; - }); + angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => { @@ -161,6 +157,10 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { + // can happen if the popover is dismessed by a keyboard key press while dragging UI controls + if (!rotationHandler.OperationInProgress.Value) + return; + rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue)); }); } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 86114f1dca..ca4a99b9cd 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -140,11 +140,7 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); ScheduleAfterChildren(() => scaleInput.TakeFocus()); - scaleInput.Current.BindValueChanged(scale => - { - if (scaleHandler.OperationInProgress.Value) - scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }; - }); + scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); xCheckBox.Current.BindValueChanged(_ => { @@ -224,6 +220,10 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInfo.BindValueChanged(scale => { + // can happen if the popover is dismissed by a keyboard key press while dragging UI controls + if (!scaleHandler.OperationInProgress.Value) + return; + var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale); scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue)); }); From 677c008b4d5ab684405731d85eb10e7859e1bbd5 Mon Sep 17 00:00:00 2001 From: NiyazBiyaz Date: Mon, 1 Sep 2025 13:57:14 +0500 Subject: [PATCH 060/267] Fix movement via slider while popover closes --- osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index 04d6afc925..a3282734be 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -176,6 +176,11 @@ namespace osu.Game.Rulesets.Osu.Edit private void applyPosition() { + // can happen if popover disabled by a keyboard key press while dragging UI controls + // it doesn't cause a crash, but it looks wrong + if (!editorBeatmap.TransactionActive) + return; + editorBeatmap.PerformOnSelection(ho => { if (!initialPositions.TryGetValue(ho, out var initialPosition)) From 0021434a62ea4aea65281957b9ac5dff4ba4121f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 17:54:24 +0900 Subject: [PATCH 061/267] Fix cancellation token not actually being used --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index ef00064ced..b2dc8404e4 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -1011,7 +1011,7 @@ namespace osu.Game.Screens.SelectV2 lastLookupResult.Value = BeatmapSetLookupResult.InProgress(); onlineLookupCancellation = new CancellationTokenSource(); - currentOnlineLookup = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID); + currentOnlineLookup = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID, onlineLookupCancellation.Token); currentOnlineLookup.ContinueWith(t => { if (t.IsCompletedSuccessfully) From 9d0043d03bf74b5f0f86aa6018c2d3cd212e0cde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 17:58:41 +0900 Subject: [PATCH 062/267] Cancel underlying web request on local cancellation of lookup request --- osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs index 486dfbe255..832095058a 100644 --- a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs +++ b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs @@ -43,6 +43,8 @@ namespace osu.Game.Screens.SelectV2 var request = new GetBeatmapSetRequest(id); var tcs = new TaskCompletionSource(); + token.Register(() => request.Cancel()); + // async request success callback is a bit of a dangerous game, but there's some reasoning for it. // - don't really want to use `IAPIAccess.PerformAsync()` because we still want to respect request queueing & online status checks // - we want the realm write here to be async because it is known to be slow for some users with large beatmap collections From 9827f9f189f2e55dab9ad35106af4ac595d7afff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 11:10:30 +0200 Subject: [PATCH 063/267] Recursively update hit results for nested drawables --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 9d9202d597..66e04d1c09 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -14,6 +14,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -127,12 +128,20 @@ namespace osu.Game.Screens.Edit.GameplayTest .AliveObjects .LastOrDefault(it => it.HitObject == hitObject); + preventMissOnDrawable(drawableObject); + } + + void preventMissOnDrawable(DrawableHitObject? drawableObject) + { if (drawableObject?.Entry == null) return; var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); result.Type = result.Judgement.MaxResult; drawableObject.Entry.Result = result; + + foreach (var nested in drawableObject.NestedHitObjects) + preventMissOnDrawable(nested); } void removeListener() From da7e256302f5045a50ca0d7c49b9fe8c6038f729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 11:11:26 +0200 Subject: [PATCH 064/267] Move `markPreviousObjectsHit` into `LoadAsyncComplete` --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 66e04d1c09..8e0a71ddd3 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -60,14 +60,13 @@ namespace osu.Game.Screens.Edit.GameplayTest base.LoadAsyncComplete(); preventMissOnPreviousHitObjects(); + markPreviousObjectsHit(); } protected override void LoadComplete() { base.LoadComplete(); - markPreviousObjectsHit(); - ScoreProcessor.HasCompleted.BindValueChanged(completed => { if (completed.NewValue) From 689cc27e6806c236b14ff5eab067df5f2236eaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 11:12:19 +0200 Subject: [PATCH 065/267] Prevent npr in tests due to drawable ruleset not being available --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 8e0a71ddd3..5f0139a100 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -59,8 +59,11 @@ namespace osu.Game.Screens.Edit.GameplayTest { base.LoadAsyncComplete(); - preventMissOnPreviousHitObjects(); - markPreviousObjectsHit(); + if (DrawableRuleset != null) + { + preventMissOnPreviousHitObjects(); + markPreviousObjectsHit(); + } } protected override void LoadComplete() From e2d661736e56f2643bfd982f96799c1f87e06322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 1 Sep 2025 12:01:36 +0200 Subject: [PATCH 066/267] Fix typos --- osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index a3282734be..f3739ab445 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void applyPosition() { - // can happen if popover disabled by a keyboard key press while dragging UI controls + // can happen if popover is dismissed by a keyboard key press while dragging UI controls // it doesn't cause a crash, but it looks wrong if (!editorBeatmap.TransactionActive) return; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index ba67bf1f2d..e2cde1a325 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { - // can happen if the popover is dismessed by a keyboard key press while dragging UI controls + // can happen if the popover is dismissed by a keyboard key press while dragging UI controls if (!rotationHandler.OperationInProgress.Value) return; From 060854f23a2029692eb0944b7542c5fcdb573e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 12:09:33 +0200 Subject: [PATCH 067/267] Revert moving `markPreviousObjectsHit` into `LoadAsyncComplete` Running that there caused a test failure due to modifying drawables' transforms outside the update thread --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 5f0139a100..b99c0afdeb 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -60,16 +60,17 @@ namespace osu.Game.Screens.Edit.GameplayTest base.LoadAsyncComplete(); if (DrawableRuleset != null) - { preventMissOnPreviousHitObjects(); - markPreviousObjectsHit(); - } } protected override void LoadComplete() { base.LoadComplete(); + // this will notify components such as the skin's combo counter, which needs to happen on the update thread + // and therefore can't happen alongside `preventMissOnPreviousHitObjects()` in `LoadAsyncComplete()` + markPreviousObjectsHit(); + ScoreProcessor.HasCompleted.BindValueChanged(completed => { if (completed.NewValue) From 5079a53cca81691e768e5e2e5ccdaaa2183bc439 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 19:37:35 +0900 Subject: [PATCH 068/267] Add more panel types to `TestSceneRoomPanel` --- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 71 +++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index 58eb0f1ea1..6eb356d28f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Beatmaps; using osuTK; @@ -38,10 +39,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create rooms", () => { - PlaylistItem item1 = new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) + PlaylistItem item1 = new PlaylistItem(new APIBeatmap { - BeatmapInfo = { StarRating = 2.5 } - }.BeatmapInfo); + OnlineBeatmapSetID = 173612, + OnlineID = 502132, + }); PlaylistItem item2 = new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { @@ -72,6 +74,14 @@ namespace osu.Game.Tests.Visual.Multiplayer Spacing = new Vector2(10), Children = new Drawable[] { + createMultiplayerPanel(new Room + { + Name = "Multiplayer room", + EndDate = DateTimeOffset.Now.AddDays(1), + Type = MatchType.HeadToHead, + Playlist = [item1], + CurrentPlaylistItem = item1 + }), createLoungeRoom(new Room { Name = "Multiplayer room", @@ -98,6 +108,14 @@ namespace osu.Game.Tests.Visual.Multiplayer Playlist = [item3], CurrentPlaylistItem = item3 }), + createPlaylistRoomPanel(new Room + { + Name = "Playlist room with multiple beatmaps", + Status = RoomStatus.Playing, + EndDate = DateTimeOffset.Now.AddDays(1), + Playlist = [item1, item2], + CurrentPlaylistItem = item1 + }), createLoungeRoom(new Room { Name = "Playlist room with multiple beatmaps", @@ -131,8 +149,10 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for panel load", () => rooms.Count, () => Is.EqualTo(8)); - AddUntilStep("\"currently playing\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(3)); - AddUntilStep("\"ready to play\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)), () => Is.EqualTo(4)); + AddUntilStep("\"currently playing\" room count correct", + () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(3)); + AddUntilStep("\"ready to play\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)), + () => Is.EqualTo(4)); } [Test] @@ -207,7 +227,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { new MultiplayerRoomPanel(new Room { - Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", + Name = + "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", QueueMode = QueueMode.HostOnly, Type = MatchType.HeadToHead, RoomID = 1337, @@ -231,7 +252,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { new MultiplayerRoomPanel(room = new Room { - Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", + Name = + "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", QueueMode = QueueMode.HostOnly, Type = MatchType.HeadToHead, }), @@ -243,6 +265,41 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("clear room ID", () => room.RoomID = null); } + private RoomPanel createPlaylistRoomPanel(Room room) + { + room.Host ??= new APIUser { Username = "peppy", Id = 2 }; + + if (room.RecentParticipants.Count == 0) + { + room.RecentParticipants = Enumerable.Range(0, 20).Select(i => new APIUser + { + Id = i, + Username = $"User {i}" + }).ToArray(); + } + + return new PlaylistsRoomPanel(room) + { + SelectedItem = new Bindable(room.CurrentPlaylistItem), + }; + } + + private RoomPanel createMultiplayerPanel(Room room) + { + room.Host ??= new APIUser { Username = "peppy", Id = 2 }; + + if (room.RecentParticipants.Count == 0) + { + room.RecentParticipants = Enumerable.Range(0, 20).Select(i => new APIUser + { + Id = i, + Username = $"User {i}" + }).ToArray(); + } + + return new MultiplayerRoomPanel(room); + } + private RoomPanel createLoungeRoom(Room room) { room.Host ??= new APIUser { Username = "peppy", Id = 2 }; From 209ba76b219f5c5357938816c6c24539ea79c2ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 19:37:47 +0900 Subject: [PATCH 069/267] Reduce size of online play screen's header --- osu.Game/Screens/OnlinePlay/Header.cs | 2 +- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index 860042fd37..825f809397 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay { public partial class Header : Container { - public const float HEIGHT = 80; + public const float HEIGHT = 50; private readonly ScreenStack? stack; private readonly MultiHeaderTitle title; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index a4e808ff76..b4b039501f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -173,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { d.Anchor = Anchor.BottomLeft; d.Origin = Anchor.BottomLeft; - d.Size = new Vector2(150, 37.5f); + d.Size = new Vector2(150, 30f); d.Action = () => Open(); })), new FillFlowContainer From 659480fa3f137007873977f602b6359c12785a3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 19:38:18 +0900 Subject: [PATCH 070/267] Adjust sizing of room panels and other elements to make things fit better on mobile layouts --- .../DrawableRoomParticipantsList.cs | 20 ++++++------- .../Lounge/Components/RoomListing.cs | 6 +--- .../OnlinePlay/Lounge/Components/RoomPanel.cs | 28 +++++++++---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 5bcc974c26..135b2b4db2 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -23,10 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public partial class DrawableRoomParticipantsList : CompositeDrawable { - public const float SHEAR_WIDTH = 12f; - private const float avatar_size = 36; - private const float height = 60f; - private static readonly Vector2 shear = new Vector2(SHEAR_WIDTH / height, 0); + private const float avatar_size = 30; + private const float height = 40f; private readonly Room room; @@ -54,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, - Shear = shear, + Shear = OsuGame.SHEAR, Child = new Box { RelativeSizeAxes = Axes.Both, @@ -71,10 +69,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Spacing = new Vector2(8), + Spacing = new Vector2(4), Padding = new MarginPadding { - Left = 8, + Left = 4, Right = 16 }, Children = new Drawable[] @@ -84,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - hostText = new LinkFlowContainer + hostText = new LinkFlowContainer(s => s.Font = OsuFont.Style.Caption2) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -103,7 +101,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, - Shear = shear, + Shear = OsuGame.SHEAR, Child = new Box { RelativeSizeAxes = Axes.Both, @@ -128,12 +126,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(16), + Size = new Vector2(12), Icon = FontAwesome.Solid.User, }, totalCount = new OsuSpriteText { - Font = OsuFont.Default.With(weight: FontWeight.Bold), + Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index b93d26880d..f04de97f9b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -45,8 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly ScrollContainer scroll; private readonly FillFlowContainer roomFlow; - private const float display_scale = 0.8f; - // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; @@ -58,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Width = display_scale, + Width = 0.8f, ScrollbarOverlapsContent = false, Padding = new MarginPadding { Right = 5 }, Child = new OsuContextMenuContainer @@ -188,8 +186,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedRoom = selectedRoom, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = new Vector2(display_scale), - Width = 1 / display_scale, }; roomFlow.Add(drawableRoom); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index 258c9c3a97..fe03fca4b8 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -31,7 +31,6 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; using osuTK; -using osuTK.Graphics; using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Lounge.Components @@ -39,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public abstract partial class RoomPanel : CompositeDrawable, IHasContextMenu { protected const float CORNER_RADIUS = 10; - private const float height = 100; + private const float height = 80; [Resolved] private IAPIProvider api { get; set; } = null!; @@ -80,12 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Masking = true; CornerRadius = CORNER_RADIUS; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, - }; } [BackgroundDependencyLoader] @@ -99,6 +92,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components AutoSizeAxes = Axes.X }; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = colourProvider.Background6.Opacity(0.4f), + Radius = 4, + }; + InternalChildren = new Drawable[] { // This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites. @@ -118,7 +118,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Name = @"Room content", RelativeSizeAxes = Axes.Both, // This negative padding resolves 1px gaps between this background and the background above. - Padding = new MarginPadding { Left = 20, Vertical = -0.5f }, + Padding = new MarginPadding { Left = 10, Vertical = -0.5f }, Child = new Container { RelativeSizeAxes = Axes.Both, @@ -158,8 +158,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Left = 20, - Right = DrawableRoomParticipantsList.SHEAR_WIDTH, + Left = 10, + Right = 10, Vertical = 5 }, Children = new Drawable[] @@ -516,12 +516,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { statusText = new OsuSpriteText { - Font = OsuFont.Default.With(size: 16), + Font = OsuFont.Style.Caption2, Colour = colours.Lime1 }, beatmapText = new LinkFlowContainer(s => { - s.Font = OsuFont.Default.With(size: 16); + s.Font = OsuFont.Style.Caption2; s.Colour = colours.Lime1; }) { @@ -636,7 +636,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(size: 28), + Font = OsuFont.Style.Heading2, }, linkButton = new ExternalLinkButton { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 689a8df12f..bbac86fd2d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -280,7 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new AddItemButton { RelativeSizeAxes = Axes.X, - Height = 40, + Height = 30, Text = "Add item", Action = () => ShowSongSelect() }, From cf471066bfac41a007c3d5277355c9fcfe01c918 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 19:38:27 +0900 Subject: [PATCH 071/267] Add basic spacing between participants in list --- .../OnlinePlay/Multiplayer/Participants/ParticipantsList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index b553fcc9cd..7429fc817c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private MultiplayerClient client { get; set; } = null!; public ParticipantsList() - : base(ParticipantPanel.HEIGHT, initialPoolSize: 20) + : base(ParticipantPanel.HEIGHT + 1, initialPoolSize: 20) { } From 385529ec7813d49beba1692328100ca1e2f7745f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 13:05:22 +0200 Subject: [PATCH 072/267] Fix mismatch in cutoff time check between `preventMissOnPreviousHitObjects` and `markPreviousObjectsHit` --- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index b99c0afdeb..589ce34450 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -124,27 +124,28 @@ namespace osu.Game.Screens.Edit.GameplayTest { void preventMiss(HitObject hitObject) { - if (hitObject.StartTime > editorState.Time) - return; - var drawableObject = DrawableRuleset.Playfield.HitObjectContainer .AliveObjects .LastOrDefault(it => it.HitObject == hitObject); - preventMissOnDrawable(drawableObject); + if (drawableObject != null) + preventMissOnDrawable(drawableObject); } - void preventMissOnDrawable(DrawableHitObject? drawableObject) + void preventMissOnDrawable(DrawableHitObject drawableObject) { - if (drawableObject?.Entry == null) + if (drawableObject.Entry == null) return; - var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); - result.Type = result.Judgement.MaxResult; - drawableObject.Entry.Result = result; - foreach (var nested in drawableObject.NestedHitObjects) preventMissOnDrawable(nested); + + if (drawableObject.HitObject.GetEndTime() < editorState.Time) + { + var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); + result.Type = result.Judgement.MaxResult; + drawableObject.Entry.Result = result; + } } void removeListener() From ffb6ae206682218d93f451d102391753c5925e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 13:13:28 +0200 Subject: [PATCH 073/267] Move null check after loop over nested hitobjects --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 589ce34450..90996fda6f 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -134,13 +134,10 @@ namespace osu.Game.Screens.Edit.GameplayTest void preventMissOnDrawable(DrawableHitObject drawableObject) { - if (drawableObject.Entry == null) - return; - foreach (var nested in drawableObject.NestedHitObjects) preventMissOnDrawable(nested); - if (drawableObject.HitObject.GetEndTime() < editorState.Time) + if (drawableObject.Entry != null && drawableObject.HitObject.GetEndTime() < editorState.Time) { var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); result.Type = result.Judgement.MaxResult; From a008a66fb27e55e06fc84665b2e3bf842c46afad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 1 Sep 2025 13:50:42 +0200 Subject: [PATCH 074/267] Fix test --- osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index 6eb356d28f..aa9dddae4d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -148,11 +148,11 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); - AddUntilStep("wait for panel load", () => rooms.Count, () => Is.EqualTo(8)); + AddUntilStep("wait for panel load", () => rooms.Count, () => Is.EqualTo(10)); AddUntilStep("\"currently playing\" room count correct", - () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(3)); + () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(4)); AddUntilStep("\"ready to play\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)), - () => Is.EqualTo(4)); + () => Is.EqualTo(5)); } [Test] From cac136d3c6026cf2bb8b8e35d736520e5ebdc67c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Sep 2025 09:21:31 +0900 Subject: [PATCH 075/267] Fix editor memory leak --- .../Compose/Components/Timeline/SamplePointPiece.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 5e8637c1ac..cdd2f52dab 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -57,9 +56,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime; [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { - HitObject.DefaultsApplied += _ => updateText(); Label.AllowMultiline = false; LabelContainer.AutoSizeAxes = Axes.None; updateText(); @@ -74,6 +72,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); + HitObject.DefaultsApplied += onDefaultsApplied; + if (timelineBlueprintContainer != null) contracted.BindTo(timelineBlueprintContainer.SamplePointContracted); @@ -96,12 +96,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline FinishTransforms(); } + private void onDefaultsApplied(HitObject hitObject) + { + updateText(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (editor != null) editor.ShowSampleEditPopoverRequested -= onShowSampleEditPopoverRequested; + + HitObject.DefaultsApplied -= onDefaultsApplied; } private void onShowSampleEditPopoverRequested(double time) From f9e89afe03c13e544656e31071fc86048ebba211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Ptach-=C5=BBurakowski?= <69014595+kptach@users.noreply.github.com> Date: Tue, 2 Sep 2025 03:10:58 +0200 Subject: [PATCH 076/267] Add increase visibility setting for taiko hidden (#34879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Mods/TestSceneTaikoModHidden.cs | 104 ++++++++++++++++++ .../Mods/TaikoModHidden.cs | 2 +- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs index e6d5c51902..5336ea604e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs @@ -5,10 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Tests.Mods { @@ -69,5 +72,106 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods }, }); } + + [Test] + public void TestIncreasedVisibilityOnFirstObject() + { + bool firstHitNeverFadedOut = true; + AddStep("enable increased visibility", () => LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, true)); + CreateModTest(new ModTestData + { + Mod = new TaikoModHidden(), + Autoplay = true, + PassCondition = () => + { + var firstHit = this.ChildrenOfType().FirstOrDefault(h => h.HitObject.StartTime == 100); + + if (firstHit?.Alpha < 1 && !firstHit.IsHit) + firstHitNeverFadedOut = false; + + return firstHitNeverFadedOut && checkAllMaxResultJudgements(2).Invoke(); + }, + CreateBeatmap = () => + { + var beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + Type = HitType.Rim, + StartTime = 100, + }, + new Hit + { + Type = HitType.Centre, + StartTime = 200, + }, + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderTickRate = 4, + OverallDifficulty = 0, + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + return beatmap; + }, + }); + } + + [Test] + public void TestNoIncreasedVisibilityOnFirstObject() + { + bool firstHitFadedOut = true; + AddStep("enable increased visibility", () => LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false)); + CreateModTest(new ModTestData + { + Mod = new TaikoModHidden(), + Autoplay = true, + PassCondition = () => + { + var firstHit = this.ChildrenOfType().FirstOrDefault(h => h.HitObject.StartTime == 100); + firstHitFadedOut |= firstHit?.IsHit == false && firstHit.Alpha < 1; + return firstHitFadedOut && checkAllMaxResultJudgements(2).Invoke(); + }, + CreateBeatmap = () => + { + var beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + Type = HitType.Rim, + StartTime = 100, + }, + new Hit + { + Type = HitType.Centre, + StartTime = 200, + }, + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderTickRate = 4, + OverallDifficulty = 0, + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + return beatmap; + }, + }); + } } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 2c3b4a8d18..8b6fb71d51 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Mods protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { - ApplyNormalVisibilityState(hitObject, state); + // intentional no-op } protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) From b0dcd06b383e1585bc9b281a2fc62314f019e56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 08:02:32 +0200 Subject: [PATCH 077/267] Add one more comment --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 90996fda6f..0b6c9960c8 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -59,6 +59,8 @@ namespace osu.Game.Screens.Edit.GameplayTest { base.LoadAsyncComplete(); + // `preventMissOnPreviousHitObjects()` needs to be called to install its hooks before drawable hit objects get the chance to run update logic, + // because it will not work otherwise due to being too late (various effects of the objects getting missed will have already taken place). if (DrawableRuleset != null) preventMissOnPreviousHitObjects(); } From 903d91b69784dd537edaa17bd0a178c7eb7d5a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 08:05:37 +0200 Subject: [PATCH 078/267] Use `SingleOrDefault()` instead of `LastOrDefault()` `LastOrDefault()` is arbitrary, and I hope this doesn't matter for anything, because if it does, then that's utterly *horrifying*. --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 0b6c9960c8..525f6f62ad 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit.GameplayTest { var drawableObject = DrawableRuleset.Playfield.HitObjectContainer .AliveObjects - .LastOrDefault(it => it.HitObject == hitObject); + .SingleOrDefault(it => it.HitObject == hitObject); if (drawableObject != null) preventMissOnDrawable(drawableObject); From a8ef57ad0a3fc6e95a0b2dae0076ee2b2b4d6d91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Sep 2025 17:36:08 +0900 Subject: [PATCH 079/267] Revert "Adjust bass invalid data threshold" This reverts commit ddce11fbc8e9aabbb2e4e943b2901ff701987685. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 329a41ef28..7f29ed3703 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.UI // // In testing this triggers *very* rarely even when set to super low values (10 ms). The cases we're worried about involve multi-second jumps. // A difference of more than 500 ms seems like a sane number we should never exceed. - if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 1500) + if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500) { if (invalidBassTimeLogCount < 10) { From 677beb4251b0017ef2c2c2b8e56512988d328705 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Sep 2025 17:37:33 +0900 Subject: [PATCH 080/267] Fix gameplay freezing on stutter frames / long load times Closes https://github.com/ppy/osu/issues/34732. May hotfix for this one. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 7f29ed3703..892f4acb78 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -161,7 +161,9 @@ namespace osu.Game.Rulesets.UI // // In testing this triggers *very* rarely even when set to super low values (10 ms). The cases we're worried about involve multi-second jumps. // A difference of more than 500 ms seems like a sane number we should never exceed. - if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500) + // + // Double-checking against the parent clock ensures we don't accidentally freeze time when the game stutters due to a long running frame. + if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && parentGameplayClock?.ElapsedFrameTime <= 500) { if (invalidBassTimeLogCount < 10) { From 1519084f72bd34d3af83cfbcccf728225a79b791 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Sep 2025 17:57:15 +0900 Subject: [PATCH 081/267] Eagerly clear the request queue on join --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 1dfa3c0cfb..745e773512 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -202,6 +202,7 @@ namespace osu.Game.Online.Multiplayer await joinOrLeaveTaskChain.Add(async () => { + await runOnUpdateThreadAsync(() => pendingRequests.Clear(), cancellationSource.Token).ConfigureAwait(false); var multiplayerRoom = await CreateRoomInternal(new MultiplayerRoom(room)).ConfigureAwait(false); await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); }, cancellationSource.Token).ConfigureAwait(false); @@ -225,6 +226,7 @@ namespace osu.Game.Online.Multiplayer await joinOrLeaveTaskChain.Add(async () => { + await runOnUpdateThreadAsync(() => pendingRequests.Clear(), cancellationSource.Token).ConfigureAwait(false); var multiplayerRoom = await JoinRoomInternal(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false); await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); }, cancellationSource.Token).ConfigureAwait(false); From a7997202321936dfc337bbe808ea942aa0122c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 12:23:38 +0200 Subject: [PATCH 082/267] Add failing test --- .../Multiplayer/TestSceneMultiSpectatorScreen.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index b9c77e20c0..0a042d189d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -20,6 +20,7 @@ using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; @@ -497,6 +498,18 @@ namespace osu.Game.Tests.Visual.Multiplayer b.Storyboard.GetLayer("Background").Add(sprite); }); + [Test] + public void TestFRankDisplay() + { + int[] userIds = getPlayerIds(1); + + start(userIds); + loadSpectateScreen(); + + sendFrames(userIds, 1000); + AddUntilStep("player has F rank", () => this.ChildrenOfType().All(msp => msp.GameplayState.ScoreProcessor.Rank.Value == ScoreRank.F)); + } + private void testLeadIn(Action? applyToBeatmap = null) { start(PLAYER_1_ID); From 9354547e152b06a923530a91d4ee9ba783ad064d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 12:25:42 +0200 Subject: [PATCH 083/267] Move solo spectator-specific fail logic to `SoloSpectatorPlayer` --- osu.Game/Screens/Play/SoloSpectatorPlayer.cs | 20 ++++++++++++++++++++ osu.Game/Screens/Play/SoloSpectatorScreen.cs | 2 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 16 ---------------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs index be83a4c6b5..87d77db847 100644 --- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs @@ -45,6 +45,26 @@ namespace osu.Game.Screens.Play }); } + #region Fail handling + + protected override bool CheckModsAllowFailure() + { + if (!allowFail) + return false; + + return base.CheckModsAllowFailure(); + } + + private bool allowFail; + + /// + /// Should be called when it is apparent that the player being spectated has failed. + /// This will subsequently stop blocking the fail screen from displaying (usually done out of safety). + /// + public void AllowFail() => allowFail = true; + + #endregion + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 269bc3bb92..75f8da707c 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -183,7 +183,7 @@ namespace osu.Game.Screens.Play { if (this.GetChildScreen() is SpectatorPlayerLoader loader) { - if (loader.GetChildScreen() is SpectatorPlayer player) + if (loader.GetChildScreen() is SoloSpectatorPlayer player) { player.AllowFail(); resetStartState(); diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 6bfb6e033a..4bd9bfafc0 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -29,16 +29,6 @@ namespace osu.Game.Screens.Play private readonly Score score; - protected override bool CheckModsAllowFailure() - { - if (!allowFail) - return false; - - return base.CheckModsAllowFailure(); - } - - private bool allowFail; - protected SpectatorPlayer(Score score, PlayerConfiguration? configuration = null) : base(configuration) { @@ -72,12 +62,6 @@ namespace osu.Game.Screens.Play }, true); } - /// - /// Should be called when it is apparent that the player being spectated has failed. - /// This will subsequently stop blocking the fail screen from displaying (usually done out of safety). - /// - public void AllowFail() => allowFail = true; - protected override void StartGameplay() { base.StartGameplay(); From 5c6bbfcc6a82642af7a30e6b04bfb4519a6797dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 12:30:22 +0200 Subject: [PATCH 084/267] Adjust fail handling in multiplayer spectator player to match multiplayer player Closes https://github.com/ppy/osu/issues/34884. --- .../OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 2 ++ .../Spectate/MultiSpectatorPlayer.cs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 9083a21704..a001863780 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -70,6 +70,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!LoadedBeatmapSuccessfully) return; + // also applied in `MultiSpectatorPlayer.load()` ScoreProcessor.ApplyNewJudgementsWhenFailed = true; LoadComponentAsync(new FillFlowContainer @@ -170,6 +171,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void PerformFail() { // base logic intentionally suppressed - failing in multiplayer only marks the score with F rank + // see also: `MultiSpectatorPlayer.PerformFail()` ScoreProcessor.FailScore(Score.ScoreInfo); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 8526e11e12..0dd547bfbb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.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.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -42,6 +43,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (cancellationToken.IsCancellationRequested) return; + if (!LoadedBeatmapSuccessfully) + return; + + // also applied in `MultiplayerPlayer.load()` + ScoreProcessor.ApplyNewJudgementsWhenFailed = true; + HUDOverlay.PlayerSettingsOverlay.Expire(); HUDOverlay.HoldToQuit.Expire(); } @@ -76,5 +83,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } protected override ResultsScreen CreateResults(ScoreInfo score) => new MultiSpectatorResultsScreen(score); + + protected override void PerformFail() + { + // base logic intentionally suppressed - failing in multiplayer only marks the score with F rank + // see also: `MultiplayerPlayer.PerformFail()` + ScoreProcessor.FailScore(Score.ScoreInfo); + } + + protected override void ConcludeFailedScore(Score score) + => throw new NotSupportedException($"{nameof(MultiSpectatorPlayer)} should never be calling {nameof(ConcludeFailedScore)}. Failing in multiplayer only marks the score with F rank."); } } From 4ed72efeae45ee8788c42f99f8b5e7b1f1bf4503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 11:22:41 +0200 Subject: [PATCH 085/267] Use better guard (and reword subsequent comment) Co-authored-by: Dean Herbert --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 525f6f62ad..eedde8b7a4 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -59,10 +59,12 @@ namespace osu.Game.Screens.Edit.GameplayTest { base.LoadAsyncComplete(); - // `preventMissOnPreviousHitObjects()` needs to be called to install its hooks before drawable hit objects get the chance to run update logic, + if (!LoadedBeatmapSuccessfully) + return; + + // This hack needs to be called to install its hooks before drawable hit objects get the chance to run update logic, // because it will not work otherwise due to being too late (various effects of the objects getting missed will have already taken place). - if (DrawableRuleset != null) - preventMissOnPreviousHitObjects(); + preventMissOnPreviousHitObjects(); } protected override void LoadComplete() From 6c82f543e6d03aa775ab7ca417305ec35eb5d6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 12:55:40 +0200 Subject: [PATCH 086/267] Download online beatmap / present local beatmap on shift-clicking beatmap cards Closes https://github.com/ppy/osu/issues/34883. --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 135e5129ae..54f8d656fe 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -7,7 +7,9 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; @@ -35,6 +37,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected readonly BeatmapDownloadTracker DownloadTracker; + private readonly Bindable preferNoVideo = new BindableBool(); + private InputManager? containingInputManager; + + [Resolved] + private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + + [Resolved] + private BeatmapModelDownloader? beatmaps { get; set; } + + [Resolved] + private OsuGame? game { get; set; } + protected BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true) : base(HoverSampleSet.Button) { @@ -45,10 +59,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards DownloadTracker = new BeatmapDownloadTracker(beatmapSet); } - [BackgroundDependencyLoader(true)] - private void load(BeatmapSetOverlay? beatmapSetOverlay) + [BackgroundDependencyLoader] + private void load(OsuConfigManager configManager) { - Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); + configManager.BindWith(OsuSetting.PreferNoVideo, preferNoVideo); AddInternal(DownloadTracker); } @@ -60,6 +74,28 @@ namespace osu.Game.Beatmaps.Drawables.Cards DownloadTracker.State.BindValueChanged(_ => UpdateState()); Expanded.BindValueChanged(_ => UpdateState(), true); FinishTransforms(true); + + containingInputManager = GetContainingInputManager(); + + Action = () => + { + if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true) + { + switch (DownloadTracker.State.Value) + { + case DownloadState.NotDownloaded: + if (!BeatmapSet.Availability.DownloadDisabled) + beatmaps?.Download(BeatmapSet, preferNoVideo.Value); + break; + + case DownloadState.LocallyAvailable: + game?.PresentBeatmap(BeatmapSet); + break; + } + } + else + beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); + }; } protected override bool OnHover(HoverEvent e) From bee6c32b83de4b4a5ef1b28a7ad880b7f13146aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Sep 2025 21:39:12 +0900 Subject: [PATCH 087/267] Change bass workaround fix to use game clock intead of another-audio-clock paper trail: https://github.com/ppy/osu/pull/34890#issuecomment-3244549790 --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 892f4acb78..ffefea570e 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -63,6 +63,9 @@ namespace osu.Game.Rulesets.UI /// private readonly FramedClock framedClock; + [Resolved] + private OsuGame game { get; set; } = null!; + private readonly Stopwatch stopwatch = new Stopwatch(); /// @@ -163,7 +166,7 @@ namespace osu.Game.Rulesets.UI // A difference of more than 500 ms seems like a sane number we should never exceed. // // Double-checking against the parent clock ensures we don't accidentally freeze time when the game stutters due to a long running frame. - if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && parentGameplayClock?.ElapsedFrameTime <= 500) + if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && game.Clock.ElapsedFrameTime <= 500) { if (invalidBassTimeLogCount < 10) { From a1105ba16fc660bf5a618a95cdedbd773d9e066d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Sep 2025 21:40:52 +0900 Subject: [PATCH 088/267] Make `OsuGame` dependency optional for sanity --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index ffefea570e..990c1c839b 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.UI private readonly FramedClock framedClock; [Resolved] - private OsuGame game { get; set; } = null!; + private OsuGame? game { get; set; } private readonly Stopwatch stopwatch = new Stopwatch(); @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.UI // A difference of more than 500 ms seems like a sane number we should never exceed. // // Double-checking against the parent clock ensures we don't accidentally freeze time when the game stutters due to a long running frame. - if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && game.Clock.ElapsedFrameTime <= 500) + if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && game?.Clock.ElapsedFrameTime <= 500) { if (invalidBassTimeLogCount < 10) { From 95c72524677527dfe6b3db4ed62723804b3ade21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 14:43:41 +0200 Subject: [PATCH 089/267] Add failing test case --- .../Database/BeatmapImporterTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 38746f2567..f3ca665380 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -1018,6 +1018,49 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestBeatmapFilesInNestedDirectoriesAreIgnored() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); + + string? temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + var subdirectory = Directory.CreateDirectory(Path.Combine(extractedFolder, "subdir")); + string modifiedCopyPath = Path.Combine(subdirectory.FullName, "duplicate.osu"); + File.Copy(Directory.GetFiles(extractedFolder, "*.osu").First(), modifiedCopyPath); + + using (var stream = File.OpenWrite(modifiedCopyPath)) + using (var textWriter = new StreamWriter(stream)) + await textWriter.WriteLineAsync("# adding a comment so that the hashes are different"); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + await importer.Import(temp); + + EnsureLoaded(realm.Realm); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + [Test] public void TestImportNestedStructure() { From 79f7f0ecad2f5721ee84470db9f415cdcb57f417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 14:46:39 +0200 Subject: [PATCH 090/267] Ignore `.osu` files not placed at top level of beatmap archive on import Closes https://github.com/ppy/osu/issues/34677. --- osu.Game/Beatmaps/BeatmapImporter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 28997509dc..f80c4de4ea 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -367,7 +367,11 @@ namespace osu.Game.Beatmaps { var beatmaps = new List(); - foreach (var file in beatmapSet.Files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) + // stable appears to ignore `.osu` files which are not placed at the top level of the beatmap archive. + // the logic that achieves this is very difficult to make sense of, but appears to be located somewhere around + // https://github.com/peppy/osu-stable-reference/blob/67795dba3c308e7d0493b296149dcb073ca47ecb/osu!/GameplayElements/Beatmaps/BeatmapManager.cs#L207-L208 + // only testing the `/` path separator character is sufficient as `RealmNamedFileUsage`s are normalised to use the front slash unix path separator convention + foreach (var file in beatmapSet.Files.Where(f => !f.Filename.Contains('/') && f.Filename.EndsWith(@".osu", StringComparison.OrdinalIgnoreCase))) { using (var memoryStream = new MemoryStream(Files.Store.Get(file.File.GetStoragePath()))) // we need a memory stream so we can seek { From 4a193e96e0d792d3d30309971457f54a3dff7852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Ptach-=C5=BBurakowski?= Date: Tue, 2 Sep 2025 17:53:31 +0200 Subject: [PATCH 091/267] Change deafult keys to none --- osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs | 4 ++-- osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs index 763f9f288b..929ce595ac 100644 --- a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Mania SecondaryLeftKeys = stage1SecondaryLeftKeys, SecondaryRightKeys = stage1SecondaryRightKeys, SpecialKey = InputKey.V, - SecondarySpecialKey = InputKey.Space + SecondarySpecialKey = InputKey.None }.GenerateKeyBindingsFor(singleStageVariant); var stage2Bindings = new VariantMappingGenerator @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania SecondaryLeftKeys = stage2SecondaryLeftKeys, SecondaryRightKeys = stage2SecondaryRightKeys, SpecialKey = InputKey.B, - SecondarySpecialKey = InputKey.Enter, + SecondarySpecialKey = InputKey.None, ActionStart = (ManiaAction)singleStageVariant, }.GenerateKeyBindingsFor(singleStageVariant); diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs index d5c0c16f64..3a7a014fc3 100644 --- a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs @@ -23,15 +23,15 @@ namespace osu.Game.Rulesets.Mania { leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V }; rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; - secondaryLeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.B }; - secondaryRightKeys = new[] { InputKey.M, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft }; + secondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; + secondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; } else { leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F }; rightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; - secondaryLeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R }; - secondaryRightKeys = new[] { InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft }; + secondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; + secondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; } } @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania RightKeys = rightKeys, SecondaryRightKeys = secondaryRightKeys, SpecialKey = InputKey.Space, - SecondarySpecialKey = InputKey.Enter + SecondarySpecialKey = InputKey.None }.GenerateKeyBindingsFor(variant); } } From dc5794dceb98959aa3d9908b37da114217558bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 07:51:52 +0200 Subject: [PATCH 092/267] Do not pass a whole bunch of `InputKey.None` down three levels for no reason --- .../DualStageVariantGenerator.cs | 22 ------------------- .../SingleStageVariantGenerator.cs | 9 -------- .../VariantMappingGenerator.cs | 12 +++------- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs index 929ce595ac..6a7634da01 100644 --- a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs @@ -14,10 +14,6 @@ namespace osu.Game.Rulesets.Mania private readonly InputKey[] stage1RightKeys; private readonly InputKey[] stage2LeftKeys; private readonly InputKey[] stage2RightKeys; - private readonly InputKey[] stage1SecondaryLeftKeys; - private readonly InputKey[] stage1SecondaryRightKeys; - private readonly InputKey[] stage2SecondaryLeftKeys; - private readonly InputKey[] stage2SecondaryRightKeys; public DualStageVariantGenerator(int singleStageVariant) { @@ -31,12 +27,6 @@ namespace osu.Game.Rulesets.Mania stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G, InputKey.B }; stage2RightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; - - stage1SecondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; - stage1SecondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; - - stage2SecondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; - stage2SecondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; } else { @@ -45,12 +35,6 @@ namespace osu.Game.Rulesets.Mania stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G }; stage2RightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; - - stage1SecondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; - stage1SecondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; - - stage2SecondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; - stage2SecondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; } } @@ -60,20 +44,14 @@ namespace osu.Game.Rulesets.Mania { LeftKeys = stage1LeftKeys, RightKeys = stage1RightKeys, - SecondaryLeftKeys = stage1SecondaryLeftKeys, - SecondaryRightKeys = stage1SecondaryRightKeys, SpecialKey = InputKey.V, - SecondarySpecialKey = InputKey.None }.GenerateKeyBindingsFor(singleStageVariant); var stage2Bindings = new VariantMappingGenerator { LeftKeys = stage2LeftKeys, RightKeys = stage2RightKeys, - SecondaryLeftKeys = stage2SecondaryLeftKeys, - SecondaryRightKeys = stage2SecondaryRightKeys, SpecialKey = InputKey.B, - SecondarySpecialKey = InputKey.None, ActionStart = (ManiaAction)singleStageVariant, }.GenerateKeyBindingsFor(singleStageVariant); diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs index 3a7a014fc3..c642da6dc4 100644 --- a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs @@ -10,9 +10,7 @@ namespace osu.Game.Rulesets.Mania { private readonly int variant; private readonly InputKey[] leftKeys; - private readonly InputKey[] secondaryLeftKeys; private readonly InputKey[] rightKeys; - private readonly InputKey[] secondaryRightKeys; public SingleStageVariantGenerator(int variant) { @@ -23,26 +21,19 @@ namespace osu.Game.Rulesets.Mania { leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V }; rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; - secondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; - secondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None, InputKey.None }; } else { leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F }; rightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; - secondaryLeftKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; - secondaryRightKeys = new[] { InputKey.None, InputKey.None, InputKey.None, InputKey.None }; } } public IEnumerable GenerateMappings() => new VariantMappingGenerator { LeftKeys = leftKeys, - SecondaryLeftKeys = secondaryLeftKeys, RightKeys = rightKeys, - SecondaryRightKeys = secondaryRightKeys, SpecialKey = InputKey.Space, - SecondarySpecialKey = InputKey.None }.GenerateKeyBindingsFor(variant); } } diff --git a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs index a8146497c1..5e4da2d480 100644 --- a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs +++ b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs @@ -15,22 +15,16 @@ namespace osu.Game.Rulesets.Mania /// public InputKey[] LeftKeys; - public InputKey[] SecondaryLeftKeys; - /// /// All the s available to the right hand. /// public InputKey[] RightKeys; - public InputKey[] SecondaryRightKeys; - /// /// The for the special key. /// public InputKey SpecialKey; - public InputKey SecondarySpecialKey; - /// /// The at which the columns should begin. /// @@ -50,19 +44,19 @@ namespace osu.Game.Rulesets.Mania for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++) { bindings.Add(new KeyBinding(LeftKeys[i], currentAction)); - bindings.Add(new KeyBinding(SecondaryLeftKeys[i], currentAction++)); + bindings.Add(new KeyBinding(InputKey.None, currentAction++)); } if (columns % 2 == 1) { bindings.Add(new KeyBinding(SpecialKey, currentAction)); - bindings.Add(new KeyBinding(SecondarySpecialKey, currentAction++)); + bindings.Add(new KeyBinding(InputKey.None, currentAction++)); } for (int i = 0; i < columns / 2; i++) { bindings.Add(new KeyBinding(RightKeys[i], currentAction)); - bindings.Add(new KeyBinding(SecondaryRightKeys[i], currentAction++)); + bindings.Add(new KeyBinding(InputKey.None, currentAction++)); } return bindings; From 19361666a17fc0f581db809d5673ebc5796ca64d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Sep 2025 15:27:29 +0900 Subject: [PATCH 093/267] Add menu tip exposing new behaviour --- osu.Game/Localisation/MenuTipStrings.cs | 5 +++++ osu.Game/Screens/Menu/MenuTipDisplay.cs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs index 4d1f2ceaa6..ebab9f4d02 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -154,6 +154,11 @@ namespace osu.Game.Localisation /// public static LocalisableString RightMouseAbsoluteScroll => new TranslatableString(getKey(@"right_mouse_absolute_scroll"), @"Try holding your right mouse button near the beatmap carousel to quickly scroll to an absolute position!"); + /// + /// "Shift-click on a beatmap panel in the beatmap listing overlay to quickly download or view the beatmap in song select!" + /// + public static LocalisableString ShiftClickInBeatmapOverlay => new TranslatableString(getKey(@"shift_click_in_beatmap_overlay"), @"Shift-click on a beatmap panel in the beatmap listing overlay to quickly download or view the beatmap in song select!"); + /// /// "a tip for you:" /// diff --git a/osu.Game/Screens/Menu/MenuTipDisplay.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs index 7e538995b2..d9c90b069d 100644 --- a/osu.Game/Screens/Menu/MenuTipDisplay.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Menu .FadeOutFromOne(2000, Easing.OutQuint); } - private const int available_tips = 29; + private const int available_tips = 30; private LocalisableString getRandomTip() { @@ -216,6 +216,9 @@ namespace osu.Game.Screens.Menu case 28: return MenuTipStrings.RightMouseAbsoluteScroll; + + case 29: + return MenuTipStrings.ShiftClickInBeatmapOverlay; } return string.Empty; From 6399f7e3db1645d6d9ea4a2cf3c083a77169d1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 08:59:33 +0200 Subject: [PATCH 094/267] Add failing test case --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 62e7a80435..66e286cf4f 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -294,6 +294,39 @@ namespace osu.Game.Tests.Skins.IO #endregion + /// + /// Note that this test passing / failing is platform / OS-specific (if it is to fail, it'll fail on windows). + /// + [Test] + public async Task TestExternallyMountingImportWithInvalidFilename() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host); + + var zipStream = new MemoryStream(); + using var zip = ZipArchive.Create(); + zip.AddEntry("test?.png", new MemoryStream(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF })); + zip.SaveTo(zipStream); + + var import = await loadSkinIntoOsu(osu, new ImportTask(zipStream, "test skin.osk")); + + var skinManager = osu.Dependencies.Get(); + var externalEdit = await skinManager.BeginExternalEditing(import.PerformRead(s => s.Detach())); // should not fail + + Task finishTask = Task.CompletedTask; + host.UpdateThread.Scheduler.Add(() => finishTask = externalEdit.Finish()); + await finishTask; + } + finally + { + host.Exit(); + } + } + } + private void assertCorrectMetadata(Live import1, string name, string creator, decimal version, OsuGameBase osu) { import1.PerformRead(i => From 51e75934462d78bb83122d67b54b6e2d258c2a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 08:59:57 +0200 Subject: [PATCH 095/267] Fix external edit operations failing due to invalid filenames --- osu.Game/Database/RealmArchiveModelImporter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index a3cdc2dc77..0f9832578b 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -208,7 +208,16 @@ namespace osu.Game.Database foreach (var realmFile in model.Files) { string sourcePath = Files.Storage.GetFullPath(realmFile.File.GetStoragePath()); - string destinationPath = Path.Join(mountedPath, realmFile.Filename); + // there are edge cases where externalising an imported model to the filesystem could fail due to invalid filenames. + // one scenario where this happens goes something like this: + // - stable user exports an archive, which contains filenames that get mangled by stable's default zip encoding codepage (Shift-JIS) + // - said archive is imported to lazer, but the invalid filename is not actually an issue due to lazer file store structure + // (the file is stored under a filename correspondent to its SHA instead, and its real filename is only stored in realm) + // - however attempts to externally edit the model fail as the external edit attempts and fails to produce the file's "real" filename in the mounted path + // to prevent this bricking external edit, strip invalid characters on external edit. + // the presumption here is that whatever produced the mangled archive is primarily at fault here, and we're just trying to trudge on locally as best as possible. + // if there are further troubles related to similar issues, reevaluate moving this sort of check to the import side instead (sanitising filenames on import from archive). + string destinationPath = Path.Join(mountedPath, realmFile.Filename.GetValidFilename()); Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); From 5c66998c57850993b671ab1f17185908f91721ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 09:32:43 +0200 Subject: [PATCH 096/267] Replace local copy of `GetValidFilename()` with direct usage Noticed in passing. --- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 26 +++++------------------- osu.Game/Extensions/ModelExtensions.cs | 1 + 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 30bbbbc1fe..9957935977 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -20,6 +20,7 @@ using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Beatmaps.Formats; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -341,27 +342,10 @@ namespace osu.Game.Beatmaps { // Matches stable implementation, because it's probably simpler than trying to do anything else. // This may need to be reconsidered after we begin storing storyboards in the new editor. - return windowsFilenameStrip( - (metadata.Artist.Length > 0 ? metadata.Artist + @" - " + metadata.Title : Path.GetFileNameWithoutExtension(metadata.AudioFile)) - + (metadata.Author.Username.Length > 0 ? @" (" + metadata.Author.Username + @")" : string.Empty) - + @".osb"); - - string windowsFilenameStrip(string entry) - { - // Inlined from Path.GetInvalidFilenameChars() to ensure the windows characters are used (to match stable). - char[] invalidCharacters = - { - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', - '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', - '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', - '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' - }; - - foreach (char c in invalidCharacters) - entry = entry.Replace(c.ToString(), string.Empty); - - return entry; - } + string baseFilename = (metadata.Artist.Length > 0 ? metadata.Artist + @" - " + metadata.Title : Path.GetFileNameWithoutExtension(metadata.AudioFile)) + + (metadata.Author.Username.Length > 0 ? @" (" + metadata.Author.Username + @")" : string.Empty) + + @".osb"; + return baseFilename.GetValidFilename(); } } } diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index 18c991297a..7c9d929999 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -175,6 +175,7 @@ namespace osu.Game.Extensions /// DO NOT CHANGE THE SEMANTICS OF THIS METHOD unless you know well what you are doing. /// /// + /// public static string GetValidFilename(this string filename) { foreach (char c in invalid_filename_chars) From 315eea8d9ade4e2b2f23a4d1cfa4214670fe45c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 09:52:37 +0200 Subject: [PATCH 097/267] Simplify beatmap access in carousel panels --- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 18 ++---------------- .../Screens/SelectV2/PanelBeatmapStandalone.cs | 17 ++--------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 545439684b..7dfd82cd1f 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -72,14 +71,7 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private ISongSelect? songSelect { get; set; } - private GroupedBeatmap groupedBeatmap - { - get - { - Debug.Assert(Item != null); - return (GroupedBeatmap)Item!.Model; - } - } + private BeatmapInfo beatmap => ((GroupedBeatmap)Item!.Model).Beatmap; public PanelBeatmap() { @@ -216,8 +208,6 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - var beatmap = groupedBeatmap.Beatmap; - difficultyIcon.Icon = getRulesetIcon(beatmap.Ruleset); localRank.Beatmap = beatmap; @@ -256,8 +246,6 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return; - var beatmap = groupedBeatmap.Beatmap; - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE); starDifficultyBindable.BindValueChanged(starDifficulty => { @@ -301,8 +289,6 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return; - var beatmap = groupedBeatmap.Beatmap; - if (ruleset.Value.OnlineID == 3) { // Account for mania differences locally for now. @@ -327,7 +313,7 @@ namespace osu.Game.Screens.SelectV2 List items = new List(); if (songSelect != null) - items.AddRange(songSelect.GetForwardActions(groupedBeatmap.Beatmap)); + items.AddRange(songSelect.GetForwardActions(beatmap)); return items.ToArray(); } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 226a1b1d06..b54eecd548 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -73,14 +72,7 @@ namespace osu.Game.Screens.SelectV2 private Box backgroundBorder = null!; - private GroupedBeatmap groupedBeatmap - { - get - { - Debug.Assert(Item != null); - return (GroupedBeatmap)Item!.Model; - } - } + private BeatmapInfo beatmap => ((GroupedBeatmap)Item!.Model).Beatmap; public PanelBeatmapStandalone() { @@ -228,7 +220,6 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - var beatmap = groupedBeatmap.Beatmap; var beatmapSet = beatmap.BeatmapSet!; beatmapBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmap); @@ -269,8 +260,6 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return; - var beatmap = groupedBeatmap.Beatmap; - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE); starDifficultyBindable.BindValueChanged(starDifficulty => { @@ -307,8 +296,6 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return; - var beatmap = groupedBeatmap.Beatmap; - if (ruleset.Value.OnlineID == 3) { // Account for mania differences locally for now. @@ -333,7 +320,7 @@ namespace osu.Game.Screens.SelectV2 List items = new List(); if (songSelect != null) - items.AddRange(songSelect.GetForwardActions(groupedBeatmap.Beatmap)); + items.AddRange(songSelect.GetForwardActions(beatmap)); return items.ToArray(); } From a840e55977aed7510ee8610035b34b670a87f7f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Sep 2025 17:14:45 +0900 Subject: [PATCH 098/267] Check exported filenames for added safety --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 66e286cf4f..f909638333 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -316,6 +316,13 @@ namespace osu.Game.Tests.Skins.IO var skinManager = osu.Dependencies.Get(); var externalEdit = await skinManager.BeginExternalEditing(import.PerformRead(s => s.Detach())); // should not fail + Assert.That(Directory.Exists(externalEdit.MountedPath)); + Assert.That(new DirectoryInfo(externalEdit.MountedPath).GetFiles().Select(f => f.Name), Is.EquivalentTo(new[] + { + "skin.ini", + "test.png" + })); + Task finishTask = Task.CompletedTask; host.UpdateThread.Scheduler.Add(() => finishTask = externalEdit.Finish()); await finishTask; From eb1263aa32cc399d0c860178ac4f07757cc44377 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Sep 2025 17:23:41 +0900 Subject: [PATCH 099/267] Update framework --- 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 46d558354e..5d9158a45a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 3768550c21..8e269d292d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4a7ee6fafc75240a36a810e79f01eced48ecda5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 10:24:25 +0200 Subject: [PATCH 100/267] Hide object-typed `CurrentSelection` and expose strongly-typed alternatives instead --- .../Background/TestSceneUserDimBackgrounds.cs | 2 +- .../SongSelectV2/BeatmapCarouselTestScene.cs | 17 +++---- .../TestSceneBeatmapCarouselArtistGrouping.cs | 4 +- ...tSceneBeatmapCarouselDifficultyGrouping.cs | 6 +-- .../TestSceneBeatmapCarouselFiltering.cs | 20 ++++---- .../TestSceneBeatmapCarouselNoGrouping.cs | 12 ++--- .../TestSceneBeatmapCarouselRandom.cs | 16 +++--- .../TestSceneBeatmapCarouselScrolling.cs | 10 ++-- .../TestSceneBeatmapCarouselUpdateHandling.cs | 20 ++++---- ...neSongSelectCurrentSelectionInvalidated.cs | 2 +- osu.Game/Graphics/Carousel/Carousel.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 51 +++++++++++++------ osu.Game/Screens/SelectV2/SongSelect.cs | 6 +-- 13 files changed, 93 insertions(+), 75 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 58fb02c90c..3021589cdb 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -325,7 +325,7 @@ namespace osu.Game.Tests.Visual.Background private void setupUserSettings() { AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen()); - AddUntilStep("Song select has selection", () => songSelect.Carousel?.CurrentSelection != null); + AddUntilStep("Song select has selection", () => songSelect.Carousel?.CurrentGroupedBeatmap != null); AddStep("Set default user settings", () => { SelectedMods.Value = new[] { new OsuModNoFail() }; diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index b616055157..a180097863 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -118,14 +118,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RequestSelection = b => { BeatmapRequestedSelections.Push(b.Beatmap); - Carousel.CurrentSelection = b; + Carousel.CurrentGroupedBeatmap = b; }, RequestRecommendedSelection = groupedBeatmaps => { var recommendedBeatmap = BeatmapRecommendationFunction?.Invoke(groupedBeatmaps.Select(gb => gb.Beatmap)) ?? groupedBeatmaps.First().Beatmap; var recommendedGroupedBeatmap = groupedBeatmaps.First(gb => gb.Beatmap.Equals(recommendedBeatmap)); BeatmapSetRequestedSelections.Push(recommendedBeatmap.BeatmapSet!); - Carousel.CurrentSelection = recommendedGroupedBeatmap; + Carousel.CurrentGroupedBeatmap = recommendedGroupedBeatmap; }, BleedTop = 50, BleedBottom = 50, @@ -219,8 +219,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter)); - protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); - protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentGroupedBeatmap, () => Is.Null); + protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentGroupedBeatmap, () => Is.Not.Null); protected void CheckRequestPresentCount(int expected) => AddAssert($"check present count is {expected}", () => Carousel.RequestPresentBeatmapCount, () => Is.EqualTo(expected)); @@ -285,8 +285,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // offset by one because the group itself is included in the items list. CarouselItem item = groupingFilter.GroupItems[groupDefinition].ElementAt(panel + 1); - return (Carousel.CurrentSelection as GroupedBeatmap)? - .Equals(item.Model as GroupedBeatmap) == true; + return Carousel.CurrentGroupedBeatmap?.Equals(item.Model as GroupedBeatmap) == true; }); } @@ -295,12 +294,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 if (diff != null) { AddUntilStep($"selected is set{set} diff{diff.Value}", - () => (Carousel.CurrentSelection as GroupedBeatmap)?.Beatmap, + () => Carousel.CurrentBeatmap, () => Is.EqualTo(BeatmapSets[set].Beatmaps[diff.Value])); } else { - AddUntilStep($"selected is set{set}", () => BeatmapSets[set].Beatmaps.Contains(((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap)); + AddUntilStep($"selected is set{set}", () => BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentBeatmap!)); } } @@ -419,7 +418,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 tracked: {Carousel.ItemsTracked} displayable: {Carousel.DisplayableItems} displayed: {Carousel.VisibleItems} - selected: {Carousel.CurrentSelection} + selected: {Carousel.CurrentGroupedBeatmap} """); void createHeader(string text) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 78b6985fdb..c34077889d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); - AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap)); RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!)); - AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap)); AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index c28860e368..58ecfcbf3b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); - AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap)); RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!)); - AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap)); AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } private void checkBeatmapIsKeyboardSelected() => - AddUntilStep("check keyboard selected group is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(Carousel.CurrentSelection)); + AddUntilStep("check keyboard selected group is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap)); private void checkGroupKeyboardSelected(int index) => AddUntilStep($"check keyboard selected group is {index}", () => GetKeyboardSelectedPanel()?.Item?.Model, () => { diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index b232d12e46..b1bd9fd3ed 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -130,14 +130,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); Select(); - AddStep("record selection", () => selectedID = ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID); + AddStep("record selection", () => selectedID = Carousel.CurrentBeatmap!.ID); for (int i = 0; i < 5; i++) { ApplyToFilterAndWaitForFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); - AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID == selectedID); + AddAssert("selection not changed", () => Carousel.CurrentBeatmap!.ID == selectedID); ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); - AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID == selectedID); + AddAssert("selection not changed", () => Carousel.CurrentBeatmap!.ID == selectedID); } } @@ -177,14 +177,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); - AddStep("record selection", () => selectedID = ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID); + AddStep("record selection", () => selectedID = Carousel.CurrentBeatmap!.ID); for (int i = 0; i < 5; i++) { ApplyToFilterAndWaitForFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); - AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID == selectedID); + AddAssert("selection not changed", () => Carousel.CurrentBeatmap!.ID == selectedID); ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); - AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap.ID == selectedID); + AddAssert("selection not changed", () => Carousel.CurrentBeatmap!.ID == selectedID); } } @@ -200,14 +200,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { int diff = i; - AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]); - AddUntilStep("selection changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(chosenBeatmap)); + AddStep($"select diff {diff}", () => Carousel.CurrentBeatmap = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]); + AddUntilStep("selection changed", () => Carousel.CurrentBeatmap, () => Is.EqualTo(chosenBeatmap)); SortBy(SortMode.Difficulty); - AddAssert("selection retained", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(chosenBeatmap)); + AddAssert("selection retained", () => Carousel.CurrentBeatmap, () => Is.EqualTo(chosenBeatmap)); SortBy(SortMode.Title); - AddAssert("selection retained", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(chosenBeatmap)); + AddAssert("selection retained", () => Carousel.CurrentBeatmap, () => Is.EqualTo(chosenBeatmap)); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 0b0f93b3bc..c839a28055 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); - AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap)); RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!)); - AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap)); AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } @@ -389,15 +389,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private void checkSelectionIterating(bool isIterating) { - object? selection = null; + GroupedBeatmap? selection = null; for (int i = 0; i < 3; i++) { - AddStep("store selection", () => selection = Carousel.CurrentSelection); + AddStep("store selection", () => selection = Carousel.CurrentGroupedBeatmap); if (isIterating) - AddUntilStep("selection changed", () => Carousel.CurrentSelection != selection); + AddUntilStep("selection changed", () => Carousel.CurrentGroupedBeatmap != selection); else - AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); + AddUntilStep("selection not changed", () => Carousel.CurrentGroupedBeatmap == selection); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index 9f31c875b6..ce68d587c8 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -50,12 +50,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 nextRandom(); ensureRandomDidNotRepeat(); - AddStep("store selection", () => originalSelected = ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap); + AddStep("store selection", () => originalSelected = Carousel.CurrentBeatmap!); SortAndGroupBy(SortMode.Artist, GroupMode.Difficulty); WaitForFiltering(); - AddAssert("selection not changed", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(originalSelected)); + AddAssert("selection not changed", () => Carousel.CurrentBeatmap, () => Is.EqualTo(originalSelected)); storeExpandedGroup(); @@ -282,7 +282,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 nextRandom(); CheckHasSelection(); - AddStep("store selection", () => originalSelected = ((GroupedBeatmap)Carousel.CurrentSelection!)); + AddStep("store selection", () => originalSelected = Carousel.CurrentGroupedBeatmap!); AddStep("random then rewind", () => { @@ -290,7 +290,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Carousel.PreviousRandom(); }); - AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(originalSelected)); + AddAssert("selection not changed", () => Carousel.CurrentGroupedBeatmap, () => Is.EqualTo(originalSelected)); } [Test] @@ -305,20 +305,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 nextRandom(); CheckHasSelection(); - AddStep("store selection", () => originalSelected = (GroupedBeatmap)Carousel.CurrentSelection!); + AddStep("store selection", () => originalSelected = Carousel.CurrentGroupedBeatmap!); nextRandom(); - AddStep("store selection", () => postRandomSelection = (GroupedBeatmap)Carousel.CurrentSelection!); + AddStep("store selection", () => postRandomSelection = Carousel.CurrentGroupedBeatmap!); AddAssert("selection changed", () => originalSelected, () => Is.Not.SameAs(postRandomSelection)); AddStep("delete previous selection beatmaps", () => BeatmapSets.Remove(originalSelected!.Beatmap.BeatmapSet!)); WaitForFiltering(); - AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection)); + AddAssert("selection not changed", () => Carousel.CurrentGroupedBeatmap, () => Is.EqualTo(postRandomSelection)); prevRandomSet(); - AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection)); + AddAssert("selection not changed", () => Carousel.CurrentGroupedBeatmap, () => Is.EqualTo(postRandomSelection)); } private void nextRandom() => diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index d05c874641..c1cee4e398 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Quad positionBefore = default; - AddStep("select middle beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First())); + AddStep("select middle beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First())); WaitForScrolling(); @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Quad positionBefore = default; - AddStep("select middle beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First())); + AddStep("select middle beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First())); WaitForScrolling(); AddStep("override scroll with user scroll", () => @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - AddStep("select last beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.Last().Beatmaps.Last())); + AddStep("select last beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.Last().Beatmaps.Last())); WaitForScrolling(); @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Quad positionBefore = default; - AddStep("select first beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.First().Beatmaps.First())); + AddStep("select first beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.First().Beatmaps.First())); WaitForScrolling(); @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Quad positionBefore = default; - AddStep("select first beatmap", () => Carousel.CurrentSelection = new GroupedBeatmap(null, BeatmapSets.First().Beatmaps.First())); + AddStep("select first beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.First().Beatmaps.First())); WaitForScrolling(); AddStep("override scroll with user scroll", () => diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index a331879684..d1cef3420a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => @@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } @@ -205,14 +205,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.DifficultyName = "new name"); assertDidFilter(); WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } @@ -222,14 +222,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.OnlineID = b.OnlineID + 1); assertDidFilter(); WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } @@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); // Add another difficulty with same online ID. @@ -252,7 +252,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } @@ -262,7 +262,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(1, 0); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); // Remove original selected difficulty, and add two difficulties with same name as selection. @@ -284,7 +284,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => ((GroupedBeatmap)Carousel.CurrentSelection!).Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs index c480d6ca7e..7c604eb37b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 /// public partial class TestSceneSongSelectCurrentSelectionInvalidated : SongSelectTestScene { - private BeatmapInfo? selectedBeatmap => (Carousel.CurrentSelection as GroupedBeatmap)?.Beatmap; + private BeatmapInfo? selectedBeatmap => Carousel.CurrentBeatmap; private BeatmapSetInfo? selectedBeatmapSet => selectedBeatmap?.BeatmapSet; [SetUpSteps] diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 5adc37ea40..0df183bb71 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -107,7 +107,7 @@ namespace osu.Game.Graphics.Carousel /// The selection is never reset due to not existing. It can be set to anything. /// If no matching carousel item exists, there will be no visually selected item while waiting for potential new item which matches. /// - public virtual object? CurrentSelection + protected object? CurrentSelection { get => currentSelection.Model; set diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 6c98630274..ab520525a5 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -295,28 +295,47 @@ namespace osu.Game.Screens.SelectV2 protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => grouping.BeatmapSetsGroupedTogether && item.Model is GroupedBeatmap; - public override object? CurrentSelection + /// + /// The currently selected . + /// + /// + /// The selection is never reset due to not existing. It can be set to anything. + /// If no matching carousel item exists, there will be no visually selected item while waiting for potential new item which matches. + /// + public GroupedBeatmap? CurrentGroupedBeatmap { - get => base.CurrentSelection; + get => CurrentSelection as GroupedBeatmap; + set => CurrentSelection = value; + } + + /// + /// The currently selected . + /// + /// + /// This is a property mostly dedicated to external consumers who only care about showing some particular copy of a beatmap + /// (there could be multiple panels for one beatmap due to grouping). + /// Through this property, the carousel basically figures out what group to use internally. + /// + public BeatmapInfo? CurrentBeatmap + { + get => CurrentGroupedBeatmap?.Beatmap; set { - // this is a special pathway for external consumers who only care about showing some particular copy of a beatmap - // (there could be multiple panels for one beatmap due to grouping). - // in this pathway we basically figure out what group to use internally, and continue working with `GroupedBeatmap` all the way after that. - if (value is BeatmapInfo beatmapInfo) + if (value == null) { - if (CurrentSelection is GroupedBeatmap groupedBeatmap && beatmapInfo.Equals(groupedBeatmap.Beatmap)) - return; - - // it is not universally guaranteed that the carousel items will be materialised at the time this is set. - // therefore, in cases where it is known that they will not be, default to a null group. - // even if grouping is active, this will be rectified to a correct group on the next invocation of `HandleFilterCompleted()`. - value = IsLoaded && !IsFiltering - ? GetCarouselItems()?.Select(item => item.Model).OfType().FirstOrDefault(gb => gb.Beatmap.Equals(beatmapInfo)) - : new GroupedBeatmap(null, beatmapInfo); + CurrentGroupedBeatmap = null; + return; } - base.CurrentSelection = value; + if (CurrentGroupedBeatmap != null && value.Equals(CurrentGroupedBeatmap.Beatmap)) + return; + + // it is not universally guaranteed that the carousel items will be materialised at the time this is set. + // therefore, in cases where it is known that they will not be, default to a null group. + // even if grouping is active, this will be rectified to a correct group on the next invocation of `HandleFilterCompleted()`. + CurrentGroupedBeatmap = IsLoaded && !IsFiltering + ? GetCarouselItems()?.Select(item => item.Model).OfType().FirstOrDefault(gb => gb.Beatmap.Equals(value)) + : new GroupedBeatmap(null, value); } } diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 7597912ae6..9949f86808 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -479,7 +479,7 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return; - carousel.CurrentSelection = groupedBeatmap; + carousel.CurrentGroupedBeatmap = groupedBeatmap; // Debounce consideration is to avoid beatmap churn on key repeat selection. selectionDebounce?.Cancel(); @@ -512,7 +512,7 @@ namespace osu.Game.Screens.SelectV2 if (validSelection) { - carousel.CurrentSelection = currentBeatmap.BeatmapInfo; + carousel.CurrentBeatmap = currentBeatmap.BeatmapInfo; return true; } @@ -535,7 +535,7 @@ namespace osu.Game.Screens.SelectV2 if (validBeatmaps.Any()) { - carousel.CurrentSelection = difficultyRecommender?.GetRecommendedBeatmap(validBeatmaps) ?? validBeatmaps.First(); + carousel.CurrentBeatmap = difficultyRecommender?.GetRecommendedBeatmap(validBeatmaps) ?? validBeatmaps.First(); return true; } } From 71d0afd4c2b3c7fe6e92558b5ecd849f67cb6384 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Sep 2025 18:49:19 +0900 Subject: [PATCH 101/267] Attempt to fix flaky test `TestFilteringRunsAfterReturningFromGameplay` The double escape key press was dodgy from the get-go. --- osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 895f148965..553205d400 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -320,9 +320,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); AddUntilStep("wait for fail", () => ((Player)Stack.CurrentScreen).GameplayState.HasFailed); - AddStep("exit gameplay", () => InputManager.Key(Key.Escape)); - AddStep("exit gameplay", () => InputManager.Key(Key.Escape)); + AddStep("exit gameplay", () => Stack.CurrentScreen.Exit()); + AddUntilStep("wait for song select", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect); AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType().Single().FilterCount, () => Is.EqualTo(2)); } From 3e8775051eeea768ebb7b49e4091f34627e5fbf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Sep 2025 17:50:20 +0900 Subject: [PATCH 102/267] Implement local song select debounce rather than using `Scheduler.AddDelayed` This is to allow more custom handling of the debounce timing. --- osu.Game/Screens/SelectV2/SongSelect.cs | 75 ++++++++++++++++++------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index b2dc8404e4..85c19fe591 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -370,8 +370,56 @@ namespace osu.Game.Screens.SelectV2 new Dimension(), new Dimension(GridSizeMode.Relative, 0.5f, minSize: 500, maxSize: 700 + widescreenBonusWidth * 300), }; + + updateDebounce(); } + #region Selection debounce + + private BeatmapInfo? debounceQueuedSelection; + private double debounceElapsedTime; + + private void debounceQueueSelection(BeatmapInfo beatmap) + { + debounceQueuedSelection = beatmap; + debounceElapsedTime = 0; + } + + private void updateDebounce() + { + if (debounceQueuedSelection == null) return; + + debounceElapsedTime += Clock.ElapsedFrameTime; + + if (debounceElapsedTime >= SELECTION_DEBOUNCE) + performDebounceSelection(); + } + + private void performDebounceSelection() + { + if (debounceQueuedSelection == null) return; + + try + { + if (Beatmap.Value.BeatmapInfo.Equals(debounceQueuedSelection)) + return; + + Beatmap.Value = beatmaps.GetWorkingBeatmap(debounceQueuedSelection); + } + finally + { + cancelDebounceSelection(); + } + } + + private void cancelDebounceSelection() + { + debounceQueuedSelection = null; + debounceElapsedTime = 0; + } + + #endregion + #region Audio [Resolved] @@ -435,8 +483,6 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling - private ScheduledDelegate? selectionDebounce; - /// /// Finalises selection on the given and runs the provided action if possible. /// @@ -452,7 +498,7 @@ namespace osu.Game.Screens.SelectV2 // To ensure sanity, cancel any pending selection as we are about to force a selection. // Carousel selection will update to the forced selection via a call of `ensureGlobalBeatmapValid` below, or when song select becomes current again. - selectionDebounce?.Cancel(); + cancelDebounceSelection(); // Forced refetch is important here to guarantee correct invalidation across all difficulties (editor specific). Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true); @@ -481,14 +527,7 @@ namespace osu.Game.Screens.SelectV2 carousel.CurrentSelection = beatmap; // Debounce consideration is to avoid beatmap churn on key repeat selection. - selectionDebounce?.Cancel(); - selectionDebounce = Scheduler.AddDelayed(() => - { - if (Beatmap.Value.BeatmapInfo.Equals(beatmap)) - return; - - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); - }, SELECTION_DEBOUNCE); + debounceQueueSelection(beatmap); } private bool ensureGlobalBeatmapValid() @@ -496,7 +535,7 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return false; - finaliseBeatmapSelection(); + performDebounceSelection(); // While filtering, let's not ever attempt to change selection. // This will be resolved after the filter completes, see `newItemsPresented`. @@ -517,7 +556,7 @@ namespace osu.Game.Screens.SelectV2 if (Beatmap.IsDefault) { validSelection = carousel.NextRandom(); - finaliseBeatmapSelection(); + performDebounceSelection(); return validSelection; } @@ -539,15 +578,9 @@ namespace osu.Game.Screens.SelectV2 // If all else fails, use the default beatmap. Beatmap.SetDefault(); - finaliseBeatmapSelection(); + performDebounceSelection(); return validSelection; - - void finaliseBeatmapSelection() - { - if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) - selectionDebounce?.RunTask(); - } } private bool checkBeatmapValidForSelection(BeatmapInfo beatmap, FilterCriteria? criteria) @@ -794,7 +827,7 @@ namespace osu.Game.Screens.SelectV2 // Interrupting could cause the debounce interval to be reduced. // // `ensureGlobalBeatmapValid` is run post-selection which will resolve any pending incompatibilities (see `Beatmap` bindable callback). - if (selectionDebounce?.State != ScheduledDelegate.RunState.Waiting) + if (debounceQueuedSelection == null) ensureGlobalBeatmapValid(); updateWedgeVisibility(); From b97cb65444132ce7e976343f1a22d73c8ac11ebf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Sep 2025 17:56:16 +0900 Subject: [PATCH 103/267] Adjust song select debounce upwards slightly I still think it can probably go higher. Just going to do this in small increments until people complain / notice. --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 85c19fe591..9346c9c4f3 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.SelectV2 { // this is intentionally slightly higher than key repeat, but low enough to not impede user experience. // this avoids rapid churn loading when iterating the carousel using keyboard. - public const int SELECTION_DEBOUNCE = 100; + public const int SELECTION_DEBOUNCE = 150; private const float logo_scale = 0.4f; private const double fade_duration = 300; From 6ce76786ede893364bd2806931df2bf5e07b270c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Sep 2025 18:06:28 +0900 Subject: [PATCH 104/267] Better document various debounce constants and split out processing debounce for clarity --- .../BeatmapTitleWedge_DifficultyDisplay.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 2 +- .../Screens/SelectV2/PanelBeatmapStandalone.cs | 2 +- osu.Game/Screens/SelectV2/SongSelect.cs | 16 ++++++++++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 0e880a740f..55ed488d87 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -249,7 +249,7 @@ namespace osu.Game.Screens.SelectV2 mapperText.Text = beatmap.Value.Metadata.Author.Username; } - starRatingDisplay.Current = (Bindable)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, SongSelect.SELECTION_DEBOUNCE); + starRatingDisplay.Current = (Bindable)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE); updateCountStatistics(cancellationSource.Token); updateDifficultyStatistics(); diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 106b911606..78b2fe7590 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -250,7 +250,7 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE); + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE); starDifficultyBindable.BindValueChanged(starDifficulty => { starRatingDisplay.Current.Value = starDifficulty.NewValue; diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 87a35facbd..9e31445a87 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -264,7 +264,7 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE); + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE); starDifficultyBindable.BindValueChanged(starDifficulty => { starRatingDisplay.Current.Value = starDifficulty.NewValue; diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 9346c9c4f3..dc1cede819 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -63,10 +63,21 @@ namespace osu.Game.Screens.SelectV2 [Cached(typeof(ISongSelect))] public abstract partial class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandler, ISongSelect { - // this is intentionally slightly higher than key repeat, but low enough to not impede user experience. - // this avoids rapid churn loading when iterating the carousel using keyboard. + /// + /// A debounce that governs how long after a panel is selected before the rest of song select (and the game at large) + /// updates to show that selection. + /// + /// This is intentionally slightly higher than key repeat, but low enough to not impede user experience. + /// public const int SELECTION_DEBOUNCE = 150; + /// + /// A general "global" debounce to be applied to anything aggressive difficulty calculation at song select, + /// either after selection or after a panel comes on screen. Value should be low enough that users don't complain, + /// but otherwise as high as possible to reduce overheads. + /// + public const int DIFFICULTY_CALCULATION_DEBOUNCE = 150; + private const float logo_scale = 0.4f; private const double fade_duration = 300; @@ -1035,6 +1046,7 @@ namespace osu.Game.Screens.SelectV2 return; onlineLookupCancellation?.Cancel(); + onlineLookupCancellation = null; if (beatmapSetInfo.OnlineID < 0) { From 4ec620c4a9138e5034272c38a82c47e7fd6c3851 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Sep 2025 17:50:35 +0900 Subject: [PATCH 105/267] Fix song select debounce happening too early during fast iteration if there's a stutter --- osu.Game/Screens/SelectV2/SongSelect.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index dc1cede819..0312518603 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -400,7 +400,8 @@ namespace osu.Game.Screens.SelectV2 { if (debounceQueuedSelection == null) return; - debounceElapsedTime += Clock.ElapsedFrameTime; + // avoid debounce running early if there's a single long frame. + debounceElapsedTime += Math.Min(1000 / 60.0, Clock.ElapsedFrameTime); if (debounceElapsedTime >= SELECTION_DEBOUNCE) performDebounceSelection(); From 0bbad3e1cd4a9fba9435ef18f45060f0d4f72ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 11:31:21 +0200 Subject: [PATCH 106/267] Extract helper method for retrieving all user local scores --- osu.Game/Scoring/ScoreInfoExtensions.cs | 14 ++++++++++++++ .../Screens/Ranking/Statistics/StatisticsPanel.cs | 8 ++------ osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 10 +++------- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 ++------ osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs | 10 +++------- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index dd08326742..33b880a794 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Models; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select.Leaderboards; +using Realms; namespace osu.Game.Scoring { @@ -64,5 +66,17 @@ namespace osu.Game.Scoring /// The to compute the maximum achievable combo for. /// The maximum achievable combo. public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); + + /// + /// Performs a realm filter that returns all scores that belong to the user with the given . + /// (for guests) is supported. + /// + public static IQueryable GetAllLocalScoresForUser(this Realm realm, int? userId) + { + return realm.All() + .Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $@" && {nameof(ScoreInfo.DeletePending)} == false", userId); + } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 3c1aec745d..5c5c814c5b 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -19,7 +19,6 @@ using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Scoring; @@ -246,11 +245,8 @@ namespace osu.Game.Screens.Ranking.Statistics // We may want to iterate on the following conditions further in the future var localUserScore = AchievedScore ?? realm.Run(r => - r.All() - .Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $@" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, newScore.BeatmapInfo.ID, newScore.BeatmapInfo.Ruleset.ShortName) + r.GetAllLocalScoresForUser(api.LocalUser.Value.Id) + .Filter($@"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0", newScore.BeatmapInfo.ID) .AsEnumerable() .OrderByDescending(score => score.Ruleset.MatchesOnlineID(newScore.BeatmapInfo.Ruleset)) .ThenByDescending(score => score.Rank) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index da9661f702..6f1f2e8370 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; @@ -59,12 +58,9 @@ namespace osu.Game.Screens.Select.Carousel { scoreSubscription?.Dispose(); scoreSubscription = realm.RegisterForNotifications(r => - r.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" - + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName), + r.GetAllLocalScoresForUser(api.LocalUser.Value.Id) + .Filter($@"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1", beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); }, true); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c2711ceef0..04cad06745 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -25,7 +25,6 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; -using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Select; @@ -693,11 +692,8 @@ namespace osu.Game.Screens.SelectV2 { var topRankMapping = new Dictionary(); - var allLocalScores = r.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" - + $" && {nameof(ScoreInfo.DeletePending)} == false", criteria.LocalUserId, criteria.Ruleset?.ShortName) + var allLocalScores = r.GetAllLocalScoresForUser(criteria.LocalUserId) + .Filter($@"{nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0", criteria.Ruleset?.ShortName) .OrderByDescending(s => s.TotalScore) .ThenBy(s => s.Date); diff --git a/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs index 273f995794..c72835144f 100644 --- a/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs +++ b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; @@ -78,12 +77,9 @@ namespace osu.Game.Screens.SelectV2 return; scoreSubscription = realm.RegisterForNotifications(r => - r.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" - + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmap.ID, ruleset.Value.ShortName), + r.GetAllLocalScoresForUser(api.LocalUser.Value.Id) + .Filter($@"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1", beatmap.ID, ruleset.Value.ShortName), localScoresChanged); } From 15d73ce07edcbfeb45e484b9a4f500b69da17138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 11:57:24 +0200 Subject: [PATCH 107/267] Add test coverage --- .../SongSelect/TestSceneTopLocalRank.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 79baae53e8..93b9efed6a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Select.Carousel; @@ -161,5 +163,53 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("SS rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.X); } + + [Test] + public void TestGuestScore() + { + AddStep("Add score for guest user", () => + { + var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap); + + testScoreInfo.User = new GuestUser(); + testScoreInfo.Rank = ScoreRank.B; + + scoreManager.Import(testScoreInfo); + }); + + AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank, () => Is.EqualTo(ScoreRank.B)); + } + + [Test] + public void TestUnknownUserScore() + { + AddStep("Add score for unknown user", () => + { + var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap); + + testScoreInfo.User = new APIUser { Username = "AAA", }; + testScoreInfo.Rank = ScoreRank.S; + + scoreManager.Import(testScoreInfo); + }); + + AddUntilStep("S rank displayed", () => topLocalRank.DisplayedRank, () => Is.EqualTo(ScoreRank.S)); + } + + [Test] + public void TestAnotherUserScore() + { + AddStep("Add score for not-current user", () => + { + var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap); + + testScoreInfo.User = new APIUser { Username = "notme", Id = 43, }; + testScoreInfo.Rank = ScoreRank.S; + + scoreManager.Import(testScoreInfo); + }); + + AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank, () => Is.Null); + } } } From f1a020d2c6392cc0f98f0a9b8da716987a44a303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 11:37:08 +0200 Subject: [PATCH 108/267] Treat guest user scores & scores of unknown users as the local user's --- osu.Game/Scoring/ScoreInfoExtensions.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 33b880a794..2eec0399d6 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Models; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select.Leaderboards; using Realms; @@ -71,10 +72,16 @@ namespace osu.Game.Scoring /// Performs a realm filter that returns all scores that belong to the user with the given . /// (for guests) is supported. /// + /// + /// All guest scores (with user ID of ), + /// as well as scores of unknown provenance (with default user ID of 1, see ), + /// will be treated as if they belong to the local user. + /// This may not be necessarily considered fully correct in some circumstances, but in most cases it is the desired effect. + /// public static IQueryable GetAllLocalScoresForUser(this Realm realm, int? userId) { return realm.All() - .Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + .Filter($@"({nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0 || {nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} <= 1)" + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + $@" && {nameof(ScoreInfo.DeletePending)} == false", userId); } From a56f81a731ab76da9ea80d8eff2c4350604089bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 13:18:05 +0200 Subject: [PATCH 109/267] Fix abysmal code quality --- .../TestSceneArgonJudgementCounter.cs | 6 +-- .../Components/ArgonJudgementCounter.cs | 43 +++++++++---------- .../ArgonJudgementCounterDisplay.cs | 18 ++++---- 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs index 64bb6497ad..925cc087c5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All); AddAssert("Check no duplicates", () => counterDisplay.CounterFlow.ChildrenOfType().Count(), - () => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType().Select(c => c.JudgementName).Distinct().Count())); + () => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType().Select(c => c.Result.DisplayName).Distinct().Count())); } [Test] @@ -174,8 +174,8 @@ namespace osu.Game.Tests.Visual.Gameplay private int hiddenCount() { - var num = counterDisplay.CounterFlow.Children.First(child => child.JudgementCounter.Types.Contains(HitResult.LargeTickHit)); - return num.JudgementCounter.ResultCount.Value; + var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Types.Contains(HitResult.LargeTickHit)); + return num.Result.ResultCount.Value; } private partial class TestArgonJudgementCounterDisplay : ArgonJudgementCounterDisplay diff --git a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs index a627a53c02..482908cef3 100644 --- a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs @@ -18,45 +18,44 @@ namespace osu.Game.Skinning.Components { public sealed partial class ArgonJudgementCounter : VisibilityContainer { - public ArgonCounterTextComponent TextComponent; - private OsuColour colours = null!; - public readonly JudgementCount JudgementCounter; - public BindableInt DisplayedValue = new BindableInt(); - public string JudgementName; + public readonly JudgementCount Result; - public ArgonJudgementCounter(JudgementCount judgementCounter) + public IBindable WireframeOpacity => textComponent.WireframeOpacity; + public IBindable ShowLabel => textComponent.ShowLabel; + + private readonly ArgonCounterTextComponent textComponent; + private readonly BindableInt displayedValue = new BindableInt(); + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ArgonJudgementCounter(JudgementCount result) { - JudgementCounter = judgementCounter; - JudgementName = judgementCounter.DisplayName.ToUpper().ToString(); + Result = result; AutoSizeAxes = Axes.Both; - AddInternal(TextComponent = new ArgonCounterTextComponent(Anchor.TopRight, judgementCounter.DisplayName.ToUpper())); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - this.colours = colours; + AddInternal(textComponent = new ArgonCounterTextComponent(Anchor.TopRight, result.DisplayName.ToUpper())); } private void updateWireframe() { - int wireframeLenght = Math.Max(2, TextComponent.Text.ToString().Length); - TextComponent.WireframeTemplate = new string('#', wireframeLenght); + int wireframeLength = Math.Max(2, textComponent.Text.ToString().Length); + textComponent.WireframeTemplate = new string('#', wireframeLength); } protected override void LoadComplete() { base.LoadComplete(); - DisplayedValue.BindValueChanged(v => + displayedValue.BindTo(Result.ResultCount); + displayedValue.BindValueChanged(v => { - TextComponent.Text = v.NewValue.ToString(); + textComponent.Text = v.NewValue.ToString(); updateWireframe(); }, true); - var result = JudgementCounter.Types.First(); - TextComponent.LabelColour.Value = getJudgementColor(result); - TextComponent.ShowLabel.BindValueChanged(v => TextComponent.TextColour.Value = !v.NewValue ? getJudgementColor(result) : Color4.White); + var result = Result.Types.First(); + textComponent.LabelColour.Value = getJudgementColor(result); + textComponent.ShowLabel.BindValueChanged(v => textComponent.TextColour.Value = !v.NewValue ? getJudgementColor(result) : Color4.White); } private Color4 getJudgementColor(HitResult result) diff --git a/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs b/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs index 5e3ca725c6..227b1fd65f 100644 --- a/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; using osu.Game.Configuration; -using osu.Game.Graphics; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Scoring; @@ -48,7 +47,7 @@ namespace osu.Game.Skinning.Components protected FillFlowContainer CounterFlow = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { AutoSizeAxes = Axes.Both; InternalChild = CounterFlow = new FillFlowContainer @@ -61,9 +60,8 @@ namespace osu.Game.Skinning.Components foreach (var counter in judgementCountController.Counters) { ArgonJudgementCounter counterComponent = new ArgonJudgementCounter(counter); - counterComponent.TextComponent.WireframeOpacity.BindTo(WireframeOpacity); - counterComponent.TextComponent.ShowLabel.BindTo(ShowLabel); - counterComponent.DisplayedValue.BindTo(counter.ResultCount); + counterComponent.WireframeOpacity.BindTo(WireframeOpacity); + counterComponent.ShowLabel.BindTo(ShowLabel); CounterFlow.Add(counterComponent); } } @@ -71,9 +69,9 @@ namespace osu.Game.Skinning.Components protected override void LoadComplete() { base.LoadComplete(); - Mode.BindValueChanged(_ => updateVisibility(), true); + Mode.BindValueChanged(_ => updateVisibility()); ShowMaxJudgement.BindValueChanged(_ => updateVisibility(), true); - FlowDirection.BindValueChanged(d => CounterFlow.Direction = getFillDirection(d.NewValue)); + FlowDirection.BindValueChanged(d => CounterFlow.Direction = getFillDirection(d.NewValue), true); } private void updateVisibility() @@ -94,7 +92,7 @@ namespace osu.Game.Skinning.Components if (index == 0 && !ShowMaxJudgement.Value) return false; - var hitResult = counter.JudgementCounter.Types.First(); + var hitResult = counter.Result.Types.First(); if (hitResult.IsBasic()) return true; @@ -110,7 +108,7 @@ namespace osu.Game.Skinning.Components return true; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(Mode), Mode.Value, null); } } @@ -125,7 +123,7 @@ namespace osu.Game.Skinning.Components return FillDirection.Vertical; default: - throw new ArgumentOutOfRangeException(nameof(flow), flow, @"Unsupported direction"); + throw new ArgumentOutOfRangeException(nameof(flow), flow, null); } } From 98c3437174d5e405e23b9323bdd6d4e57c316056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 13:42:17 +0200 Subject: [PATCH 110/267] Always show the same number of placeholder digits in argon judgement counter when it is vertical --- .../TestSceneArgonJudgementCounter.cs | 13 +++++++++ .../Components/ArgonJudgementCounter.cs | 11 +++++-- .../ArgonJudgementCounterDisplay.cs | 29 ++++++++++++++++--- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs index 925cc087c5..e08af79032 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs @@ -101,6 +101,19 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical); AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal); + + AddStep("add 100 ok judgements", () => + { + for (int i = 0; i < 100; i++) + applyOneJudgement(HitResult.Ok); + }); + AddStep("add 1000 great judgements", () => + { + for (int i = 0; i < 1000; i++) + applyOneJudgement(HitResult.Great); + }); + + AddToggleStep("toggle max judgement display", t => counterDisplay.ShowMaxJudgement.Value = t); } [Test] diff --git a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs index 482908cef3..9d0e369682 100644 --- a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs @@ -21,6 +21,9 @@ namespace osu.Game.Skinning.Components public readonly JudgementCount Result; public IBindable WireframeOpacity => textComponent.WireframeOpacity; + + public IBindable WireframeDigits { get; } = new Bindable(); + public IBindable ShowLabel => textComponent.ShowLabel; private readonly ArgonCounterTextComponent textComponent; @@ -39,13 +42,15 @@ namespace osu.Game.Skinning.Components private void updateWireframe() { - int wireframeLength = Math.Max(2, textComponent.Text.ToString().Length); - textComponent.WireframeTemplate = new string('#', wireframeLength); + textComponent.WireframeTemplate = new string('#', WireframeDigits.Value ?? Math.Max(2, textComponent.Text.ToString().Length)); } protected override void LoadComplete() { base.LoadComplete(); + + WireframeDigits.BindValueChanged(_ => updateWireframe()); + displayedValue.BindTo(Result.ResultCount); displayedValue.BindValueChanged(v => { @@ -64,6 +69,6 @@ namespace osu.Game.Skinning.Components } protected override void PopIn() => this.FadeIn(JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint); - protected override void PopOut() => this.FadeOut(100); + protected override void PopOut() => this.FadeOut(); } } diff --git a/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs b/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs index 227b1fd65f..62b6d8ecc7 100644 --- a/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs @@ -44,6 +44,8 @@ namespace osu.Game.Skinning.Components [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.FlowDirection))] public Bindable FlowDirection { get; } = new Bindable(); + private readonly Bindable wireframeDigits = new Bindable(); + protected FillFlowContainer CounterFlow = null!; [BackgroundDependencyLoader] @@ -59,9 +61,13 @@ namespace osu.Game.Skinning.Components foreach (var counter in judgementCountController.Counters) { - ArgonJudgementCounter counterComponent = new ArgonJudgementCounter(counter); - counterComponent.WireframeOpacity.BindTo(WireframeOpacity); - counterComponent.ShowLabel.BindTo(ShowLabel); + counter.ResultCount.BindValueChanged(_ => updateWireframeDigits()); + ArgonJudgementCounter counterComponent = new ArgonJudgementCounter(counter) + { + WireframeOpacity = { BindTarget = WireframeOpacity }, + WireframeDigits = { BindTarget = wireframeDigits }, + ShowLabel = { BindTarget = ShowLabel }, + }; CounterFlow.Add(counterComponent); } } @@ -71,7 +77,7 @@ namespace osu.Game.Skinning.Components base.LoadComplete(); Mode.BindValueChanged(_ => updateVisibility()); ShowMaxJudgement.BindValueChanged(_ => updateVisibility(), true); - FlowDirection.BindValueChanged(d => CounterFlow.Direction = getFillDirection(d.NewValue), true); + FlowDirection.BindValueChanged(_ => updateFlowDirection(), true); } private void updateVisibility() @@ -85,6 +91,21 @@ namespace osu.Game.Skinning.Components else counter.Hide(); } + + updateWireframeDigits(); + } + + private void updateFlowDirection() + { + CounterFlow.Direction = getFillDirection(FlowDirection.Value); + updateWireframeDigits(); + } + + private void updateWireframeDigits() + { + wireframeDigits.Value = FlowDirection.Value == Direction.Vertical + ? Math.Max(2, CounterFlow.Children.Where(counter => counter.State.Value == Visibility.Visible).Max(counter => counter.Result.ResultCount.Value).ToString().Length) + : null; } private bool shouldBeVisible(int index, ArgonJudgementCounter counter) From b2501ae58f83afc295aaa59b73ccff4725d4fdb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Sep 2025 02:19:28 +0900 Subject: [PATCH 111/267] Change debounce consideration to use dynamic FPS moving average rather than fixed 60 fps This should better account for scenarios where user FPS is below 60 fps. Previously the debounce would unexpectedly be longer than usual for low FPS scenarios. --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index ce9d18cb71..64dd92f75b 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -402,7 +402,7 @@ namespace osu.Game.Screens.SelectV2 if (debounceQueuedSelection == null) return; // avoid debounce running early if there's a single long frame. - debounceElapsedTime += Math.Min(1000 / 60.0, Clock.ElapsedFrameTime); + debounceElapsedTime += Math.Min(1000 / Clock.FramesPerSecond, Clock.ElapsedFrameTime); if (debounceElapsedTime >= SELECTION_DEBOUNCE) performDebounceSelection(); From 1c608e779d1997190cee853a5a728967df4c1172 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Thu, 4 Sep 2025 14:01:51 +0800 Subject: [PATCH 112/267] Make DrawableDate formatting localizable --- osu.Game/Graphics/DrawableDate.cs | 3 ++- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs | 3 ++- osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs | 3 ++- osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 0e5bcc8019..641a4d80ce 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Utils; @@ -71,7 +72,7 @@ namespace osu.Game.Graphics Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate); } - protected virtual string Format() => HumanizerUtils.Humanize(Date); + protected virtual LocalisableString Format() => HumanizerUtils.Humanize(Date); private void updateTime() => Text = Format(); diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 8a30c4315b..0f29163e39 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -429,7 +429,7 @@ namespace osu.Game.Online.Leaderboards Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold); } - protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); + protected override LocalisableString Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); } public class LeaderboardScoreStatistic diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs index 59ba9cd449..9f7c5c848d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Localisation; using osu.Game.Extensions; using osu.Game.Graphics; @@ -14,7 +15,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { } - protected override string Format() + protected override LocalisableString Format() => Date.ToShortRelativeTime(TimeSpan.FromHours(1)); } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs index 3b03ce61f1..86a79ef0d6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Online.Rooms; @@ -62,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Date = room.EndDate ?? DateTimeOffset.Now.AddYears(1); } - protected override string Format() + protected override LocalisableString Format() { if (room.EndDate == null) return string.Empty; diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 67f3075e0e..80414d3f44 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -652,7 +652,7 @@ namespace osu.Game.Screens.SelectV2 Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold); } - protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); + protected override LocalisableString Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); } private partial class ScoreComponentLabel : Container From be365dfdc51673a86353f442c3c8b3f52e772256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Sep 2025 09:45:32 +0200 Subject: [PATCH 113/267] Fix not being able to report users from playlists chat Reported internally. --- .../Playlists/PlaylistsRoomSubScreen.cs | 411 +++++++++--------- 1 file changed, 208 insertions(+), 203 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 5b42bcf254..fdda6f6c85 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -13,6 +13,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Screens; @@ -164,225 +165,229 @@ namespace osu.Game.Screens.OnlinePlay.Playlists InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Child = new PopoverContainer { - roomUpdater = new PlaylistsRoomUpdater(room), - beatmapAvailabilityTracker, - new MultiplayerRoomSounds(), - new Container + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + roomUpdater = new PlaylistsRoomUpdater(room), + beatmapAvailabilityTracker, + new MultiplayerRoomSounds(), + new Container { - Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Bottom = footer_height + footer_padding - }, - Children = new[] - { - roomContent = new GridContainer + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = footer_height + footer_padding + }, + Children = new[] + { + roomContent = new GridContainer { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, row_padding), - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new PlaylistsRoomPanel(room) - { - SelectedItem = SelectedItem - } + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), }, - null, - new Drawable[] + Content = new[] { - new Container + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Children = new Drawable[] + new PlaylistsRoomPanel(room) { - new Box + SelectedItem = SelectedItem + } + }, + null, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(content_padding), - ColumnDimensions = new[] + new Box { - new Dimension(), - new Dimension(GridSizeMode.Absolute, column_padding), - new Dimension(), - new Dimension(GridSizeMode.Absolute, column_padding), - new Dimension(), + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. }, - Content = new[] + new GridContainer { - new Drawable?[] + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(content_padding), + ColumnDimensions = new[] { - new GridContainer + new Dimension(), + new Dimension(GridSizeMode.Absolute, column_padding), + new Dimension(), + new Dimension(GridSizeMode.Absolute, column_padding), + new Dimension(), + }, + Content = new[] + { + new Drawable?[] { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + new GridContainer { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new OverlinedPlaylistHeader(room), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), }, - new Drawable[] + Content = new[] { - drawablePlaylist = new DrawableRoomPlaylist + new Drawable[] { - RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem }, - AllowSelection = true, - AllowShowingResults = true, - RequestResults = showResults - } - }, - new Drawable[] - { - new AddPlaylistToCollectionButton(room) + new OverlinedPlaylistHeader(room), + }, + new Drawable[] { - Margin = new MarginPadding { Top = 5 }, - RelativeSizeAxes = Axes.X, - Size = new Vector2(1, 40) + drawablePlaylist = new DrawableRoomPlaylist + { + RelativeSizeAxes = Axes.Both, + SelectedItem = { BindTarget = SelectedItem }, + AllowSelection = true, + AllowShowingResults = true, + RequestResults = showResults + } + }, + new Drawable[] + { + new AddPlaylistToCollectionButton(room) + { + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 40) + } } } - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), }, - Content = new[] + null, + new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - userModsSection = new FillFlowContainer + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = row_padding }, - Alpha = 0, - Children = new Drawable[] + userModsSection = new FillFlowContainer { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, + Alpha = 0, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - new UserModSelectButton + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Height = 30, - Text = "Select", - Action = showUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Height = 30, + Text = "Select", + Action = showUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + } } } } } - } - }, - new Drawable[] - { - userStyleSection = new FillFlowContainer + }, + new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = row_padding }, - Alpha = 0, - Children = new Drawable[] + userStyleSection = new FillFlowContainer { - new OverlinedHeader("Difficulty"), - userStyleDisplayContainer = new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, + Alpha = 0, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y + new OverlinedHeader("Difficulty"), + userStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } } } - } - }, - new Drawable[] - { - progressSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = row_padding }, - Alpha = 0, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OverlinedHeader("Progress"), - new RoomLocalUserInfo(room), - } - } - }, - new Drawable[] - { - new OverlinedHeader("Leaderboard") - }, - new Drawable[] - { - leaderboard = new MatchLeaderboard(room) - { - RelativeSizeAxes = Axes.Both }, - } - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new OverlinedHeader("Chat") - }, - new Drawable[] - { - new MatchChatDisplay(room) + new Drawable[] { - RelativeSizeAxes = Axes.Both + progressSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, + Alpha = 0, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OverlinedHeader("Progress"), + new RoomLocalUserInfo(room), + } + } + }, + new Drawable[] + { + new OverlinedHeader("Leaderboard") + }, + new Drawable[] + { + leaderboard = new MatchLeaderboard(room) + { + RelativeSizeAxes = Axes.Both + }, + } + } + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OverlinedHeader("Chat") + }, + new Drawable[] + { + new MatchChatDisplay(room) + { + RelativeSizeAxes = Axes.Both + } } } } @@ -393,39 +398,39 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } } - } - }, - settingsOverlay = new PlaylistsRoomSettingsOverlay(room) - { - EditPlaylist = () => + }, + settingsOverlay = new PlaylistsRoomSettingsOverlay(room) { - if (this.IsCurrentScreen()) - this.Push(new PlaylistsSongSelect(room)); + EditPlaylist = () => + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistsSongSelect(room)); + } } } - } - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = footer_height, - Children = new Drawable[] + }, + new Container { - new Box + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = footer_height, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d") // Temporary. - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(5), - Child = new PlaylistsRoomFooter(room) + new Box { - OnStart = startPlay, - OnClose = closePlaylist + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = new PlaylistsRoomFooter(room) + { + OnStart = startPlay, + OnClose = closePlaylist + } } } } From b0c7b6c7007f48ebe3caad44ded179d50d4dbcac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Sep 2025 18:13:16 +0900 Subject: [PATCH 114/267] Fix multiple concerns with new debounce logic Tried many approaches but this seems simplest to guarantee no test (or other) regressions.. --- osu.Game/Screens/SelectV2/SongSelect.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 64dd92f75b..cc8c6afec2 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -12,6 +12,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -383,7 +384,8 @@ namespace osu.Game.Screens.SelectV2 new Dimension(GridSizeMode.Relative, 0.5f, minSize: 500, maxSize: 700 + widescreenBonusWidth * 300), }; - updateDebounce(); + if (this.IsCurrentScreen()) + updateDebounce(); } #region Selection debounce @@ -401,8 +403,13 @@ namespace osu.Game.Screens.SelectV2 { if (debounceQueuedSelection == null) return; + double elapsed = Clock.ElapsedFrameTime; + // avoid debounce running early if there's a single long frame. - debounceElapsedTime += Math.Min(1000 / Clock.FramesPerSecond, Clock.ElapsedFrameTime); + if (!DebugUtils.IsNUnitRunning && Clock.FramesPerSecond > 0) + elapsed = Math.Min(1000 / Clock.FramesPerSecond, elapsed); + + debounceElapsedTime += elapsed; if (debounceElapsedTime >= SELECTION_DEBOUNCE) performDebounceSelection(); From 815bf9c37bc920231bd024636d4690914f396793 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Sep 2025 15:04:22 +0900 Subject: [PATCH 115/267] Add matchmaking model types required for server-side deploy --- .../Matchmaking/MatchmakingRoomStateTest.cs | 153 ++++++++++++++++++ .../Online/Matchmaking/IMatchmakingClient.cs | 53 ++++++ .../Online/Matchmaking/IMatchmakingServer.cs | 51 ++++++ .../Matchmaking/MatchmakingLobbyStatus.cs | 16 ++ .../Matchmaking/MatchmakingQueueStatus.cs | 34 ++++ .../Online/Matchmaking/MatchmakingSettings.cs | 29 ++++ .../Matchmaking/MatchmakingStageCountdown.cs | 16 ++ osu.Game/Online/Multiplayer/MatchRoomState.cs | 2 + .../Matchmaking/MatchmakingRoomState.cs | 101 ++++++++++++ .../Matchmaking/MatchmakingRound.cs | 54 +++++++ .../Matchmaking/MatchmakingRoundList.cs | 49 ++++++ .../Matchmaking/MatchmakingStage.cs | 59 +++++++ .../MatchTypes/Matchmaking/MatchmakingUser.cs | 40 +++++ .../Matchmaking/MatchmakingUserComparer.cs | 65 ++++++++ .../Matchmaking/MatchmakingUserList.cs | 49 ++++++ .../Multiplayer/MultiplayerCountdown.cs | 2 + osu.Game/Online/Rooms/MatchType.cs | 2 + .../Online/Rooms/MultiplayerPlaylistItem.cs | 40 ++++- osu.Game/Online/SignalRWorkaroundTypes.cs | 9 ++ .../Multiplayer/TestMultiplayerClient.cs | 2 +- 20 files changed, 824 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs create mode 100644 osu.Game/Online/Matchmaking/IMatchmakingClient.cs create mode 100644 osu.Game/Online/Matchmaking/IMatchmakingServer.cs create mode 100644 osu.Game/Online/Matchmaking/MatchmakingLobbyStatus.cs create mode 100644 osu.Game/Online/Matchmaking/MatchmakingQueueStatus.cs create mode 100644 osu.Game/Online/Matchmaking/MatchmakingSettings.cs create mode 100644 osu.Game/Online/Matchmaking/MatchmakingStageCountdown.cs create mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs create mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRound.cs create mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs create mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingStage.cs create mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs create mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs create mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs diff --git a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs new file mode 100644 index 0000000000..c9219c871a --- /dev/null +++ b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs @@ -0,0 +1,153 @@ +// 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.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; + +namespace osu.Game.Tests.Online.Matchmaking +{ + public class MatchmakingRoomStateTest + { + /// + /// The number of points awarded for each placement position (index 0 = #1, index 7 = #8). + /// + private static readonly int[] placement_points = [8, 7, 6, 5, 4, 3, 2, 1]; + + [Test] + public void Basic() + { + var state = new MatchmakingRoomState(); + + // 1 -> 3 -> 2 + + state.AdvanceRound(); + state.RecordScores( + [ + new SoloScoreInfo { UserID = 2, TotalScore = 500 }, + new SoloScoreInfo { UserID = 1, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 3, TotalScore = 750 }, + ], placement_points); + + Assert.AreEqual(8, state.Users[1].Points); + Assert.AreEqual(1, state.Users[1].Placement); + Assert.AreEqual(1, state.Users[1].Rounds[1].Placement); + + Assert.AreEqual(6, state.Users[2].Points); + Assert.AreEqual(3, state.Users[2].Placement); + Assert.AreEqual(3, state.Users[2].Rounds[1].Placement); + + Assert.AreEqual(7, state.Users[3].Points); + Assert.AreEqual(2, state.Users[3].Placement); + Assert.AreEqual(2, state.Users[3].Rounds[1].Placement); + + // 2 -> 1 -> 3 + + state.AdvanceRound(); + state.RecordScores( + [ + new SoloScoreInfo { UserID = 2, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 1, TotalScore = 750 }, + new SoloScoreInfo { UserID = 3, TotalScore = 500 }, + ], placement_points); + + Assert.AreEqual(15, state.Users[1].Points); + Assert.AreEqual(1, state.Users[1].Placement); + Assert.AreEqual(2, state.Users[1].Rounds[2].Placement); + + Assert.AreEqual(14, state.Users[2].Points); + Assert.AreEqual(2, state.Users[2].Placement); + Assert.AreEqual(1, state.Users[2].Rounds[2].Placement); + + Assert.AreEqual(13, state.Users[3].Points); + Assert.AreEqual(3, state.Users[3].Placement); + Assert.AreEqual(3, state.Users[3].Rounds[2].Placement); + } + + [Test] + public void MatchingScores() + { + var state = new MatchmakingRoomState(); + + // 1 + 2 -> 3 + 4 + + state.AdvanceRound(); + state.RecordScores( + [ + new SoloScoreInfo { UserID = 1, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 2, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 3, TotalScore = 500 }, + new SoloScoreInfo { UserID = 4, TotalScore = 500 }, + ], placement_points); + + Assert.AreEqual(7, state.Users[1].Points); + Assert.AreEqual(1, state.Users[1].Placement); + Assert.AreEqual(2, state.Users[1].Rounds[1].Placement); + + Assert.AreEqual(7, state.Users[2].Points); + Assert.AreEqual(2, state.Users[2].Placement); + Assert.AreEqual(2, state.Users[2].Rounds[1].Placement); + + Assert.AreEqual(5, state.Users[3].Points); + Assert.AreEqual(3, state.Users[3].Placement); + Assert.AreEqual(4, state.Users[3].Rounds[1].Placement); + + Assert.AreEqual(5, state.Users[4].Points); + Assert.AreEqual(4, state.Users[4].Placement); + Assert.AreEqual(4, state.Users[4].Rounds[1].Placement); + } + + [Test] + public void RoundTieBreaker() + { + var state = new MatchmakingRoomState(); + + // 1 -> 2 + + state.AdvanceRound(); + state.RecordScores( + [ + new SoloScoreInfo { UserID = 1, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 2, TotalScore = 500 }, + ], placement_points); + + // 2 -> 1 + + state.AdvanceRound(); + state.RecordScores( + [ + new SoloScoreInfo { UserID = 1, TotalScore = 500 }, + new SoloScoreInfo { UserID = 2, TotalScore = 1000 }, + ], placement_points); + + Assert.AreEqual(1, state.Users[1].Placement); + Assert.AreEqual(2, state.Users[2].Placement); + } + + [Test] + public void UserIdTieBreaker() + { + var state = new MatchmakingRoomState(); + + // 1 + 2 + 3 + 4 + 5 + 6 + + state.AdvanceRound(); + state.RecordScores( + [ + new SoloScoreInfo { UserID = 4, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 6, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 2, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 3, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 1, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 5, TotalScore = 1000 }, + ], placement_points); + + Assert.AreEqual(1, state.Users[1].Placement); + Assert.AreEqual(2, state.Users[2].Placement); + Assert.AreEqual(3, state.Users[3].Placement); + Assert.AreEqual(4, state.Users[4].Placement); + Assert.AreEqual(5, state.Users[5].Placement); + Assert.AreEqual(6, state.Users[6].Placement); + } + } +} diff --git a/osu.Game/Online/Matchmaking/IMatchmakingClient.cs b/osu.Game/Online/Matchmaking/IMatchmakingClient.cs new file mode 100644 index 0000000000..70e1ce0b5d --- /dev/null +++ b/osu.Game/Online/Matchmaking/IMatchmakingClient.cs @@ -0,0 +1,53 @@ +// 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; + +namespace osu.Game.Online.Matchmaking +{ + public interface IMatchmakingClient : IStatefulUserHubClient + { + /// + /// Signals that the local user was placed in the matchmaking queue. + /// + Task MatchmakingQueueJoined(); + + /// + /// Signals that the local user was removed from the matchmaking queue. + /// + Task MatchmakingQueueLeft(); + + /// + /// Signals that a match has been found and the local user is invited to it. + /// The invitation may be accepted, + /// declined, + /// or ignored - in which case it will automatically be declined after a short timeout period. + /// + Task MatchmakingRoomInvited(); + + /// + /// Signals that the matchmaking room is ready to be opened. + /// + Task MatchmakingRoomReady(long roomId, string password); + + /// + /// The matchmaking lobby status has changed. + /// + Task MatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status); + + /// + /// The matchmaking status of the current user has changed. + /// + Task MatchmakingQueueStatusChanged(MatchmakingQueueStatus status); + + /// + /// The user has raised a candidate playlist item to be played. + /// + Task MatchmakingItemSelected(int userId, long playlistItemId); + + /// + /// The user has removed a candidate playlist item. + /// + Task MatchmakingItemDeselected(int userId, long playlistItemId); + } +} diff --git a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs new file mode 100644 index 0000000000..6bfd340b8c --- /dev/null +++ b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs @@ -0,0 +1,51 @@ +// 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; + +namespace osu.Game.Online.Matchmaking +{ + public interface IMatchmakingServer + { + /// + /// Joins the matchmaking lobby, allowing the local user to receive status updates. + /// + Task MatchmakingJoinLobby(); + + /// + /// Leaves the matchmaking lobby. + /// + Task MatchmakingLeaveLobby(); + + /// + /// Joins the matchmaking queue, allowing the local user to get matched up with others. + /// + Task MatchmakingJoinQueue(MatchmakingSettings settings); + + /// + /// Leaves the matchmaking queue. + /// + Task MatchmakingLeaveQueue(); + + /// + /// Accepts a matchmaking room invitation. + /// + Task MatchmakingAcceptInvitation(); + + /// + /// Declines a matchmaking room invitation. + /// + Task MatchmakingDeclineInvitation(); + + /// + /// Raise a candidate playlist item to be played in the current round. + /// + /// The playlist item. + Task MatchmakingToggleSelection(long playlistItemId); + + /// + /// Debug only - skips to the next stage of the matchmaking room. + /// + Task MatchmakingSkipToNextStage(); + } +} diff --git a/osu.Game/Online/Matchmaking/MatchmakingLobbyStatus.cs b/osu.Game/Online/Matchmaking/MatchmakingLobbyStatus.cs new file mode 100644 index 0000000000..9a1e083b84 --- /dev/null +++ b/osu.Game/Online/Matchmaking/MatchmakingLobbyStatus.cs @@ -0,0 +1,16 @@ +// 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 MessagePack; + +namespace osu.Game.Online.Matchmaking +{ + [Serializable] + [MessagePackObject] + public class MatchmakingLobbyStatus + { + [Key(0)] + public int[] UsersInQueue { get; set; } = []; + } +} diff --git a/osu.Game/Online/Matchmaking/MatchmakingQueueStatus.cs b/osu.Game/Online/Matchmaking/MatchmakingQueueStatus.cs new file mode 100644 index 0000000000..a57e04bd10 --- /dev/null +++ b/osu.Game/Online/Matchmaking/MatchmakingQueueStatus.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 System; +using MessagePack; + +namespace osu.Game.Online.Matchmaking +{ + [Serializable] + [MessagePackObject] + [Union(0, typeof(Searching))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + [Union(1, typeof(MatchFound))] + [Union(2, typeof(JoiningMatch))] + public abstract class MatchmakingQueueStatus + { + [Serializable] + [MessagePackObject] + public class Searching : MatchmakingQueueStatus + { + } + + [Serializable] + [MessagePackObject] + public class MatchFound : MatchmakingQueueStatus + { + } + + [Serializable] + [MessagePackObject] + public class JoiningMatch : MatchmakingQueueStatus + { + } + } +} diff --git a/osu.Game/Online/Matchmaking/MatchmakingSettings.cs b/osu.Game/Online/Matchmaking/MatchmakingSettings.cs new file mode 100644 index 0000000000..050133e192 --- /dev/null +++ b/osu.Game/Online/Matchmaking/MatchmakingSettings.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 System; +using System.Diagnostics.CodeAnalysis; +using MessagePack; + +namespace osu.Game.Online.Matchmaking +{ + [MessagePackObject] + [Serializable] + public class MatchmakingSettings : IEquatable + { + [Key(0)] + public int RulesetId { get; set; } + + public bool Equals(MatchmakingSettings? other) + => other != null && RulesetId == other.RulesetId; + + public override bool Equals(object? obj) + => obj is MatchmakingSettings other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() + { + return RulesetId; + } + } +} diff --git a/osu.Game/Online/Matchmaking/MatchmakingStageCountdown.cs b/osu.Game/Online/Matchmaking/MatchmakingStageCountdown.cs new file mode 100644 index 0000000000..8df1bb000a --- /dev/null +++ b/osu.Game/Online/Matchmaking/MatchmakingStageCountdown.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using MessagePack; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; + +namespace osu.Game.Online.Matchmaking +{ + [MessagePackObject] + public class MatchmakingStageCountdown : MultiplayerCountdown + { + [Key(2)] + public MatchmakingStage Stage { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MatchRoomState.cs b/osu.Game/Online/Multiplayer/MatchRoomState.cs index cae3aaf7d0..25de8c7fab 100644 --- a/osu.Game/Online/Multiplayer/MatchRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchRoomState.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; namespace osu.Game.Online.Multiplayer @@ -14,6 +15,7 @@ namespace osu.Game.Online.Multiplayer [Serializable] [MessagePackObject] [Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + [Union(1, typeof(MatchmakingRoomState))] public abstract class MatchRoomState { } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs new file mode 100644 index 0000000000..9e1953fc59 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs @@ -0,0 +1,101 @@ +// 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 MessagePack; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking +{ + /// + /// Describes the state of a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingRoomState : MatchRoomState + { + /// + /// The current room status. + /// + [Key(0)] + public MatchmakingStage Stage { get; set; } + + /// + /// The current round number (1-based). + /// + [Key(1)] + public int CurrentRound { get; set; } + + /// + /// The playlist items that were picked as gameplay candidates. + /// + [Key(2)] + public long[] CandidateItems { get; set; } = []; + + /// + /// The final gameplay candidate. + /// + [Key(3)] + public long CandidateItem { get; set; } + + /// + /// The users in the room. + /// + [Key(4)] + public MatchmakingUserList Users { get; set; } = new MatchmakingUserList(); + + /// + /// Advances to the next round. + /// + public void AdvanceRound() + { + CurrentRound++; + } + + /// + /// Sets scores for the current round, applying points and adjusting user placements. + /// + /// + /// When applying points: + /// + /// Matching scores are considered to be placed in the lower-equal (e.g. two equal top scores are considered "equal-second"). + /// Failed scores are considered to have passed the map. + /// Missing scores are not considered. + /// + /// + /// The scores to apply. + /// The number of points to award for each placement position (0-indexed). Must be at least of equal length to . + public void RecordScores(SoloScoreInfo[] scores, int[] placementPoints) + { + if (placementPoints.Length < scores.Length) + throw new ArgumentException($"{nameof(placementPoints)} must be at least of equal length to {nameof(scores)}."); + + SoloScoreInfo[] orderedScores = scores.OrderByDescending(s => s.TotalScore).ToArray(); + + int placement = 0; + + foreach (var scoreGroup in orderedScores.GroupBy(s => s.TotalScore)) + { + placement += scoreGroup.Count(); + + foreach (var score in scoreGroup) + { + MatchmakingUser mmUser = Users[score.UserID]; + mmUser.Points += placementPoints[placement - 1]; + + MatchmakingRound mmRound = mmUser.Rounds[CurrentRound]; + mmRound.Placement = placement; + mmRound.TotalScore = score.TotalScore; + mmRound.Accuracy = score.Accuracy; + mmRound.MaxCombo = score.MaxCombo; + mmRound.Statistics = score.Statistics; + } + } + + int i = 1; + foreach (var user in Users.Order(new MatchmakingUserComparer(CurrentRound))) + user.Placement = i++; + } + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRound.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRound.cs new file mode 100644 index 0000000000..6a9d595ab5 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRound.cs @@ -0,0 +1,54 @@ +// 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 MessagePack; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking +{ + /// + /// Describes a user's score for a round of a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingRound + { + /// + /// The round. + /// + [Key(0)] + public required int Round { get; set; } + + /// + /// The user's placement in this round (1-based). + /// + [Key(1)] + public int Placement { get; set; } + + /// + /// The achieved total score. + /// + [Key(2)] + public long TotalScore { get; set; } + + /// + /// The achieved accuracy. + /// + [Key(3)] + public double Accuracy { get; set; } + + /// + /// The achieved maximum combo. + /// + [Key(4)] + public int MaxCombo { get; set; } + + /// + /// The achieved score statistics. + /// + [Key(5)] + public IDictionary Statistics { get; set; } = new Dictionary(); + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs new file mode 100644 index 0000000000..c34d1771f8 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.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 System; +using System.Collections; +using System.Collections.Generic; +using MessagePack; + +namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking +{ + /// + /// Describes the per-round scores of a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingRoundList : IEnumerable + { + /// + /// A key-value-pair mapping of rounds to scores. + /// + [Key(0)] + public IDictionary RoundsDictionary { get; set; } = new Dictionary(); + + /// + /// Creates or retrieves the score for the given round. + /// + /// The round. + public MatchmakingRound this[int round] + { + get + { + if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score)) + return score; + + return RoundsDictionary[round] = new MatchmakingRound { Round = round }; + } + } + + /// + /// The total number of rounds. + /// + [IgnoreMember] + public int Count => RoundsDictionary.Count; + + public IEnumerator GetEnumerator() => RoundsDictionary.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingStage.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingStage.cs new file mode 100644 index 0000000000..edffa4ec23 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingStage.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking +{ + /// + /// Describes the current status of a matchmaking room. + /// + [Serializable] + public enum MatchmakingStage + { + /// + /// The initial state of a room. Users are still joining. + /// + WaitingForClientsJoin, + + /// + /// A short delay before the round begins. + /// + RoundWarmupTime, + + /// + /// Users are given a chance to lock in their beatmap picks. + /// + UserBeatmapSelect, + + /// + /// Clients have sent their picks, and the server has responded with the finalised beatmap. + /// + ServerBeatmapFinalised, + + /// + /// Clients are given an opportunity to download the beatmap. + /// + WaitingForClientsBeatmapDownload, + + /// + /// A short delay before gameplay starts. + /// + GameplayWarmupTime, + + /// + /// Gameplay is ongoing. + /// + Gameplay, + + /// + /// Gameplay has finished, results are being displayed. + /// + ResultsDisplaying, + + /// + /// All rounds have completed. Users may still be chatting. + /// + Ended + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs new file mode 100644 index 0000000000..f596f2473e --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs @@ -0,0 +1,40 @@ +// 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 MessagePack; + +namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking +{ + /// + /// Describes a user of a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingUser + { + /// + /// The user's ID. + /// + [Key(0)] + public required int UserId { get; set; } + + /// + /// The aggregate room placement (1-based). + /// + [Key(1)] + public int Placement { get; set; } + + /// + /// The aggregate points. + /// + [Key(2)] + public int Points { get; set; } + + /// + /// The scores set. + /// + [Key(3)] + public MatchmakingRoundList Rounds { get; set; } = new MatchmakingRoundList(); + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs new file mode 100644 index 0000000000..74da6a9b2a --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs @@ -0,0 +1,65 @@ +// 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; + +namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking +{ + /// + /// Orders in order of placement. + /// + public class MatchmakingUserComparer : Comparer + { + private readonly int rounds; + + public MatchmakingUserComparer(int rounds) + { + this.rounds = rounds; + } + + public override int Compare(MatchmakingUser? x, MatchmakingUser? y) + { + ArgumentNullException.ThrowIfNull(x); + ArgumentNullException.ThrowIfNull(y); + + // X appears earlier in the list if it has more points. + if (x.Points > y.Points) + return -1; + + // Y appears earlier in the list if it has more points. + if (y.Points > x.Points) + return 1; + + // Tiebreaker 1 (likely): From each user's point-of-view, their earliest and best placement. + for (int r = 1; r <= rounds; r++) + { + MatchmakingRound? xRound; + x.Rounds.RoundsDictionary.TryGetValue(r, out xRound); + + MatchmakingRound? yRound; + y.Rounds.RoundsDictionary.TryGetValue(r, out yRound); + + // Nothing to do if both players haven't played this round. + if (xRound == null && yRound == null) + continue; + + // X appears later in the list if it hasn't played this round. + if (xRound == null) + return 1; + + // Y appears later in the list if it hasn't played this round. + if (yRound == null) + return -1; + + // X appears earlier in the list if it has a better placement in the round. + int compare = xRound.Placement.CompareTo(yRound.Placement); + if (compare != 0) + return compare; + } + + // Tiebreaker 2 (unlikely): User ID. + return x.UserId.CompareTo(y.UserId); + } + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs new file mode 100644 index 0000000000..600134de4e --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.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 System; +using System.Collections; +using System.Collections.Generic; +using MessagePack; + +namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking +{ + /// + /// Describes the users of a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingUserList : IEnumerable + { + /// + /// A key-value-pair mapping of ids to users. + /// + [Key(0)] + public IDictionary UserDictionary { get; set; } = new Dictionary(); + + /// + /// Creates or retrieves the user for the given id. + /// + /// The user id. + public MatchmakingUser this[int userId] + { + get + { + if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user)) + return user; + + return UserDictionary[userId] = new MatchmakingUser { UserId = userId }; + } + } + + /// + /// The total number of users. + /// + [IgnoreMember] + public int Count => UserDictionary.Count; + + public IEnumerator GetEnumerator() => UserDictionary.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs index c59f5937b0..bc2536848b 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer.Countdown; namespace osu.Game.Online.Multiplayer @@ -14,6 +15,7 @@ namespace osu.Game.Online.Multiplayer [Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. [Union(1, typeof(ForceGameplayStartCountdown))] [Union(2, typeof(ServerShuttingDownCountdown))] + [Union(3, typeof(MatchmakingStageCountdown))] public abstract class MultiplayerCountdown { /// diff --git a/osu.Game/Online/Rooms/MatchType.cs b/osu.Game/Online/Rooms/MatchType.cs index ade28458e8..bbfe25c8fd 100644 --- a/osu.Game/Online/Rooms/MatchType.cs +++ b/osu.Game/Online/Rooms/MatchType.cs @@ -17,5 +17,7 @@ namespace osu.Game.Online.Rooms [LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesTeamVersus))] TeamVersus, + + Matchmaking } } diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index d8ed20a3a8..3386b8654d 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using MessagePack; using osu.Game.Online.API; @@ -13,7 +14,7 @@ namespace osu.Game.Online.Rooms { [Serializable] [MessagePackObject] - public class MultiplayerPlaylistItem + public class MultiplayerPlaylistItem : IEquatable { [Key(0)] public long ID { get; set; } @@ -118,5 +119,42 @@ namespace osu.Game.Online.Rooms clone.AllowedMods = AllowedMods.ToArray(); return clone; } + + public bool Equals(MultiplayerPlaylistItem? other) + => other != null + && ID == other.ID + && OwnerID == other.OwnerID + && BeatmapID == other.BeatmapID + && BeatmapChecksum == other.BeatmapChecksum + && RulesetID == other.RulesetID + && RequiredMods.SequenceEqual(other.RequiredMods) + && AllowedMods.SequenceEqual(other.AllowedMods) + && Expired == other.Expired + && PlaylistOrder == other.PlaylistOrder + && PlayedAt == other.PlayedAt + && StarRating == other.StarRating + && Freestyle == other.Freestyle; + + public override bool Equals(object? obj) + => obj is MultiplayerPlaylistItem other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(ID); + hashCode.Add(OwnerID); + hashCode.Add(BeatmapID); + hashCode.Add(BeatmapChecksum); + hashCode.Add(RulesetID); + hashCode.Add(RequiredMods); + hashCode.Add(AllowedMods); + hashCode.Add(Expired); + hashCode.Add(PlaylistOrder); + hashCode.Add(PlayedAt); + hashCode.Add(StarRating); + hashCode.Add(Freestyle); + return hashCode.ToHashCode(); + } } } diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 6ae178e04c..04d4b8d7af 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Users; @@ -45,6 +47,13 @@ namespace osu.Game.Online (typeof(UserActivity.TestingBeatmap), typeof(UserActivity)), (typeof(UserActivity.InDailyChallengeLobby), typeof(UserActivity)), (typeof(UserActivity.PlayingDailyChallenge), typeof(UserActivity)), + + // matchmaking + (typeof(MatchmakingQueueStatus.Searching), typeof(MatchmakingQueueStatus)), + (typeof(MatchmakingQueueStatus.MatchFound), typeof(MatchmakingQueueStatus)), + (typeof(MatchmakingQueueStatus.JoiningMatch), typeof(MatchmakingQueueStatus)), + (typeof(MatchmakingRoomState), typeof(MatchRoomState)), + (typeof(MatchmakingStageCountdown), typeof(MultiplayerCountdown)) }; } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 3843460add..806dc63aed 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -508,7 +508,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (item == null) throw new InvalidOperationException("Item does not exist in the room."); - if (item == currentItem) + if (item.Equals(currentItem)) throw new InvalidOperationException("The room's current item cannot be removed."); if (item.OwnerID != userId) From ae0f9619b9fe2b9518912a16d7dbdf6f01f1fbfe Mon Sep 17 00:00:00 2001 From: CloneWith Date: Thu, 4 Sep 2025 17:30:10 +0800 Subject: [PATCH 116/267] Change wrong overwrite type in ScheduleScreen --- osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index d02559d6b7..149b0a25d8 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -264,7 +265,7 @@ namespace osu.Game.Tournament.Screens.Schedule { } - protected override string Format() => Date < DateTimeOffset.Now + protected override LocalisableString Format() => Date < DateTimeOffset.Now ? $"Started {base.Format()}" : $"Starting {base.Format()}"; } From 1627f67ada890829b5b0af7f964c8de8f61f966e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Sep 2025 18:43:46 +0900 Subject: [PATCH 117/267] Add variant to `MatchmakingSettings` --- osu.Game/Online/Matchmaking/MatchmakingSettings.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Matchmaking/MatchmakingSettings.cs b/osu.Game/Online/Matchmaking/MatchmakingSettings.cs index 050133e192..c1a10e97ad 100644 --- a/osu.Game/Online/Matchmaking/MatchmakingSettings.cs +++ b/osu.Game/Online/Matchmaking/MatchmakingSettings.cs @@ -14,16 +14,18 @@ namespace osu.Game.Online.Matchmaking [Key(0)] public int RulesetId { get; set; } + [Key(1)] + public int Variant { get; set; } + public bool Equals(MatchmakingSettings? other) - => other != null && RulesetId == other.RulesetId; + => other != null + && RulesetId == other.RulesetId + && Variant == other.Variant; public override bool Equals(object? obj) => obj is MatchmakingSettings other && Equals(other); [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] - public override int GetHashCode() - { - return RulesetId; - } + public override int GetHashCode() => HashCode.Combine(RulesetId, Variant); } } From 6e5bf57fe7918d1a12b63f9ff5da6598db823661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Sep 2025 12:44:18 +0200 Subject: [PATCH 118/267] Ensure that matching beatmap still exists when performing replace operation in the carousel --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 952e545d0a..15e8ed5f97 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -251,6 +251,11 @@ namespace osu.Game.Screens.SelectV2 newSetBeatmaps.FirstOrDefault(b => b.OnlineID > 0 && b.OnlineID == beatmap.OnlineID) ?? newSetBeatmaps.FirstOrDefault(b => b.DifficultyName == beatmap.DifficultyName && b.Ruleset.Equals(beatmap.Ruleset)); + // The matching beatmap may have been deleted or invalidated in some way since this event was fired. + // Let's make sure we have the most up-to-date realm state. + if (matchingNewBeatmap?.ID is Guid matchingID) + matchingNewBeatmap = realm.Run(r => r.FindWithRefresh(matchingID)?.Detach()); + if (matchingNewBeatmap != null) { // TODO: should this exist in song select instead of here? From bae288859b49d25a6903fd036dad9893add4c5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Sep 2025 12:44:36 +0200 Subject: [PATCH 119/267] Fix test failing because of new realm check --- .../TestSceneBeatmapCarouselUpdateHandling.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index d1cef3420a..86ef2cffba 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -30,7 +31,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RemoveAllBeatmaps(); CreateCarousel(); WaitForFiltering(); - AddBeatmaps(1, 3); + AddStep("add beatmap", () => + { + var beatmap = CreateTestBeatmapSetInfo(3, false); + Realm.Write(r => r.Add(beatmap, update: true)); + BeatmapSets.Add(beatmap.Detach()); + }); WaitForFiltering(); AddStep("generate and add test beatmap", () => { @@ -44,7 +50,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var b in baseTestBeatmap.Beatmaps) b.Metadata = metadata; - BeatmapSets.Add(baseTestBeatmap); + + Realm.Write(r => r.Add(baseTestBeatmap, update: true)); + BeatmapSets.Add(baseTestBeatmap.Detach()); }); WaitForFiltering(); @@ -269,14 +277,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2 updateBeatmap(null, bs => { string selectedName = bs.Beatmaps[0].DifficultyName; + Realm.Write(r => r.Remove(r.Find(bs.Beatmaps[0].ID)!)); bs.Beatmaps.RemoveAt(0); var newBeatmap = createBeatmap(bs); + newBeatmap.ID = Guid.NewGuid(); newBeatmap.DifficultyName = selectedName; newBeatmap.OnlineID = -1; bs.Beatmaps.Add(newBeatmap); newBeatmap = createBeatmap(bs); + newBeatmap.ID = Guid.NewGuid(); newBeatmap.DifficultyName = selectedName; newBeatmap.OnlineID = -1; bs.Beatmaps.Add(newBeatmap); @@ -284,8 +295,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForFiltering(); - AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); - AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(BeatmapSets[1].Beatmaps[2])); + AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(BeatmapSets[1].Beatmaps[2])); } /// @@ -439,7 +450,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 int originalIndex = BeatmapSets.IndexOf(baseTestBeatmap); - BeatmapSets.ReplaceRange(originalIndex, 1, [updatedSet]); + Realm.Write(r => r.Add(updatedSet, update: true)); + BeatmapSets.ReplaceRange(originalIndex, 1, [updatedSet.Detach()]); }); } From 134f854d7ba18e7a2ee82c51f99c49154c711422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Sep 2025 12:46:00 +0200 Subject: [PATCH 120/267] Fix broken reselection logic d4b357dfa0133d24083aa08372644f86c799a14b contains a sneaky regression. The previous code read: if (CurrentSelection != null && CheckModelEquality(beatmap, CurrentSelection)) RequestSelection(matchingNewBeatmap); and the new one reads: if (CurrentSelection is GroupedBeatmap currentBeatmapUnderGrouping) { var candidateSelection = currentBeatmapUnderGrouping with { Beatmap = beatmap }; if (CheckModelEquality(candidateSelection, CurrentSelection)) RequestSelection(candidateSelection); } The point is that we want to reselect `matchingNewBeatmap` here, not the old selection. The `CheckModelEquality()` check's purpose is to check whether *the current selection needs updating*. I'm not sure why tests just wonderfully passed despite this, but my suspicion is that it was because of accidental copying of realm guids that obscured this problem. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 15e8ed5f97..bcac74662e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -260,12 +260,10 @@ namespace osu.Game.Screens.SelectV2 { // TODO: should this exist in song select instead of here? // we need to ensure the global beatmap is also updated alongside changes. - if (CurrentSelection is GroupedBeatmap currentBeatmapUnderGrouping) - { - var candidateSelection = currentBeatmapUnderGrouping with { Beatmap = beatmap }; - if (CheckModelEquality(candidateSelection, CurrentSelection)) - RequestSelection(candidateSelection); - } + if (CurrentBeatmap != null && beatmap.Equals(CurrentBeatmap)) + // we don't know in which group the matching new beatmap is, but that's fine - we can leave it null for now. + // we are about to modify `Items`, which will trigger a re-filter, which will pick a correct group - if one is present - via `HandleFilterCompleted()`. + RequestSelection(new GroupedBeatmap(null, matchingNewBeatmap)); Items.ReplaceRange(previousIndex, 1, [matchingNewBeatmap]); newSetBeatmaps.Remove(matchingNewBeatmap); From 32576ff2498c255fb5e6e2cb14fe10ff1dfa00a2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Sep 2025 14:33:28 +0900 Subject: [PATCH 121/267] Replace MatchmakingSettings with MatchmakingPool --- .../Online/Matchmaking/IMatchmakingServer.cs | 2 +- ...chmakingSettings.cs => MatchmakingPool.cs} | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) rename osu.Game/Online/Matchmaking/{MatchmakingSettings.cs => MatchmakingPool.cs} (55%) diff --git a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs index 6bfd340b8c..aef18371e3 100644 --- a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs +++ b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Online.Matchmaking /// /// Joins the matchmaking queue, allowing the local user to get matched up with others. /// - Task MatchmakingJoinQueue(MatchmakingSettings settings); + Task MatchmakingJoinQueue(int poolId); /// /// Leaves the matchmaking queue. diff --git a/osu.Game/Online/Matchmaking/MatchmakingSettings.cs b/osu.Game/Online/Matchmaking/MatchmakingPool.cs similarity index 55% rename from osu.Game/Online/Matchmaking/MatchmakingSettings.cs rename to osu.Game/Online/Matchmaking/MatchmakingPool.cs index c1a10e97ad..3f256d5251 100644 --- a/osu.Game/Online/Matchmaking/MatchmakingSettings.cs +++ b/osu.Game/Online/Matchmaking/MatchmakingPool.cs @@ -9,23 +9,31 @@ namespace osu.Game.Online.Matchmaking { [MessagePackObject] [Serializable] - public class MatchmakingSettings : IEquatable + public class MatchmakingPool : IEquatable { [Key(0)] - public int RulesetId { get; set; } + public int Id { get; set; } [Key(1)] + public int RulesetId { get; set; } + + [Key(2)] public int Variant { get; set; } - public bool Equals(MatchmakingSettings? other) + [Key(3)] + public string Name { get; set; } = string.Empty; + + public bool Equals(MatchmakingPool? other) => other != null + && Id == other.Id && RulesetId == other.RulesetId - && Variant == other.Variant; + && Variant == other.Variant + && Name == other.Name; public override bool Equals(object? obj) - => obj is MatchmakingSettings other && Equals(other); + => obj is MatchmakingPool other && Equals(other); [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] - public override int GetHashCode() => HashCode.Combine(RulesetId, Variant); + public override int GetHashCode() => HashCode.Combine(Id, RulesetId, Variant, Name); } } From 4475bcafa9de366078d6b3857a79dc71a8f8aa84 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Sep 2025 14:40:58 +0900 Subject: [PATCH 122/267] Add `GetMatchmakingPools` server method --- osu.Game/Online/Matchmaking/IMatchmakingServer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs index aef18371e3..66fd8c36da 100644 --- a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs +++ b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs @@ -7,6 +7,11 @@ namespace osu.Game.Online.Matchmaking { public interface IMatchmakingServer { + /// + /// Retrieves all active matchmaking pools. + /// + Task GetMatchmakingPools(); + /// /// Joins the matchmaking lobby, allowing the local user to receive status updates. /// From 54d4b16a01addc23d34449f206afd6f73182f026 Mon Sep 17 00:00:00 2001 From: NiyazBiyaz Date: Tue, 2 Sep 2025 23:53:26 +0500 Subject: [PATCH 123/267] Fix `rank-up` and `rank-down` sounds playing too often in some scenarios --- .../Screens/Play/HUD/DefaultRankDisplay.cs | 45 +++++++--- osu.Game/Skinning/LegacyRankDisplay.cs | 83 +++++++++++-------- 2 files changed, 82 insertions(+), 46 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index f184ad6a03..15901861c8 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -34,8 +34,14 @@ namespace osu.Game.Screens.Play.HUD private Bindable lastSamplePlaybackTime = null!; + private readonly Bindable lastRankChangeTime = new Bindable(); + private IBindable rank = null!; + private ScoreRank lastRank; + + private const int minimum_update_rate = 3000; + public DefaultRankDisplay() { Size = new Vector2(70, 35); @@ -67,21 +73,34 @@ namespace osu.Game.Screens.Play.HUD rank = scoreProcessor.Rank.GetBoundCopy(); rank.BindValueChanged(r => { - bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + bool enoughTimeElapsed = !lastRankChangeTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= minimum_update_rate; - // Don't play rank-down sfx on quit/retry - if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughTimeElapsed) - { - if (r.NewValue > rankDisplay.Rank) - rankUpSample.Play(); - else - rankDownSample.Play(); - - lastSamplePlaybackTime.Value = Time.Current; - } - - rankDisplay.Rank = r.NewValue; + Scheduler.CancelDelayedTasks(); + if (enoughTimeElapsed || r.NewValue == ScoreRank.F) + onRankChange(r); + else + Scheduler.AddDelayed(onRankChange, r, (double)lastRankChangeTime.Value! - Time.Current + minimum_update_rate); }, true); } + + private void onRankChange(ValueChangedEvent r) + { + bool enoughSampleTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + + // Don't play rank-down sfx on quit/retry and entering + if (r.NewValue != lastRank && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && lastRankChangeTime.Value.HasValue) + { + if (r.NewValue > lastRank) + rankUpSample.Play(); + else + rankDownSample.Play(); + + lastSamplePlaybackTime.Value = Time.Current; + } + + rankDisplay.Rank = r.NewValue; + lastRank = r.NewValue; + lastRankChangeTime.Value = Time.Current; + } } } diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index 7c2f8ffdef..216f7f3679 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -36,9 +36,14 @@ namespace osu.Game.Skinning private Bindable lastSamplePlaybackTime = null!; + private readonly Bindable lastRankChangeTime = new Bindable(); + private IBindable rank = null!; + private ScoreRank lastRank; + private const int minimum_update_rate = 3000; + public LegacyRankDisplay() { AutoSizeAxes = Axes.Both; @@ -70,43 +75,55 @@ namespace osu.Game.Skinning rank = scoreProcessor.Rank.GetBoundCopy(); rank.BindValueChanged(r => { - var texture = source.GetTexture($"ranking-{r.NewValue}-small"); + bool enoughTimeElapsed = !lastRankChangeTime.Value.HasValue || Time.Current - lastRankChangeTime.Value >= minimum_update_rate; - rankDisplay.Texture = texture; - - if (texture != null) - { - var transientRank = new Sprite - { - Texture = texture, - Blending = BlendingParameters.Additive, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BypassAutoSizeAxes = Axes.Both, - }; - AddInternal(transientRank); - transientRank.FadeOutFromOne(500, Easing.Out) - .ScaleTo(new Vector2(1.625f), 500, Easing.Out) - .Expire(); - } - - bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; - - // Don't play rank-down sfx on quit/retry - if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughTimeElapsed) - { - if (r.NewValue > lastRank) - rankUpSample.Play(); - else - rankDownSample.Play(); - - lastSamplePlaybackTime.Value = Time.Current; - } - - lastRank = r.NewValue; + Scheduler.CancelDelayedTasks(); + if (enoughTimeElapsed || r.NewValue == ScoreRank.F) + onRankChange(r); + else + Scheduler.AddDelayed(onRankChange, r, (double)lastRankChangeTime.Value! - Time.Current + minimum_update_rate); }, true); FinishTransforms(true); } + + private void onRankChange(ValueChangedEvent r) + { + var texture = source.GetTexture($"ranking-{r.NewValue}-small"); + + rankDisplay.Texture = texture; + + if (texture != null) + { + var transientRank = new Sprite + { + Texture = texture, + Blending = BlendingParameters.Additive, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BypassAutoSizeAxes = Axes.Both, + }; + AddInternal(transientRank); + transientRank.FadeOutFromOne(500, Easing.Out) + .ScaleTo(new Vector2(1.625f), 500, Easing.Out) + .Expire(); + } + + bool enoughSampleTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + + // Don't play rank-down sfx on quit/retry and entering + if (r.NewValue != lastRank && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && lastRankChangeTime.Value.HasValue) + { + if (r.NewValue > lastRank) + rankUpSample.Play(); + else + rankDownSample.Play(); + + lastSamplePlaybackTime.Value = Time.Current; + } + + lastRank = r.NewValue; + lastRankChangeTime.Value = Time.Current; + } } } From 45ef97c92cb78ca0b3969b2494942ad5d3fe0d2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Sep 2025 16:01:22 +0900 Subject: [PATCH 124/267] Simplify implementation --- .../Screens/Play/HUD/DefaultRankDisplay.cs | 55 +++++++++---------- osu.Game/Skinning/LegacyRankDisplay.cs | 54 +++++++++--------- 2 files changed, 52 insertions(+), 57 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index 15901861c8..dd8c324c9e 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -19,26 +19,23 @@ namespace osu.Game.Screens.Play.HUD { public partial class DefaultRankDisplay : CompositeDrawable, ISerialisableDrawable { + public bool UsesFixedAnchor { get; set; } + [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; [SettingSource(typeof(DefaultRankDisplayStrings), nameof(DefaultRankDisplayStrings.PlaySamplesOnRankChange))] public BindableBool PlaySamples { get; set; } = new BindableBool(true); - public bool UsesFixedAnchor { get; set; } - private UpdateableRank rankDisplay = null!; private SkinnableSound rankDownSample = null!; private SkinnableSound rankUpSample = null!; - private Bindable lastSamplePlaybackTime = null!; + private Bindable lastSamplePlayback = null!; + private double lastRankUpdate; - private readonly Bindable lastRankChangeTime = new Bindable(); - - private IBindable rank = null!; - - private ScoreRank lastRank; + private ScoreRank displayedRank; private const int minimum_update_rate = 3000; @@ -63,44 +60,44 @@ namespace osu.Game.Screens.Play.HUD if (skinEditor != null) PlaySamples.Value = false; - lastSamplePlaybackTime = statics.GetBindable(Static.LastRankChangeSamplePlaybackTime); + lastSamplePlayback = statics.GetBindable(Static.LastRankChangeSamplePlaybackTime); } - protected override void LoadComplete() + protected override void Update() { - base.LoadComplete(); + base.Update(); - rank = scoreProcessor.Rank.GetBoundCopy(); - rank.BindValueChanged(r => + var currentRank = scoreProcessor.Rank.Value; + + if (currentRank != displayedRank) { - bool enoughTimeElapsed = !lastRankChangeTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= minimum_update_rate; + bool enoughTimeElapsed = Time.Current - lastRankUpdate >= minimum_update_rate; - Scheduler.CancelDelayedTasks(); - if (enoughTimeElapsed || r.NewValue == ScoreRank.F) - onRankChange(r); - else - Scheduler.AddDelayed(onRankChange, r, (double)lastRankChangeTime.Value! - Time.Current + minimum_update_rate); - }, true); + if (enoughTimeElapsed || currentRank == ScoreRank.F) + updateRank(currentRank); + } } - private void onRankChange(ValueChangedEvent r) + private void updateRank(ScoreRank rank) { - bool enoughSampleTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + rankDisplay.Rank = rank; - // Don't play rank-down sfx on quit/retry and entering - if (r.NewValue != lastRank && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && lastRankChangeTime.Value.HasValue) + // Check sample time separately to ensure two copies of the rank display don't both play samples on a change. + bool enoughSampleTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + + // Also don't play rank-down sfx on quit/retry/initial update. + if (rank != displayedRank && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && lastRankUpdate > 0) { - if (r.NewValue > lastRank) + if (rank > displayedRank) rankUpSample.Play(); else rankDownSample.Play(); - lastSamplePlaybackTime.Value = Time.Current; + lastSamplePlayback.Value = Time.Current; } - rankDisplay.Rank = r.NewValue; - lastRank = r.NewValue; - lastRankChangeTime.Value = Time.Current; + displayedRank = rank; + lastRankUpdate = Time.Current; } } } diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index 216f7f3679..ee67d77487 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -34,13 +34,10 @@ namespace osu.Game.Skinning private SkinnableSound rankDownSample = null!; private SkinnableSound rankUpSample = null!; - private Bindable lastSamplePlaybackTime = null!; + private Bindable lastSamplePlayback = null!; + private double lastRankUpdate; - private readonly Bindable lastRankChangeTime = new Bindable(); - - private IBindable rank = null!; - - private ScoreRank lastRank; + private ScoreRank displayedRank; private const int minimum_update_rate = 3000; @@ -67,29 +64,27 @@ namespace osu.Game.Skinning if (skinEditor != null) PlaySamples.Value = false; - lastSamplePlaybackTime = statics.GetBindable(Static.LastRankChangeSamplePlaybackTime); + lastSamplePlayback = statics.GetBindable(Static.LastRankChangeSamplePlaybackTime); } - protected override void LoadComplete() + protected override void Update() { - rank = scoreProcessor.Rank.GetBoundCopy(); - rank.BindValueChanged(r => + base.Update(); + + var currentRank = scoreProcessor.Rank.Value; + + if (currentRank != displayedRank) { - bool enoughTimeElapsed = !lastRankChangeTime.Value.HasValue || Time.Current - lastRankChangeTime.Value >= minimum_update_rate; + bool enoughTimeElapsed = Time.Current - lastRankUpdate >= minimum_update_rate; - Scheduler.CancelDelayedTasks(); - if (enoughTimeElapsed || r.NewValue == ScoreRank.F) - onRankChange(r); - else - Scheduler.AddDelayed(onRankChange, r, (double)lastRankChangeTime.Value! - Time.Current + minimum_update_rate); - }, true); - - FinishTransforms(true); + if (enoughTimeElapsed || currentRank == ScoreRank.F) + updateRank(currentRank); + } } - private void onRankChange(ValueChangedEvent r) + private void updateRank(ScoreRank rank) { - var texture = source.GetTexture($"ranking-{r.NewValue}-small"); + var texture = source.GetTexture($"ranking-{rank}-small"); rankDisplay.Texture = texture; @@ -103,27 +98,30 @@ namespace osu.Game.Skinning Origin = Anchor.Centre, BypassAutoSizeAxes = Axes.Both, }; + AddInternal(transientRank); + transientRank.FadeOutFromOne(500, Easing.Out) .ScaleTo(new Vector2(1.625f), 500, Easing.Out) .Expire(); } - bool enoughSampleTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + // Check sample time separately to ensure two copies of the rank display don't both play samples on a change. + bool enoughSampleTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; - // Don't play rank-down sfx on quit/retry and entering - if (r.NewValue != lastRank && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && lastRankChangeTime.Value.HasValue) + // Also don't play rank-down sfx on quit/retry/initial update. + if (rank != displayedRank && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && lastRankUpdate > 0) { - if (r.NewValue > lastRank) + if (rank > displayedRank) rankUpSample.Play(); else rankDownSample.Play(); - lastSamplePlaybackTime.Value = Time.Current; + lastSamplePlayback.Value = Time.Current; } - lastRank = r.NewValue; - lastRankChangeTime.Value = Time.Current; + displayedRank = rank; + lastRankUpdate = Time.Current; } } } From 912c0a39cf787cb0c021fa6c9cc03feb1ac4e72f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Sep 2025 16:10:12 +0900 Subject: [PATCH 125/267] Change debounce to be delayed since last actual change to avoid state flickering --- .../Screens/Play/HUD/DefaultRankDisplay.cs | 21 ++++++++++--------- osu.Game/Skinning/LegacyRankDisplay.cs | 21 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index dd8c324c9e..59e7ce3b10 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -33,11 +33,11 @@ namespace osu.Game.Screens.Play.HUD private SkinnableSound rankUpSample = null!; private Bindable lastSamplePlayback = null!; - private double lastRankUpdate; + private double timeSinceChange; - private ScoreRank displayedRank; + private ScoreRank? displayedRank; - private const int minimum_update_rate = 3000; + private const int time_before_commit = 1500; public DefaultRankDisplay() { @@ -69,13 +69,14 @@ namespace osu.Game.Screens.Play.HUD var currentRank = scoreProcessor.Rank.Value; - if (currentRank != displayedRank) + if (currentRank == displayedRank) { - bool enoughTimeElapsed = Time.Current - lastRankUpdate >= minimum_update_rate; - - if (enoughTimeElapsed || currentRank == ScoreRank.F) - updateRank(currentRank); + timeSinceChange = 0; + return; } + + if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value) + updateRank(currentRank); } private void updateRank(ScoreRank rank) @@ -86,7 +87,7 @@ namespace osu.Game.Screens.Play.HUD bool enoughSampleTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; // Also don't play rank-down sfx on quit/retry/initial update. - if (rank != displayedRank && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && lastRankUpdate > 0) + if (rank != displayedRank && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && displayedRank != null) { if (rank > displayedRank) rankUpSample.Play(); @@ -97,7 +98,7 @@ namespace osu.Game.Screens.Play.HUD } displayedRank = rank; - lastRankUpdate = Time.Current; + timeSinceChange = 0; } } } diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index ee67d77487..da033d9756 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -35,11 +35,11 @@ namespace osu.Game.Skinning private SkinnableSound rankUpSample = null!; private Bindable lastSamplePlayback = null!; - private double lastRankUpdate; + private double timeSinceChange; - private ScoreRank displayedRank; + private ScoreRank? displayedRank; - private const int minimum_update_rate = 3000; + private const int time_before_commit = 1500; public LegacyRankDisplay() { @@ -73,13 +73,14 @@ namespace osu.Game.Skinning var currentRank = scoreProcessor.Rank.Value; - if (currentRank != displayedRank) + if (currentRank == displayedRank) { - bool enoughTimeElapsed = Time.Current - lastRankUpdate >= minimum_update_rate; - - if (enoughTimeElapsed || currentRank == ScoreRank.F) - updateRank(currentRank); + timeSinceChange = 0; + return; } + + if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value) + updateRank(currentRank); } private void updateRank(ScoreRank rank) @@ -110,7 +111,7 @@ namespace osu.Game.Skinning bool enoughSampleTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; // Also don't play rank-down sfx on quit/retry/initial update. - if (rank != displayedRank && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && lastRankUpdate > 0) + if (rank != displayedRank && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && displayedRank != null) { if (rank > displayedRank) rankUpSample.Play(); @@ -121,7 +122,7 @@ namespace osu.Game.Skinning } displayedRank = rank; - lastRankUpdate = Time.Current; + timeSinceChange = 0; } } } From 52c10a42bef1364a65679fe779fc7da782b25332 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Sep 2025 16:10:27 +0900 Subject: [PATCH 126/267] Change `DefaultRankDisplay` to start with no value --- osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index 59e7ce3b10..ec31a03f90 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Play.HUD { rankDownSample = new SkinnableSound(new SampleInfo("Gameplay/rank-down")), rankUpSample = new SkinnableSound(new SampleInfo("Gameplay/rank-up")), - rankDisplay = new UpdateableRank(ScoreRank.X) + rankDisplay = new UpdateableRank { RelativeSizeAxes = Axes.Both }, From ab7985f31ea0a104b5db666e95a5f9e85195baa2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Sep 2025 16:19:55 +0900 Subject: [PATCH 127/267] Improve initial state handling --- osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs | 11 +++++++++-- osu.Game/Skinning/LegacyRankDisplay.cs | 13 ++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index ec31a03f90..5ea0e75956 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -63,6 +63,13 @@ namespace osu.Game.Screens.Play.HUD lastSamplePlayback = statics.GetBindable(Static.LastRankChangeSamplePlaybackTime); } + protected override void LoadComplete() + { + base.LoadComplete(); + + updateRank(scoreProcessor.Rank.Value); + } + protected override void Update() { base.Update(); @@ -75,7 +82,7 @@ namespace osu.Game.Screens.Play.HUD return; } - if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value) + if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value || currentRank == ScoreRank.F) updateRank(currentRank); } @@ -87,7 +94,7 @@ namespace osu.Game.Screens.Play.HUD bool enoughSampleTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; // Also don't play rank-down sfx on quit/retry/initial update. - if (rank != displayedRank && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && displayedRank != null) + if (displayedRank != null && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed) { if (rank > displayedRank) rankUpSample.Play(); diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index da033d9756..3109f68e9f 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -67,6 +67,13 @@ namespace osu.Game.Skinning lastSamplePlayback = statics.GetBindable(Static.LastRankChangeSamplePlaybackTime); } + protected override void LoadComplete() + { + base.LoadComplete(); + + updateRank(scoreProcessor.Rank.Value); + } + protected override void Update() { base.Update(); @@ -79,7 +86,7 @@ namespace osu.Game.Skinning return; } - if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value) + if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value || currentRank == ScoreRank.F) updateRank(currentRank); } @@ -89,7 +96,7 @@ namespace osu.Game.Skinning rankDisplay.Texture = texture; - if (texture != null) + if (texture != null && displayedRank != null) { var transientRank = new Sprite { @@ -111,7 +118,7 @@ namespace osu.Game.Skinning bool enoughSampleTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; // Also don't play rank-down sfx on quit/retry/initial update. - if (rank != displayedRank && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed && displayedRank != null) + if (displayedRank != null && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed) { if (rank > displayedRank) rankUpSample.Play(); From ea79422b60065abf2a38f419becd8def9590ff84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Sep 2025 19:16:47 +0900 Subject: [PATCH 128/267] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9de156dc2a..cd6b572a2f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 058835440dd7357dec41e5527f9f540bfdc325bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Sep 2025 21:20:12 +0900 Subject: [PATCH 129/267] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index cd6b572a2f..32e1b41ad8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 6af48975b04967031b297ec4932206d7ca66fca0 Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 5 Sep 2025 01:20:29 -0700 Subject: [PATCH 130/267] Add "retro" default skin --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 1 + .../Overlays/Settings/Sections/SkinSection.cs | 1 + .../Backgrounds/BackgroundScreenDefault.cs | 1 + osu.Game/Skinning/RetroSkin.cs | 58 +++++++++++++++++++ osu.Game/Skinning/SkinInfo.cs | 1 + osu.Game/Skinning/SkinManager.cs | 6 ++ osu.Game/Skinning/SkinnableSprite.cs | 1 + 7 files changed, 69 insertions(+) create mode 100644 osu.Game/Skinning/RetroSkin.cs diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index c7bf1f3538..cc64ee0d69 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -79,6 +79,7 @@ namespace osu.Game.Rulesets.Mania return new ManiaArgonSkinTransformer(skin, beatmap); case DefaultLegacySkin: + case RetroSkin: return new ManiaClassicSkinTransformer(skin, beatmap); case LegacySkin: diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 764f5fdfb6..2c24a5b277 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -130,6 +130,7 @@ namespace osu.Game.Overlays.Settings.Sections dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.ARGON_PRO_SKIN).ToLive(realm)); dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.TRIANGLES_SKIN).ToLive(realm)); dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.CLASSIC_SKIN).ToLive(realm)); + dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.RETRO_SKIN).ToLive(realm)); dropdownItems.Add(random_skin_info); diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 7be96718bd..82bfd23801 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -163,6 +163,7 @@ namespace osu.Game.Screens.Backgrounds case TrianglesSkin: case ArgonSkin: case DefaultLegacySkin: + case RetroSkin: // default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them. break; diff --git a/osu.Game/Skinning/RetroSkin.cs b/osu.Game/Skinning/RetroSkin.cs new file mode 100644 index 0000000000..abeab9ab17 --- /dev/null +++ b/osu.Game/Skinning/RetroSkin.cs @@ -0,0 +1,58 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Game.Extensions; +using osu.Game.IO; + +namespace osu.Game.Skinning +{ + /// + /// A skin that looks like osu!stable as it was around 2008. + /// + /// + /// "Around 2008" was chosen as the cutoff for this skin because that's when the look of core gameplay settled into its final design (until ). Skin elements from later versions of osu! were preferred as long as they only fixed bugs or applied minor tweaks to 2008 elements. + /// + public class RetroSkin : LegacySkin + { + public static SkinInfo CreateInfo() => new SkinInfo + { + ID = Skinning.SkinInfo.RETRO_SKIN, + Name = "osu! \"retro\" (2008)", + Creator = "team osu!", + Protected = true, + InstantiationInfo = typeof(RetroSkin).GetInvariantInstantiationInfo(), + }; + + public RetroSkin(IStorageResourceProvider resources) + : this(CreateInfo(), resources) + { + } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] + public RetroSkin(SkinInfo skin, IStorageResourceProvider resources) + : base( + skin, + resources, + new NamespacedResourceStore(resources.Resources, "Skins/Retro") + ) + { + } + + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + // Retro taiko hit explosions use osu textures + if (componentName.StartsWith("taiko-hit", StringComparison.Ordinal)) + componentName = componentName.Substring(6); + + // Retro taiko slider has no fail variant, but it needs to exist to avoid displaying nothing + if (componentName == "taiko-slider-fail") + componentName = "taiko-slider"; + + return base.GetTexture(componentName, wrapModeS, wrapModeT); + } + } +} diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 9763d3b57e..4c9c16e721 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -20,6 +20,7 @@ namespace osu.Game.Skinning internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0"); internal static readonly Guid ARGON_PRO_SKIN = new Guid("9FC9CF5D-0F16-4C71-8256-98868321AC43"); internal static readonly Guid CLASSIC_SKIN = new Guid("81F02CD3-EEC6-4865-AC23-FAE26A386187"); + internal static readonly Guid RETRO_SKIN = new Guid("0555C76A-CC6B-4BB4-9548-DF76BA72EF25"); internal static readonly Guid RANDOM_SKIN = new Guid("D39DFEFB-477C-4372-B1EA-2BCEA5FB8908"); [PrimaryKey] diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 1be6f1bc4a..e92d0d3d49 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -64,6 +64,8 @@ namespace osu.Game.Skinning private Skin trianglesSkin { get; } + private Skin retroSkin { get; } + public override bool PauseImports { get => base.PauseImports; @@ -91,6 +93,7 @@ namespace osu.Game.Skinning var defaultSkins = new[] { + retroSkin = new RetroSkin(this), DefaultClassicSkin = new DefaultLegacySkin(this), trianglesSkin = new TrianglesSkin(this), argonSkin = new ArgonSkin(this), @@ -369,6 +372,9 @@ namespace osu.Game.Skinning { if (guid == SkinInfo.CLASSIC_SKIN) skinInfo = DefaultClassicSkin.SkinInfo; + + if (guid == SkinInfo.RETRO_SKIN) + skinInfo = retroSkin.SkinInfo; } CurrentSkinInfo.Value = skinInfo ?? trianglesSkin.SkinInfo; diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 49ce7e48ab..25bc32eaf2 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -122,6 +122,7 @@ namespace osu.Game.Skinning || skin.GetType() == typeof(ArgonProSkin) || skin.GetType() == typeof(ArgonSkin) || skin.GetType() == typeof(DefaultLegacySkin) + || skin.GetType() == typeof(RetroSkin) || skin.GetType() == typeof(LegacySkin); } } From 3c1c537b1b42030ea449cce4a4b98a3b878a2f0a Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 5 Sep 2025 01:22:01 -0700 Subject: [PATCH 131/267] Replace old-skin with retro skin in tests --- .../Resources/old-skin/fruit-apple-overlay.png | Bin 5035 -> 0 bytes .../Resources/old-skin/fruit-apple.png | Bin 5083 -> 0 bytes .../old-skin/fruit-bananas-overlay.png | Bin 7823 -> 0 bytes .../Resources/old-skin/fruit-bananas.png | Bin 14274 -> 0 bytes .../Resources/old-skin/fruit-drop.png | Bin 4203 -> 0 bytes .../old-skin/fruit-grapes-overlay.png | Bin 11873 -> 0 bytes .../Resources/old-skin/fruit-grapes.png | Bin 3985 -> 0 bytes .../old-skin/fruit-orange-overlay.png | Bin 4096 -> 0 bytes .../Resources/old-skin/fruit-orange.png | Bin 9021 -> 0 bytes .../Resources/old-skin/fruit-pear-overlay.png | Bin 7194 -> 0 bytes .../Resources/old-skin/fruit-pear.png | Bin 4873 -> 0 bytes .../Resources/old-skin/fruit-plate.png | Bin 3583 -> 0 bytes .../Resources/old-skin/fruit-ryuuta.png | Bin 11722 -> 0 bytes .../Resources/old-skin/hit0.png | Bin 9518 -> 0 bytes .../Resources/old-skin/hit100.png | Bin 27548 -> 0 bytes .../Resources/old-skin/hit300.png | Bin 30325 -> 0 bytes .../Resources/old-skin/hit50.png | Bin 22638 -> 0 bytes .../Resources/old-skin/score-0.png | Bin 3092 -> 0 bytes .../Resources/old-skin/score-1.png | Bin 1237 -> 0 bytes .../Resources/old-skin/score-2.png | Bin 3134 -> 0 bytes .../Resources/old-skin/score-3.png | Bin 3712 -> 0 bytes .../Resources/old-skin/score-4.png | Bin 2395 -> 0 bytes .../Resources/old-skin/score-5.png | Bin 3067 -> 0 bytes .../Resources/old-skin/score-6.png | Bin 3337 -> 0 bytes .../Resources/old-skin/score-7.png | Bin 1910 -> 0 bytes .../Resources/old-skin/score-8.png | Bin 3652 -> 0 bytes .../Resources/old-skin/score-9.png | Bin 3561 -> 0 bytes .../Resources/old-skin/skin.ini | 2 -- .../Resources/old-skin/approachcircle.png | Bin 4540 -> 0 bytes .../Resources/old-skin/cursor-smoke.png | Bin 2249 -> 0 bytes .../Resources/old-skin/cursor.png | Bin 10496 -> 0 bytes .../Resources/old-skin/cursortrail.png | Bin 3763 -> 0 bytes .../Resources/old-skin/default-0.png | Bin 2003 -> 0 bytes .../Resources/old-skin/default-1.png | Bin 1191 -> 0 bytes .../Resources/old-skin/default-2.png | Bin 1756 -> 0 bytes .../Resources/old-skin/default-3.png | Bin 1822 -> 0 bytes .../Resources/old-skin/default-4.png | Bin 1814 -> 0 bytes .../Resources/old-skin/default-5.png | Bin 1848 -> 0 bytes .../Resources/old-skin/default-6.png | Bin 2014 -> 0 bytes .../Resources/old-skin/default-7.png | Bin 1452 -> 0 bytes .../Resources/old-skin/default-8.png | Bin 1953 -> 0 bytes .../Resources/old-skin/default-9.png | Bin 1814 -> 0 bytes .../Resources/old-skin/hit0.png | Bin 12904 -> 0 bytes .../Resources/old-skin/hit100.png | Bin 30853 -> 0 bytes .../Resources/old-skin/hit300.png | Bin 33649 -> 0 bytes .../Resources/old-skin/hit50.png | Bin 27832 -> 0 bytes .../Resources/old-skin/hitcircle.png | Bin 3572 -> 0 bytes .../Resources/old-skin/hitcircleoverlay.png | Bin 7113 -> 0 bytes .../Resources/old-skin/reversearrow.png | Bin 4853 -> 0 bytes .../Resources/old-skin/score-0.png | Bin 3092 -> 0 bytes .../Resources/old-skin/score-1.png | Bin 1237 -> 0 bytes .../Resources/old-skin/score-2.png | Bin 3134 -> 0 bytes .../Resources/old-skin/score-3.png | Bin 3712 -> 0 bytes .../Resources/old-skin/score-4.png | Bin 2395 -> 0 bytes .../Resources/old-skin/score-5.png | Bin 3067 -> 0 bytes .../Resources/old-skin/score-6.png | Bin 3337 -> 0 bytes .../Resources/old-skin/score-7.png | Bin 1910 -> 0 bytes .../Resources/old-skin/score-8.png | Bin 3652 -> 0 bytes .../Resources/old-skin/score-9.png | Bin 3561 -> 0 bytes .../Resources/old-skin/score-comma.png | Bin 865 -> 0 bytes .../Resources/old-skin/score-dot.png | Bin 771 -> 0 bytes .../Resources/old-skin/score-percent.png | Bin 4904 -> 0 bytes .../Resources/old-skin/score-x.png | Bin 2536 -> 0 bytes .../Resources/old-skin/skin.ini | 6 ------ .../Resources/old-skin/sliderpoint10.png | Bin 2349 -> 0 bytes .../Resources/old-skin/sliderpoint30.png | Bin 2718 -> 0 bytes .../old-skin/spinner-approachcircle.png | Bin 26350 -> 0 bytes .../Resources/old-skin/spinner-background.png | Bin 46103 -> 0 bytes .../Resources/old-skin/spinner-circle.png | Bin 166439 -> 0 bytes .../Resources/old-skin/spinner-clear.png | Bin 39074 -> 0 bytes .../Resources/old-skin/spinner-metre.png | Bin 14518 -> 0 bytes .../Resources/old-skin/spinner-osu.png | Bin 18585 -> 0 bytes .../Resources/old-skin/spinner-rpm.png | Bin 10583 -> 0 bytes .../Resources/old-skin/spinner-spin.png | Bin 21353 -> 0 bytes .../Resources/old-skin/spinnerbonus.wav | Bin 309536 -> 0 bytes .../Resources/old-skin/spinnerspin.wav | Bin 36868 -> 0 bytes .../Resources/old-skin/approachcircle.png | Bin 10333 -> 0 bytes .../Resources/old-skin/skin.ini | 5 ----- .../Resources/old-skin/taiko-bar-left.png | Bin 17758 -> 0 bytes .../Resources/old-skin/taiko-drum-inner.png | Bin 4661 -> 0 bytes .../Resources/old-skin/taiko-drum-outer.png | Bin 5585 -> 0 bytes .../old-skin/taiko-slider-fail@2x.png | Bin 102010 -> 0 bytes .../Resources/old-skin/taiko-slider@2x.png | Bin 96449 -> 0 bytes .../Resources/old-skin/taikobigcircle.png | Bin 3079 -> 0 bytes .../old-skin/taikobigcircleoverlay-0.png | Bin 17018 -> 0 bytes .../old-skin/taikobigcircleoverlay-1.png | Bin 18837 -> 0 bytes .../Resources/old-skin/taikohitcircle.png | Bin 6028 -> 0 bytes .../old-skin/taikohitcircleoverlay-0.png | Bin 20284 -> 0 bytes .../old-skin/taikohitcircleoverlay-1.png | Bin 20333 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-0.png | Bin 3092 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-1.png | Bin 1237 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-2.png | Bin 3134 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-3.png | Bin 3712 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-4.png | Bin 2395 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-5.png | Bin 3067 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-6.png | Bin 3337 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-7.png | Bin 1910 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-8.png | Bin 3652 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-9.png | Bin 3561 -> 0 bytes .../Resources/old-skin/score-comma.png | Bin 865 -> 0 bytes .../Resources/old-skin/score-dot.png | Bin 771 -> 0 bytes .../Resources/old-skin/score-percent.png | Bin 4904 -> 0 bytes osu.Game.Tests/Resources/old-skin/score-x.png | Bin 2536 -> 0 bytes .../Resources/old-skin/scorebar-bg.png | Bin 7087 -> 0 bytes .../Resources/old-skin/scorebar-colour-0.png | Bin 465 -> 0 bytes .../Resources/old-skin/scorebar-colour-1.png | Bin 475 -> 0 bytes .../Resources/old-skin/scorebar-colour-2.png | Bin 466 -> 0 bytes .../Resources/old-skin/scorebar-colour-3.png | Bin 464 -> 0 bytes .../Resources/old-skin/scorebar-ki.png | Bin 8579 -> 0 bytes .../Resources/old-skin/scorebar-kidanger.png | Bin 7361 -> 0 bytes .../Resources/old-skin/scorebar-kidanger2.png | Bin 9360 -> 0 bytes osu.Game.Tests/Resources/old-skin/skin.ini | 2 -- osu.Game/Tests/Visual/SkinnableTestScene.cs | 6 +++--- 113 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple-overlay.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-drop.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes-overlay.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange-overlay.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear-overlay.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-plate.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-ryuuta.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit0.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit100.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit300.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit50.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png delete mode 100755 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png delete mode 100755 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursortrail.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-0.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-1.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-3.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-6.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-8.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-9.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit0.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit100.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit300.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit50.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircleoverlay.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/reversearrow.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint10.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint30.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav delete mode 100755 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/approachcircle.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-0.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-1.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircle.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-1.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-0.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-1.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-2.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-3.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-4.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-5.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-6.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-7.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-8.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-9.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-comma.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-dot.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-percent.png delete mode 100644 osu.Game.Tests/Resources/old-skin/score-x.png delete mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-bg.png delete mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png delete mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png delete mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png delete mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png delete mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-ki.png delete mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png delete mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png delete mode 100644 osu.Game.Tests/Resources/old-skin/skin.ini diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple-overlay.png deleted file mode 100644 index 8d9608cfc9ba4157de11450e29d1124bdc93255c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5035 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png deleted file mode 100644 index 3a6612378e5cc655ec5d9d8ea7c53486c5a6270b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7823 zcmY+J^;gpmz;HhsjE*7QDGa2Pt}!}9>24%MYJ@awAczvuf`Bw4T~ac-K}7i?ofAYF z1_&dc^ZxKY=R7~$bMOCf-x@sCq9kJ>0|0na)KOP430~NgF@=6ca(hota3O6s$ zXCp52eqVIyA6tCorO{WRKnD*1Z?yie6tOhP!2`S;j~)23#)$WKcdh-$MOrK*&z z-@SWxU~U&u)@&ghm~*!y-Dkg;SehiOsmS}dSo1)b9;mizuJk(F`E@8CxHq?m!-xA6 z86d+~CsnrfUJo+~Xg+0@b8`MMl9Ecp+n-{{JIDl(`v-h!ZC#at!*6NKoa8^kbF}%M zw%01S_N;+PDBhR_6W%U>k$K{Nb0g0*FF_euNqJI_=m{Jg6BARFG`H>d_x6^$T_>P`n83le|V?FAC_gk;}Nk7OmW+5SC1Jp{O7=O)ZT*@gd^@iInL zH(*~IRZOS&UMWk>z#E?pzk9pf5yaTyy}aPv7*%QrPw%EB;)*MDCI#iOSI3-ngB;fg zG;dkCd#r`M{kXffL}6~}sUJPpHCMbhOnJ+b!V5B;u~+qucY{xIrR9cp(z9&*bY@`i zj*yc-)A95VK1qC_xVtsh#1pSCXdFK%JW5Ey437NLjC9^WCFUhGp~)p0-}KhSWgfns z`i@TRPCT4sDNAN`Q5ae<)?BY)mGSnp?+EmYI|lFb6JCgjh|u!0VH~D<4{L1N8%YL( z0E_DDGlSdQBpdGy2=6%ye%=XrMNSDY!)}u_BU#Dh;=UyQ79ZYkT3%iz$g+4hpR zwEMdaS%l;o*8U7}%kn+>sQE~98*bEgGqh7+7=H-(INa7hwS=Q?N>lG3Z48+>Mlc}n z7<=AB$ti=YPG!W0{T?E&ZNtV_RMmbRlZp90-{|sq#sZZvdcK%G>Rdjy^?~b85~k04 z3D#{d*h!9n?ygeQdr9C7d6QW_SbVFP3TGu9a5NhbZCiP!d7Eil&mACU8?u>MBC~|- zKYclCn}s=@di9EgNwK%6Gr_M zLczgYg44snWXCh*lqpja-2`+nb~Y9Qjm65fil7EaaO!76{=up3upLc9eG@A zcx`;q`5n61{r>X)J{*dNRM^XjUxB(p?V!{L#M5v-3gP~H4`qtYz@?Q zO{H-mf80ztrXLv8%3 zGHb3jc+NL+lw5dg?3`Zx>-|1lB-ygUt9dK{N@Gaa4}wjm?rgg&;d-gupT z^MhY+LE%oow}$a8n_K3;^77A4Pcf&*$*85}Rv-JtuP=8)7Zw)Y{|;KDS#@c1 z_*Z-piG1C!i6H|fXVVS5y1nO=pnE>j!Ii4}dNZr5GqO`8`l#&Fb2!M~Ft3U2n?!W{9%*VSX2 zaXot)_-6)+VX=b4z5`8sTrF3VR__maI$3+V8_vSGK3HOIB@qoXJoyiS9k#u(x`((B zp_(%PZ>W~4s@>^+YUQPl$eU2#tL1t-Up#>>o|g-iUjH4Op|6W?xtg>X(=Uc2r_Z~& z!W~)OuaY89$=jR&Nw*_HJDoCy%JQP?0%Yy&0Ihz23cjlFnvOqGRuyk-&_&*Q2&386 zw{B1_0mP$s>{RAD8*bf8!)o5#X_y-9lodie4byuE*uV+nm8cl(H;i`I^Q{)I z#TK&ky39#3U$Kt8#!Ov2Gt68lWNQud8k#{ME6YPeKvx=-J=-Aw+H^bOz*Y9aVw&6poxu}Cfd1uu&4V~B zO0P^Fg|fF_#d%6U(8y<_d-TaEQfe$n6>tbcf8mKt2kd(-y2DqB$L^{3I-R8po!Y!} zGNC}T5Luts@qWTpa>j|U2a7Ov z@AfQj(P<%%t~da35=0lo`Nvz3!LoM$ixrm*osA`mm$pw{=U*mJg$l8-QeF5n(zlqBvvYY_PC zjjYC`>Mnm_JGYNvod^mF8V`*mhn)ReC)qS_4EMVEipy#gw$Da!Rxg+q!i@0wDt(6| zigZ1X`6!n$bL6TM0H7~={_~h@6~sH4uzMa+U|iz@Xsfo*Iht;anipNea9Lyxw}ctW ziPQRUfesW1q1yQ;DPm^{B(Zhyu=($Apk}u5efxY>K zE4hT@p8W%5(u9Nb%P5}EuCj2gRMlgl{7TlNmWL~RI9F$ihX4;z&`}qo?;Vu?U5&Du z3UvViBGIruFE<&6n|8W!vF;`w3^6&MOnIGMBoER8@T`CjIUrwL`Zb z#0u0Ha1(2e$|2aWX8-XhS~jOr^OgIu#g(0^{q&q~9Q%Wqr=_djp2c34jMxtub;0U6 z$0WF}qf10MYU~{yQoNkyB}V#leYJlp#dcH#02-o1h>-bn+w4M#H;0fx$|*X}g>3YO z2lD1=?3le!DQ*iJp=XY~>$*ps)&TG;I>bbaIN6wd1(iw@0sIjUmawE2@5 z4t1o`%RXiXsIVxO17G042uLwAl>(vdkQIYxyQ)uw2nk7HJ9HR5i@jMwaUo1h$o)?F zU@DG3x-p&iDbd0>Crc^TL3MmQ=g8Qiq(Q`>*(&af$9y6#3Xd0x|}pM5nO zpH*-|Po^OX+24sc0?!&(G>uwT0&5g&JT5oJV^d$G=t0q;)%FjBb@5Y{k{m`mXRaZi zh*XNeG^fIVIP;Q9Sy$XQBM7?Co<)}Y1uj6&6j+-{z1%((qX7Y&V+B;B`&HbiE+OW) zRDC1$K)U(w;}=WaBx)9ZfOZknquT1#e1z}EL85A93PQ(6MRc3yA1>#~RU);YbCHj* zxqdf7@(8we`V?#YkhXgO%JB&Omm^6r^#snN)f&3E@|GE zz49rIMh{KM0pVVXUg(KlR5zj~wV}arvnPC%8N2;RLQP8~8&~|e-K00Ixm-8anI-d9 zI{7Q$A6393vXSU;uF4zJ{8cy*)hL|v>F-Br?^iSSlt?d!izYg>=R2&WpfeN6wPjn6 zAem?m)^ut$&Mi2eVMK$rITWy?3&|hTUxyK(nKdeF@FfdSgxRkKFINQkyH1ptrr!~oXp&1~)bN-5a#NB2z#i-JjTamNG>f9%*a6R9!-J694+@P(cA!;?}j~WJppa>-;wP_EW|Ux%bB!|<#5fC%qbIC@(0j;2Obb z*AzWUp!1_%fm~kYIGghYA99hTtE2=ZIcndhHxOjej^=9iL( zGrd#-vSU1cFDjcEAi;AnekF}#e}2_dV(yXb?bxbXns;*5ccLZRHei2gp<)Jw3uB8@ z2%b8zsx02m)YSwe`7v5&4EKQe&yh)eWoKyUI~X>r@bY()y#^6Y$sP<@r0q z?cg@kBXAuL1z(N$+Dr=B&Wv{*m>RiOc|r-UxyP)BIF=Lg@vYlDnG-Db%wpBj9dSOU z68hMD+G=8D%%o6&DYT>Jf^dt5SF3C{Ems!a-=%<9;q78%C7V)DnztoqS1_=PAe&Zz6u zNvk~kQzLzrF~J}BOj~|{{LSq14cCX;g0fF~lU$aNV@gCzCO9B7kTZgY`&o2Oc!_%n zz@)}8ezhTOh<7gqn?qj{6S49&=dELAv%3PnsLX6fx5FwxNm#pEndx-Z6WE<-D zL>W&WHit72`ylWNLL#t_r4ITV5ZuM*b_9S~43=we+Ka!StA3i?P%_|uW=5UmeI)1U}iWwm^Qss}b zM*jejd0$TZDtKB4HnYk_JkZcH3fX$ftNtU>+b{~@pQNV^L*}kQPW>SienB|Pm<5?$ zUgX(Pm1b8|`}-374LJmeW?U<2_ZiNWf84uH<4u@88%9DV|GYWYXZI3~c+_3?$2;KoN$d{T#9_UvSzK4WCK$}Z4PDQ#aI zMD%yzivOcqdOqYI0UgMt^kx-$D`4{SxXmS|sN}=4VB_mKi2#sSB4)L)&^j=BZ1%qrh9FO9%rdlgmig2wg1gkxLWnJLE}GUgpLTN zqP61jrXwoFtz~Uad#y715n4n4DNlgTl>50DBng^M*J7oZ`K`(bd0llZ3=(sngIc%dG&5sM@nR}*bvFw(+#t|?3C7mMdndAMk9B- z5jX$VsW>JS<>5L%x9+5!UJj;wG2qY3=sR=Ca2#kg8(bvfn#|G=n$K+m`*(3PGez3# z8@P84ee}5Z{6Sh1TFkJ58JENW6vG2lA`Uf72>1KhZOD|>zlsw(>y34#OP-jdG6;2# z!F|elYF#SMV76jUT6tEQu=piSyEseA&8E#d4YPl8M}&9bU<4c%M1>j~>8HzMI(Qxa zT;ZUDJHH1$-Ag%2=oxL{>Bn?b(+u8^ZmPVT{{)EB8xp?-qiZg{R4Qy_V8aZ8(=+F! z00cEw$(-WuRpnoJ{Nv7yPnn-Z*g{ZceFsfOI97T+ysE*95w2;l&{q%}ELnNdR zt|JjN&!;w$RI*g#F=!wI21>ETPYAvAr>eoJwG?9u>SA;mB{+^(iW7?R=2`vwRmjG% zoki~AYZoAwP5s!5_fECU?8d*@j9`le;8>ZKL9Znn;Mc#A=FG3$Z|?;9la7EKyj#ZE zya+aQiWLjN459xR8&>m230iCrL81vWgIHoiz*?5!X7t&7Gp(@0sd&l5D}#&Q$P=}* zjHpRsK%Iu*h)_|myTr!+5l`#W&!Z14Fg+^^HLU7;|7r}$s4_&QcPO2UNV!txa?F0U z&|!}yghwJ|_?54&J3$&wBp16e{P}8}vc^rY?Q==dE&AkTWoB~xFJ)jp$Z~tp zQg5nlFuc6xMFDLeLA(&VUa!=c)R~&LnwBX)R+@|ujqtf-8kQ)%o_Kgcu6yh`s4#um z>sR{X(o8Dyux{;!!&oCf`jYJ|uM6(+Jp`@=t{}6Q8J|cF?_;Qo;4E9i3ChI<$B*So zN|6|uhqnQ0ZZqmy%0`_i0JEwPK1uV?H3`sT#_R%P?Fh%pT(eSc>TAOzGI zq&MVqG9X00uSlzOw?OKmWV|95t?-nltfM7(Fi}LZZ0=gx7^h=Y1x1~wWk;zurcTtiWO!ML4EQ0m6QJWJ*hJ< zw?$PyoWks^4?t`!=)$*FzXbUdd-*0j9oAWWeDyNw^#6>ZF^vIfZ8u|n~bzQ8Y>Q^PNwgsw&9 z+Fz}NM~JvYoc>fxJJ&Zt7GO<-Z@FU%vEiVeaJg3gsJRIxpCAXybq$Meq|T(Tgd%sR z8tbO`U1!2Ocf}0c1y2dybnimbQH&^mFtk|kY?*kD7v=%4Aap-eo>TKr?o6Q&VS75eCi1wdh`2s5O_r3IR_i`VcqwtqBRcS zDK&}&7ue~yC_+ZzKJK#nHI#}hLZQ+n0K}mg`_n5FZcD2Fv#RH331Z46BJ;i#?StHO8O-? zu0KGg7L{3l{@N^b@oH^mQiX_43)bT#78UF7mG5)2!in`RJ6#|UtEaRaH3K}*(IvL8 zP259N+h1N?=GC+6HwEkOLPKR0k3S_}t-ZxC0ZrYL3>NqG*L;jBww_$BUkXEI>z$v9 zh!HVghtlsU5I-^lba zXqXS;gtt4VMi`Jm#b=s^vL>-Lga`@Spiv9~fk#K#`L`QP>W|MT0g3lxY5_yAqC UW+^IE1ONcg(Riv}tLhN_KhiDVy#N3J diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png deleted file mode 100644 index afb8698b2d8ff7285d53e140bcf0c5379ef8a6c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14274 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-drop.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-drop.png deleted file mode 100644 index 12c74f46e227957667386ef1fe1017c515778a28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4203 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 B0*L?s diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes-overlay.png deleted file mode 100644 index bb37ba1920dc33ee2330f0ce865d5b4c072a6ab9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11873 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange.png deleted file mode 100644 index 42cc80399f2b7d0a62ed8e82faf47dd0c90cf0bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9021 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 VU^hAElmP$$40TO)Dzq^%{|Cx@Y0Lls diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear.png deleted file mode 100644 index 9fe400bdd1660c26afb5f3426ef398c9bc5f42e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4873 zcmV+k6ZY(hP)07yEP9d)mhUhvJmJ^S2i)qqTrIw7Lfh{*&alt94{D)hY{G~te z5B}2y*9~O(y$PUY#XX+$9mUg+del7*M)Ex!DMVJOj03_=S6p(+fBPHfw)~CX@HhU| zIX4XD_Zont>wt%S!uQ;@?^%z!!#?vmx+oM#k$x5sOpFW+Ty@_6`*(lsf5-mZFZ*-< z=8{_`I|ZS%9no{l)4rql;SbG!&a>`y&}tO^wmBVb9WAYx+e$HXbuC!4-ytXLDQ+FV z{e~NEH9rKp$b;x1u&c=ho zhXTQtp-O)D14z1#c-Hst`^ZZy;z11PW!0mq2vQS+?qsgZgWip0X5W zVqh%4+W~a!^_cJ8`_(Jazy0sDZvMmC3sq zKxxH8zTW=MFMH8?tovRZ`$#-q>$DqlQc4W~@m?#I!%A!)H;&x6&nOU#Ox~>kk~w$# zgdhKO=fm!`X3m^W7{9EOkCbbrP@yS}Ut7ACEL*d=dg-Lnb$tU9`P~N~X*=jSKl;?s zPk7iK^XBSx9|cbw-IuA*d{@+ds9&|rTe4=*J^P5UzFW4YR}fq80KlSkA6tF+Irmx9 z3){Er6`vF!5p#~A{;rB|3YEg7PG0SWuCA5VZ-2KnN#T@R#`3!fKxw~6ec?q9I$&N$ z9CbyELLvr`W26|66>^9aqp06y2!t);^hckcjDpiyo6|thdI!h!7eEW!y2 zkZLCc5Mtg;fAl$5jZ`k!38m9k;Bl^|D5nV}7G()`Lo+`Oj(Mc)-}^{NI0z8~TPe zY$5Ni13LD*uXA+WK|L*Htv8>(W_PGPK&!?Bl_ix2jZ_Q&_;okjG%_*VZ3lE6bn*^I ztkm0`x_o&{Q6or-#y=^GZ_N=19@2N|@cFN}7%r%c<=xu|l~z6GTb@~d)DiPKb^kAv zNN*8Bsz+*m+n8J8Z}8vyM_h14-+1>NP{`JtNXHJ^qg(Ihie>_D+XYF1T)TjnEji{< z8-C-H&be&M*Z_Gq0AyYJ+65Cp+(ed5g;ybnCKLC~Q|Gn!AF!4#bQV$Ms$Hb05m*T{m*x0D0$XK&j_I&zzEm zQ6bruc#W2rRiR}kJms7}{elfclX`Q!Lk?)^=*0G?K6lIVwx=I0F?fri;e$QaebiR( zZ@)t__V+Fu?i7GROV3KAA0pfCXl{?AB_h|VWcxf|q`&%a-{OXWf2v#Jj%t01x@jWs1c0`#9tCg0`0uy!E$gkL_o0QxToFpn_sqN%h&}2F zZA0_+xz}|YcV7Rzv(Xasn=gjN+cAgFG%yg=xLF26(}@~w<0gA} zpnmXb$#M7atOxAXQA*nlAS21fZVsd4)M?ikFck#3Mv#P*Lg9cyCF?n4U3tGp9oJJ% zp#}ui-|%bhNCTDWhd@C_LYK}N0}?R#JL4i= z4t_yQq}oiol0?V`cepdh@Awg`%92yB0{{_l+9e>C0`dzb!B4^@#EhxfzB47mMQ-p2 z{=_f1NmYL^RRUc371O8IC16Bh@Mo9?OaGrlm{9n|Sf3K5g>~+*vcj))A^K`_-iz@g3PQKwON&p41@So{}^ z84~+m$Cl)6cM4Zo<2pRx5jVJ|-j5mKj)@qT2Kg0(4n~F&;W8Vr$rg($6D!m01zaW2qeZ<&m$8+? z)2axKN)vu*r>VzOCIr1#S%eMNSe|wW_!-HJ0jJat2#BXDe)GEnA4~Wcsgv=TDl|x@ z$_fk0j?bV_RXG*Q}CA25*^AM2ST!D&hs6G2EQan6VU ztq>A23fD(I>dcg?3<4IJC1Na7Sk|!juh0HI_s9YAe=7g?p-X9I$-7D>gSQe(W(P7j8#@vhF(8CCfxE34B|(LAK&L~8fqQaD zx8Y~#3O99a41cqle)8|UN&W(Cv7bt30|JTxO#;Sbv&sPgDP3Omv$HD!+nRYmexH~5 zr^0h9$GT*+sK3V@subw2Ke$8?C@ZOF#H%d_4i~ z#y%$uh!l}CRX|S6DIfFkKBo-59E}2Btdzpxn}IK6Ui3cmyZ%1Gr0;1M(dC>|c*OxN zTv94#MSulCmmQwHT(C(;5*Sf@KV8m7Kh{~yrf3J z$O2&22Pw_5n*VHm^M-ql&>n-=Ck(2GU4EYw`2uf)D9ieZS}eLpM6!?ud+ZcE;TbP! zxDdFQgOs4kR{wX~Y_MQY5Ie~FY|zd36Q{f^=IxW zH0YcEXGwsetFqDk?JDbBn*_c*5&p(FFU$nqe4mv%yjbu@GAIAfD4z3#Pt^qu8B<&g z0eQ0Xo@&irl^IPfX{>P2tzj}-X^e?UNuSL|iHPJSP;uW@-~ z>F;6{fE*Vw%vR3sxXp_dlfIX^&EQ*{JJTm$<9qYcw_ZP18xqo%_zzN^@kGh*T-iS% zzYGGryOo`?>DxxT+Aa{@fH`@V}hvZEWuY#h+TcxjN@I!i>amZ&h28#WUc%|GwF!+~40ALhQ zH0}pi=D-r6leoe6emCd>?ma(dRPuf#@J(v$@shB?6Q0TY=X9*^zo66PCWHVCLFsTn z=j0##n{6KJxRoV$!n&G2NYhRp!3yJ&PHgxa5faekoMVE7FZhH{*x{I_x;|T8zElMi z_(15$Dv=U9HhulRLkdy_kCu#O|4DjJ*$Yy#C+ z|1qJuyA~&8Z4NlnZ0{*e`F%jhh(uf!Z{s&iRNVR5Iw5tNyFB8@{DjBcyRx#zI-4xB zptA^VH*4X`NyUF(@OwFRjyd8;iybWj1`HSpye)mbwO_F@k0)V^8{FX@_qoeMwphP% z^_re*Sq7@4T%lkLgrp3K8RrZ*;zW|K)1V_xhHkOhPd2{YU$RmG+_NkTEV8VC*y1KP zxy}|FiiTCif}aB~a6Mi?Oq(|6TIFji(+>pR&A2LB-i>dWshAR^`6ZTFWtDZ-SYt)$ zpz4pLa^bIndMJN)2?%8)B7sj$_U{G(MuL0Fv5G%-)4FCWD00000000000002MH(gwF1q{1p!6Z43eDRkuH1{ya zGBYzXGw%lfztsN&95XXB^9?gIcXfd+U0QzO&^kPxt%19RR`-icr6n)^=A~5S+UE5U zw*V=8KBk(YQk9v5^aGE%F$ben^;IXRSygsAK;ss-G^!ERV$~I@t5hph<9yccq;q3# z%sHb~byT}m>r@+5`&E{5!?vqN6{)6Gm(N?a>eGYSlNU}eSQ3et;um5JL>NuPjXIBv zuN6aAf&;qAi-T+0hnPYwgZO&C|Twe0!{5&sf&Xv(> zbk-d|av)E}Cbu8kx9dFs8&$_uxysgIZf|8N&htTc_WRbl|66*+b@3%P-GhazF2=~j zB+`*_B%@TLg4d2R==#!mehkvZGxBBPbeRJ=+f3CdgnKyCK4R>I2Y97)gMj8nc zSfr98F$PH#DU?8DM59qfC74DCA>q`C31F%lq_o@mS-~eE!RG;Dk&ih%?!3Xq_q(%) zwsBso2LL+4=QObaq}G=6EYDRIxp5kSfwdS&jy^O-0#O#kCg_~#VdvVv;qTx50uJxp zxjP&5zX0G@s*_}jYz=qwYE&_m^3rFC>Jrs;s=LIP$F8{TzVy0BU&e|nufxRDG)B@9 zbx?K8Btfc5k`z%KDWrxDDhjGbLl{+20IF!Bk-2J!|Iy_vV3Ci2yNbv;g2{d8UC=ev z-0*wb-w`?^KHqVaq^X%HceEaivZv5R(7v1>e zE4bjg+p%Ez1sE9_MKY3lNMjFY;sK4L0@6sMffzw-Gy*;s&^_y5o&|6%`-X6&^XIT4 zAKf<9otN{X8{R#K1lUoipDBwUI%UR-o6N(QBddTM6wrAX2faRey)KUI-GLqd{uBQ3 zgRkQFq5X%f&Hn&kH7|K~lbLSdjv-ra7%)RPb5%wSR9%w4kI!eU%?}+txcBN`e)j*b zUw+Azqt`zCB34~}BPJIvW@}483pnG%x3^J5nL?=GG@6+3!pAU&oDw9a!?wPs2WaT` z%?)=BoY9>{Ysz{H=$sQkpMvK})^b}YS0E{W!BYS^D1lTJt?S9d2eE7IKd|~|-@x82 z>$}dmf5Ex6G_GAi+u9{N8I`v}$swnxMHJ7O7*&m_##NI%cTTJ3@%*`pXVA;Vn5(Y1 z{Q<%6-rSV51LU46}l(<`fG zoWSt|dvS2bChYj55CQG?(4^2$o-dEv6SOM>;Wq$e%q6K;zOqC8AqO5eX)$ zXevvT0L;KK2{NjE?S5!Xt=8bU2GUC1Q0_TI%PeyCz`An`SRI0)U9x4b4R*VFQbFZf z|EkWp3OIsRTSqV*^9sx$S_jLRyV51ItUO2Ns{YK3wyArucjIbo`omALXY+b1M)niR zZK|EB{i>q`@+5hfX#vs>?U;;)ic|?TLiZe}CTb44K($P@!b{`=0WMs5+11hI_dJ1R zmt2Xl=>-a8;hrNfk%v=b3W-eRnud^pc?BTl2cus}V8drp2P}L~SPr47uVuX%8ewdE zSIdwOVH?~6NE@)O{Y_k31Bu19cR9y;-2qX-SncHl@JaZLs=y?~nul(gTiCo`K&df9 z*6(RP!0hoO*tcapHvaa9*u80O?ttxZZU=xp)IQab&>;zAhw7?&ssTkPD=^*Af520O zvjQ|FpkveoHAzjY=Cj3Js=7c#E>#FuU3t%=SaF%U=aQ8eDcmzoVHDEH6mUlT77-;- z`C18RRDDI4&&AJ;nD>KZ0&qh(Lm*iT-BUu)8{rH_;~BU9uHkQ@oGTL;g+|DD#^BB2x7>OD5soHgfi70R5ld0u`jqA=!JAHBjQ z5A4RCO{=l~w?8Rh-Q}Fy3}6S{@j&R1)C@IC_2`s?np3)=&X_IFLqOxuJ)0bqajF88 zXHXGySvoqp;DYOJ)sI&$$GEy@JUWIX89_2Su8_ts9#TV)2&Jj!6_CYmRMf*ZwtyR) zt17s{9E_0xQy7VY=jGsEyP-8E{1U8d02(2rs+Q4usnxRhEc(;7OOp0Y;o4@vEbAVc z#?})muB$@V(qq%T@j-&jSZ7&xtJ1ElVh^uLtwOB|VH82Hg$@{u?~A=%`B` z+`dt7TmOQ7w|hj_Z>pUkj|Q!3gF^-`*?i&S&h_oTq2drwst%~Ca?piz&lTQ5jk!eQ zqVdTo85x`OU+GIHrZ74+4{@SLvW^{|;;$jLqQH_YfkE5OPiwmY`M# z+mRt#e?OrC*QshPDEF!?pM$``U(`5-Eaxq%)w#b}9Sl@OUc^DQF4J1p@1lF+82U3O z{8#FFvnSCxdBOoWI zF>t?#j6-C!$l;2bi=0Cw0#OK+!9;;nrKxI6fgV%Goj^K1fhZmEpOcEJaNelMje&_` z73Yyrn9I4C$b)VCnnpZxqqgBsq%gg{mzU!-i!h^tH2df z@H~iv;+_5ImE-Ggu1Ri=mah2EWtZOcRZ*%(gq~eKLwDEI`6mebwKMMt<(V> zBY@*ncorSwy0iv(Lb1m8hYD&^!mN@6qBW-~pau+_lVT`*VhCd#6W%1PKEg4Hf;}?o z=LENmA|-|WV%W- zT$XtWk=00$JP1rG2o)3p=W8Aw9Ucs;Vx5j<{d1n}2v#w-pFx$_c^&4Djtghf$QUk9JV7+dFITg5e^Pc)zLor7b#Dbti{JW%lpJ>g&R1wZN zK$w#+DZmmVrsPbb3e57E@h!LqR!T@KxFZj%kH*9R*K@C54^y?@Y@s<=3x}(L={y{) zS*O1tJUnYz?@Bzcg;162;T=ln1d(8BUeo|OpCizYcK}w(+!ncJc@tmZ@{g&O@Vu@sKqxhQgEKjL3Y^S|N!Vm< z;-v1m@1^($x@iDRPJ9+1F9D_lnvk=hbVvb40IL9E4g%27Q=M3<9X$H1eXK*tTE`l= z&@1&R8t}n0P&=?TZgUuiomDYUfV?J{-a-ANV+Y3}a>yhwM|D|`ddYJr*I*nC$PQwM zQ()^JY8gALW^|Zkyn|mpLUUgaeI;->SOjPt2*PXYHq`Jda6|IlU2;SB1f()wpp-c~ zYVXWz>l9LX?$h1X9nh=4*kjd0?rZnM=j-;rPqU9x?}wK2cC5W)kJSJ4620%q0qC$h zH21*f{{C***UbH0wS&A4abK6($2wf}uM5HJKg)NRsD1u^0Q+k{uwK(&h1~1?aJ|5M z(w|YAKOXF^^!(-L&ad|1KmY&$fML-6sSmPhWdHyG0C4-z5OSU|0K5PI002ovPDHLk FV1l}?$m9S3 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-ryuuta.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-ryuuta.png deleted file mode 100644 index f7320923793b2d9f8fe9284748581168dd17756c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11722 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit0.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit0.png deleted file mode 100644 index 2d312ceefd87171f2b38c5d68b2352957b7872f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9518 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

T4p9RL|Cz*;+1j?Dynu&H!u`CH)8E&+ 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 R^q&818mihVKa{Ma{twR0S6Ki6 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit300.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit300.png deleted file mode 100644 index 3e4ec2e047e2fe6befda6d2c174a622f479edfb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30325 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit50.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit50.png deleted file mode 100644 index f02ad11a17461e4032e969151dbe53ccdb298623..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22638 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png deleted file mode 100644 index 8304617d8c94a8400b50a90f364941bb02983065..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3092 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 Y0OO;-v8NaCZ2$lO07*qoM6N<$f&jDokN^Mx diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png deleted file mode 100644 index 82bec3babebd59263dc486e5900a3a01ea4aaed7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3712 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 JPDHLkV1kmK?iT<6 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png deleted file mode 100644 index b4cf81f26e5cab5a068ce282ee22b15b92d0df12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3337 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 TZ*tsI00000NkvXXu0mjfuoP_o diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png deleted file mode 100644 index a23f5379b223d61079e055162fdd93f107f0ec02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1910 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 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini deleted file mode 100644 index 94c6b5b58d..0000000000 --- a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini +++ /dev/null @@ -1,2 +0,0 @@ -[General] -// no version specified means v1 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png deleted file mode 100644 index ff8b02ce800824f510e8655a1a9d977418bbdeb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4540 zcmV;t5ku~YP);J

@NHN+%xmd&%Mk3yL--e&pG%0_n}mDQ`*|v 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 DAWL(? diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png deleted file mode 100755 index fe305468fe9efc47be6e9e793baabdab04aab4da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10496 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 lHvJ5V|K9=phm5}k7yxVV^TAT2(qBnTK@ z0-l32%{aNe_uRQn>?9|hf!n#ip2wW8x3=Ts3m|d+4Qb+fVseKf#rdn zA?|+&whi_j>@!#lY)`y^MH#JOM?c(q&ai4^Z*+HeKd=%Lm&=tO3A<6cJILgb*v)S|t*jXcwTurt&j1E-0hBxf&;7YnT zaKvIUo~)Z-yL#n|N9;wGU>Q!QGizaCA(Ck0yu7^Z+}zwOiin+^oxP!S<4u>OyDx{~UhYP_b_|;Gd6SKuUGbbgpp9`t4 zuP>&Gn4O&!z42De57RsXBCzh=d|g#lMHVtPHYN&LW+|WyMwhvehK7cGGhylN?d3wy z(_gF-T0Ak^+uOZn!Xh-gFxXFJArKb)^hJCPozzWzFLZ6OLOwxQ4y||u5%CTW3-#G%EpOKiF?SBr{}0zPSNZ7njxA)!!clPzCqVIo-wgyl56+~n(;ni_Jo1OkD1Vfhil za$v)$cVI>ZjFcm#>+F2@4IJ*tqtzP@=%Sg1mf)-rL@(a}LpQi(m5Sff)D!f5RVU)R;u zk%dU?u>@h+wc?TUV7K|g=kt+;=&^^25GQMNC+uM+ECPG1COS4pH=SMgkj!@Zy;t^zzGL^7M?C~|t z6(P*kfX1LgbBAOApi1 z{yuZhQ9(k1J@9cko>SVso=31tK4U#XWniAXQxWlxN0?XA z85)+b3Nrd#2H(8Hm=weF!_2XsP{{urDf3fW$_V)*zyQwC!s?l~#8?0T002ovPDHLk FV1oRZC2Ifx diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png deleted file mode 100644 index dbf7bc73bc620b8782be5518443a36c91d1216cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1756 zcmV<21|#{2P)p$w|{(9A*RO z%p)u&yhey6EFmmnL=hHhoQDXF6GjOWgfYS};Su3)o_`i<;vgGCSVdS(SWj3U!T>%b z+#z@f_XrR8=7J&~#YA@yR?dK)=2trhXbg7}s#TVi5kV`Gf|W4K6PU!MmH&*(K^ z1P2Zr5N41hELn{>GGIKuq^71W2{vM6m$taLxGxF|3vJM?k?Rx}7t2q5wLguAeAL(1 zd#qNg4NpPuxe7O14mqn4ALo>7ayp%lgY{;GQ%tnBwmOYm3$o>}uS3pi#Q)`b!GuH* zj6OR$I(}8oojrT@wvlT|vPE&XXc}?OPItxe$&)9q8eN!WWo6gZBohMVS+q2cOy!fw z@#DvP+S=NB^j^8Ds_L?ox~~v!hiJJVuurv!|Aove4l*(_?0U#oR8-h;!+eMW>3Xn6 z(A?ZC90_TERP|ykmtMlRGWo-Y51;p^d3g4&UAr13(ik?vbCLCOa&i)k$WE$T{y?p6 z(fTowA0yl8;zM7tRasf-2Jp|bv$I#m#Kicz9gWfyi~K8Y5T<+>=jkQxa=GMn;U#X* zE&fiB$fHBaVk<)R=9P*cwed5djUQ_i*BQP+Ph_KB;!RCWZt3uM-|7r>5|ekK_9v_( zY=AqdoD1*oBftXtQN_`D5gct4Z6>5{y#*U&LWQ0n{+Oi_m^hP%Muz|*gJ@nNyo#*O zt9(Q>yrS6WVhug&PH`Qw$M^L{n|$9P>HC^+JB!{65wWKn-3I}F8i%zg4o;mqWi=A* zW5(j~V$B3@mV+SuA<7HU_T9R5>oZh} zdamPeINa&!>F1@S^Eq)H)NCX|O`7VJJiVlmEB5T!W0$7BferpI=ZA=}nHMYMkuH-q zqU-(p_n(&}f^y*oudUD#XZKi!+(JkWmNd1BHYrCSdtV{aqtmLaAW27XD*uGAG{mHl zXue~cRoxRbO8ONvuY@8&8ty^q|5LrB(b6eMBiWsSi%e>`hsB^Y;&?A{tGvra2`0;k zdtf0-nuc?HjZb;!Dk$Pa8jbW?&W6k6VDJ3N( z88eMCBHY>8={|Dg$W@K~Z!`#M^G`Ta-r+?$?w2@|ewXlp{3FQX=+UFrnQ9ptzHs4! zi*D4Zh_I0X8FeekE2XkZCas4s$Wr^}K7IQ1?Eu}8-tjRfqkd(5ZQ`ge;2%{2y}iBY zV!}4)iTxzF5K;tB0XcKx)!#O;-Sr|~m*^RFb#*T3aKvBA2T?OiWPLbEqpX$W=2Dil zQ^YyJtO=$Q59P4gY|3_RTr1+(P)rdr3CF7w#r~qaS$fxBb%?jbzE>2vxw&E0Hrz?_ z{4U-OoKBo&vQA87W@aXalXzoeqbw7*Rz^j{k#yb`>3p`{Woiv+^Jik3cPofTibjA+ z%&?M1ZLZk-d8k>dFzI}g^bYYQ8DEl2Zu*Nj%pEWL|9Oc>+x^L8s>q@RYT=G%13S#Z yFpz!1`;+&0({xl z!WaUPdgUhj#U&~~aA31EC}4od2qXp);Rk>C$q(V@0L@|`3WhWb5t+!%0Xk{M#jwO7 zFy87s^+8jVj(AW5lU`k2HI|i?>6ues16J%NAXsfYe_EJVR8;t6wX3VE zSrhpVa0^y@&r#+K0;sL=Co@y+3lnK+X)(|g6VEjuz3VLHV>CI12US&7Q6V8Ahka}! zeHcmD@7Rmu)|4I@85uk~J8N*4gi@&t=6&$O!on_*i7c2!nNn^mbApYdV-}Nxx;?_f z!vm(Krrzfv-rn9mnxCI9tTsjy9dP{X5sr3<1W z930H%4Sf)^H{*i zBS{23h>YEV&d$zkj2iBePfAL3r{lM!Xu;d&A9CaVthdcD?N@}gDm z0j^g(N?h~+p(Fv+QpuyO2L%OPpy_qMm(EHzw+0F`z+JRQz&J@&lSuWsJkUqbN^g;mSxM zT!Pmws0%MwZN(QZF1vWPUU$a#S@Iu^!cEWv=#M*<86LjhJejQzO*3&iVy0Oqs=}c^78V|VEb9L^3T!n)9mi?uJ-iwG}{)zAn>o) zRH7&YS{gtoba-xnJ#*CK%&C_sDjlchk}Nkjms_j%_xH~;(|h9TJb<>r(jQz|St)T> zBrz%a0kswq$#J<~3W$==I$n?MulP}{wpWMGgDweGY!)P?#mqK!LbJHXy&Ex1u8VV5|6Z3J^r>Hsg2ytxRT5j0W(_@fAd~9rN zQD_QHOiX0+kjBNaC8iu}0 z)H~d>*;k=$m=R7u93Pg@Ru1KL!s6m$9lpP0NF$dHN2INV1oi*UQajCJo0XN7Y2JE0 zh%4OK*!UCkszew+5Z262DjaJCM6bTjQ<}EeDTJ}Nq|}UJvgiXq_NEQNbqnx4;10eD zbg9~6h$~2Fz03AZPy#@@j*Ezh2z8d6Fe)r8ELfZioYMfu#?jHyKN*J;k2$K8egz&I zTxBA=BUD77D9Ql5?4{6dqA>RhzySB(bgRC#^C|^bHkMMh$^%4ilPQtOJaYUw8a|12={s5$!(MW|36v#JpL1403@th-uUEs@Bjb+ M07*qoM6N<$f~Rj>w*UYD diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png deleted file mode 100644 index 4564f6d8bff88ce7a1e983aa9192b8c882789f5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1814 zcmV+x2kH2UP)MX4xD zf25+&_`^?;7>(E#Nd$>eNnaomF(fo5Ch!6eeIN!x0uMwXfe?AmHFJrMt*yQ6a5&_FpC`1^Z%w=C zJCY8WYw)Tc313)1#2v`)6Jb+6$LNigvs5SK0`Y68Roagv#t zd04gI?(XgpsX0WL_w_Uuj!^g^YjkvUm{A7?@uH%lNN(QG(FqU9pAJXFeQ9ZFQL245 zH#f^fMrnrsQX#Tpr%s)UGP@en)6?beh7t+$b>j)X=yP{WO#V^nzYkT ziD-p&VPZL<2?uf!UK@vnZc~*cFyuGx+{z;nBC(An#oOW<1R!anXgv}n{$vuE zg|7&3&&S1m85tRoCf&**?`=v%m;{7fnh2L5k)NNR||qVjAY%u8;Hj*e1)OT4L0@dLYwS=z2sXh`JJ z%nFIRy1F#Irw9_*^);6di-j#4_6{SG#Dy$+QPaLj_>#sv?CXNWD}<+o7IH^|T&d3m z30*Ui&cZ4yD?M$zNSj}U+pZQLhy%XR^MwTqMMWqq>!TaKBZ*Om^nL@*6nY(h( zesPn{W{cm-Bw%fAZ3A+Gg>sOHTKmM+q@<)HmMw0Y1ZzYt2dx$vjA$Tln2QAKu5!?V zeB)LoVz=96w;C2o*jGwvSFF0a`rEBcq`bUb-nYh9_dt$XSRk>S;h zK?z%})-&Avj&PR7@05u^BtAxXhwD!3W*r?JNqV`9E2Ti0XQyJN6UP!^m&@uJd}_D{>E`RYWN-cbhDXLyEsd@;Ht{L zsYE0)D6<~oHKX!BSCcIMuCIl}x)SlW{ZC}8+1~;T0CqcF2-dk}y8r+H07*qoM6N<$ Ef`bE6AOHXW diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png deleted file mode 100644 index dcef35eb594a4c047cf37dcfb7acb660cb155de6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1848 zcmV-82gmq{P)Gbi!bE0Cyd-XtDVqi0*(4Qw9_}$layrtC&D(6YcsjiO9|6~R zw>L+c{Bat|8k`(GdNjAbzP>OVh-YVK=d-f1j(NS_d6;U139Ua(hfQK$R#x_6xYRu` zFyLn3828LXo99Sg!AX98er#%L>bh`&Z)|M*Mw*VB0iP~T4jw$XHC(_)M@KzFLqiXF zdWGaxgrdo;qQjz@*zNYraDi`YYr7;(uTY?wRn`3S?CDx{b#+>Ne0*#;fKN|PKWu4f zar5*x$%ImdC9SWZLzb9VRaI>Xm)4z5r>yk>l837F0HJZw#&`{Vp7Cb z*5)JLiD{BMIO?@(89Y4

4xgQOuBUC8eI)P{;!z$8y(@N(*kQ0pnf1~BR=+9 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 E0}(Fc2LJ#7 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png deleted file mode 100644 index 4dd4a6d31923f69de6482c8882e04ecb4366ddb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 166439 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%0Dwjh0N}037W8g|D4}N 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 kWRgiHc{?~s(fh($ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png deleted file mode 100644 index 73753554f74fab988c7e5d8c7722d22928120682..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10583 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 SYhl@cOr#FN0A8=|823NoZUYhk diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png deleted file mode 100644 index 98a9991c2f103308706ea3c52baf0b3e90ef5a11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21353 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

SjXN<|hNt%y;m$|%aMvI6 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 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav deleted file mode 100644 index 5e583e77aa7441b968e4ebc8de47da1033206c29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 309536 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ⅈ6czFY!U7e?jO@*W?_!lRyzY2(E^;Kx)@)ZN?STw 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�G;%5cXFInEnT$5|KR4DPC2+ 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

8u>eqBed)Ka55Ll*N1f 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%0D+W`G!p@#dv>o=N9kf$+%S0h3 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 Vf-S6L9y8sYF^L(hV2htO{~vwmTOI%a diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav deleted file mode 100644 index bba19381f1e02239684b517fd0e5d15ec1fd33ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36868 zcmWjH1$z_(0|4Ob?(SUo1`HV8-5}D^Al)S(-6s@c0O0`$009sHb7rodsW<`vOi$NljA?8Z zn#s=^GLoz?8_CvYrvvzaPk{9R5>N>21AGNo1y~Ab&YH4CS#ySyX-fN2ob;TOC^ad$ zCb=!~PhwU4?>IX)J^oMhLTr1)7X213jE)E&i98K03x5sPgkJ~$3(gI|g17xWgDOAA zZ}xuhU-g;2ZqG^Y4NuUU@?3N`xsH3TyF)IHYrN~3Q{-Ca^f`JtUOCDge)~W6_x9!X z@ph%{zHOE5h4l~XLhCr|dCO)?UyIz#v)ncBGV{!0^BdD`Q?+TmiC{_^8;yG78RIHr zN8@GV0^?+(t$l>?W&2*^t@biwVf#|!kal1DuC^lMy*8q8L7S;PWgr@*26_8DL!^C^ zLD}xoKWIOpC%2RJtJ@an8V#?sy$yKn9sN!XMBk?VL$^xp(uP$#w124DG`&^*HEl|* zdZ%)tdbm=mA}Y_Rx+;0fg614T*MX+c)Fe z_K?YCw43*vJeCdS8`f&ePd0@mX&Yx?Ne=qj+3_YjvBk(ao1k$zNFZqy54$Ad0ie-Jdsfq&!h`lfzpTaatTlVpyj0OX3I0# zV2MWlKnhpz<&%|9S{Wp^0MyE4q!}{U+ytcm$TiYXT7mQrv2h%8%-MrN7 zvuv;w+T_*;_Wkzm&JNC$3*eS`8a+pSzxh@LMg@Kf<%V40@8Oft6VY+;Me$FGbmHgK zwN!grl$o5}lr;eifGwaApq>ybSPNYP{Tt4LuR%%?ozQ1cJnTG7K7JsMOw1>kNmk-( z$|>?@+Hh)b29cg&{>`|?rm~>iKI{zFz&^^Wd9Eld`#cNK-2Hg$I(=@`&23oLLEvSML9s(L4HcUL^@B}LhMdN6Q1Jt z;=1CX*q4~!(cRD)WIb{NVi+O=dkEVNB|=4z{a`Jq26PhW0E`0snMGzNXByJH^re(K zxh#1l(J8SiZjOzJ?TL1d3L__W3W?580Zje^pyrSc*Xuw58W4b5xjNIT+azd z7xyarWYONHCSokcqF zDN&akhiGR`zW7_t05LpwxVS8LsJM6TB=Nx91LC2%dhvqX-Z@8e_vc*6eV+3m*PQbs zmzb;1t;!AM{+x@>o0+T44dsl^W#t^pDG@&sSBsvA2*Pv12L5WndEONMI_^Z?IL-ua zC-y9kkhPIbVeVv68JC#_^maxr%}M`CSxJkLPEurq<)l5hTtXSfjMX6@qEEqhBS%74 z!_naR&_=*u@YzfiaD9rGU7QG}X2qHkQzBnt^Fogzivv4CV|_J&5|7%8bZv0A*$bVu zR=4f4`MG74@pn_7wju3RdZwYD)}fuPmZ~o*&B{P4NHJSJUfxO8QFc~(Mmkm6Nm?v* zOEQut5|d&&W+?13`?nydH?no*m7bSU;9TKKwtR&o$Co#9=OW=|L zlAe;uk}Z;Xk`~DZiCFrlWP!9+a#gC3NTfCiUIv%Wk@b-NkS&%DmYYr2!O^bTAuCKOTe^vK$8`$u?eQ_Ji)YLxOTxr^8IbyzNRa+!B zrY&ioV23-`J0#92&hajw>w#;(i{$3HH@LsL{qAL+Egp~;=soFO=PmU`yw82>eARxZ z@3w!WpBcE}KNSE6$idNpLBS~jQji-%{^2?^{p6L-zFa z%ya+le(U<~61wtS2b>$6Xy-@ARR`74+cC?ow?DFNw^!Ok_GW9NZMF5JO=O*5Q&~{9 zeHMwepXIKVVfoGKH21bXGZU;Q%u&l$v&*vBY_ZHYTP$nLUdthK+;Y#Hur!$+7O%O^ z!na(v%&_dXoUzQbytQ<)G+H2*uao(q9h-Yvn^ zzL}vz{sH0Zfew+TU|tj*rp1OtVDaP8WLzJMCD@5fa!8Vp-jvGETu&EfUu39&PgyQ7 z1y~N+2LeI{KyE@|uu1S{I1+gl`44Icx;Ms#F=BV)4&%9mLgIDebrPS9q#UA5rJAUJ z(?oP1eI;W#6UFMq*0H;CKXR-2*Z3WUyM>*_YsKAim*fu1pPav-a75vO;%UXTrCUqm z0Huf|pX)a7L7yJsS6>~g!Sr1P+@ zzjbcx(z$d0E{W<7onKdv>AbPpTivnxY;~+tkLtfW{ng3U@m@!Dhus|>RSm7$Q3 zR0XyXiNJhEBv48?0p*3&AknZXh%V4g@Wqe|u=C(Y&=$}e2nO^D+za?GXc^!&@O;(? zc$J|6+S3cNRcU4BQffkml+vV+B7xWyA!Kp);J~BJ$@^C zKGq=$jeUu%iq4JTqnL;?@;Q7nawxntGCBNHq%w?&z`_s1Z$dM|^F!H?Fmy7c3U&_d z461_V!R0|!02tg5I1|7Issfw+7yQS3&wP`-m%U`qT2GU!(!Jg(bFv+q9F4YI`(bO7 zb);p7g=p?)RvNLUEA967C2j3(gAH**A3e=5R#&87q3x=>su`oTs@H2O)wk6NA&mZ{CN zThvYA=Cw`xnwd@Anrj;&%}W|THSrpkH+^WpHcfB1)(B|m*Lb(y)v&1kd_%YTNe$%s zt_`sIAq}PV+ZukY|Il#0KHi|K@7P%0u&;4;gTHZW!|bN34Th$=hFQ(A24!>C#sMwc z8t=BeZ-hu9jiV(4n?6cXO^2ndmfN!F61V)Rv{I2PU!*+JdRCQHyjSm3IW-WCK(|&q zRj<;WF^CMZwh3)GS9t)(XpI+av2RM<08_sdHR#uW*%l!|s25YrLfa zm+w|^QJ^pk4&8{{3>U^GMW4h;@!rYziRRRn)UM30nR$TCfLWlwKno%7AZuV!*d~M% zu?_`5uf)Kx3vnR)ID&&vPI^v?l6O$9Q~S{d)0K2HVncmfma)5WL!5QI4EGe@ z&-+JU;r}Bv2zH93!eQbT5k03~Y|43=b3gY{?z+6YdA;**<#P+_3jl?!g=Y(YDtb~l zv}k9cuW(r5#zI0NvG9Arvx2<^Qws(cPz#U++Wc?%SM#^!ugx#bACO<4m&{w9_a={< zw=VB-t~A%2^Iy&}@lvr@s1tS(R14Pd_V6BY8aP>&fZdz9pSg$*V4S3GrhOr&C}7ei z@^1 z(+|hNF2_y4-o`D(s&PxOS=?wW4===);KP_o{AWxr{C-S7{1i+Xz6OK9Q!sLzAN>!` zfIf_qqL<+QL-)pgM00W9&|utmv>Pi&o3U675Vr?23fBSq1?R%{#$Us|#?Qr%A>JIqx`=xYxMVyahZkpUbb~*YZyax(R+3-WH%m9N{z39^nMBLRcq`3DaV$Fe&~a zbci1d+r(#t3h^P~7x8J~L-9Yt^WyKq&0?TvrnrY_gm|;4qxijuDTazs;yz-xsGsRe zY6}XFT!75L`y*b!zQTA=0JJZ-3_J-q8+aglAuCHq)8(m+sVfO=vM7Ep{w=y7b~MsC zIy=mWl!nmZbRZo3>{kW;@cH}?yrcXguiE#}v&}ch)7jU}Y(8_59&3 z^o(#naRc20+;?17*KF4X7uNN&>$$V9tHoLCI^b$@-*NN3aPJb|I^SEr%1;h<3{D8` z2<-@e3Ezy6qu-;`V$RsfI56=&K}x!k1u0i*V49HGl-ZrNW-EdFfN4+-xB+5<9ERhkt>$5x7J&X#=U0tRWAklu$cUyU;AuENwOIIz2+` z$@rE2hEYe)W0Dy=m}40R<~c?$mW^?W)sd-T?O>AGP0TLrJk|jAF;;&zfIWb{l--wY zVpp<9a&p)YIAv@acL;kncM1Cl_cHrE*Tv3o$8f57jhyAYncU~R2$#j*%e%~H@p}pG z@k4?hg4e>2g5{zCLWH49(puZpclEFXt`FnVB!m5fpUHH5VMty;|tW zU0O6FZ*XyQp0ya7PcD(=DT){8%_s)ueJ#42n^)8~_t!#G&d!1rIS2FG#i#Seif`vO ziJEeTi6A+jg?+{2g$G0lfmV1~P$8@l>=j7)2L2*`SH72bnfDV9%X`RO#zk|#ab|FY zoIC6}>@4dTYZ~hn^Bc3B(T|B?d}Z{d4`(c=eWP!ocBB7FxlS8S#?!i!HcI#|&tOBkFhyYizwCvXmJ)=qIq$8~Y|7}+xE2AN)J zkqwXymY{e`7#1-chOO$6666JnHj%u-Dkg8l!s%mLXE0?vZ zmGss($~yUF6FD4cVwp&t7J$;KiSe&j_jS>DNV`#mJXFIlD?LX zmiCkOkg6rQ(rpruG*?n9`Pwo=vZN*5l5O7KvZp!GENY(DtZGs;J#HG^^tj2;Sl6_n zQQl;0_}En0u(xSTeQs0ZkG~q(KZuR%z8`4t*C8AB)~%@@U8n!ir*6uR&UMc36?MnI zcdr}zeR!R>ZhYO&x_xyM>ngwBtsDH~x9@xF_xyO>@OQnl@q0r-bEI)`i?I1$$;g(B zbc1A${D#!fs+Y}E5?Vc~{)!!%GfJT@uKGh?qWQ;As9n(((em35>Ll$j{a)iHeYwe} z|6$r@SZeOsMzY}BuUbs)B5SSjq;;FgYRxfIZMV$5Y;4Oe+d+%XMzBt_U$(xocd=m{ z65D9UYWrbFv7^Nib>utWIrliXxcts4_a;}XTjXBo`Ood~EcMLycJz9@T;Ek6(|^#P z7q}GY9{d)Z5(0z|hI>R_L{>(>M32Va$4Q-8w{+RiaiDv!T z#lZ1E3b-C5fsBJ3hbf_55kujP$bS%%(E!v}%uuudcM7u@@5WvwbjQnx`v`2ZgSeP7 zlKhOipJJfxpgy2?pp`Km&<-+6=q-#}bSDGDa4{w_+8DPOpBYHzb;c^@d4`jDoiUg7 zhS9__FvM&)a{^n$T*~G!ceB%syKD3(LR~uqLs5 z%+JiP%-+ni%tpp0=0e6wCY>>lX`zo{HqbjVAJQ|7o%F|yN%YB#3i=278`@Kvj5?J1 z59Kv^1v#HoOgci46JYq|_&>2mY$c{A<_+pLst>XZ`2t=KuY&yrdk#4Q9ROYd2>~~O zvA_W6BH#&V8DKwXB48nC5MTnR8=wcM8-NSy1#kkZ0hfT)fI+~afFxi#;1ggm;4EMj zU=83`z(l|_KyScEfB;Yd@MbZ9k6ByxM)qZPPxfSXZgxv{es)^+VzyJ(n`LD?10tE3 zfNzD$vjIjR26zB;3)mIh2V@2}gN{Ic26upJ z!1d4>kOeRU1Pz}8eF)b;ry~}?=*SSP5xEh*1_ebB(Z>)E(NyF}%-=`}W)Nxu)`xOo z@1jrPwqr)($61K&-P89nE7HNtm-H{$oQy8JC$kjb%%lO+vP*!?+1J2)Kn%DBPynh23;~gV z3qUo%qo8rXC!m=?1!x~o3`T+OfQ_I@5EU2$l|!1K?a*7WBy1mCjF^R(gB*r@i24Z? zL9azuV=rR%;?!6(z7*eqxSz0*1R@#8&&j)}*QwL#H|SlNcbG8tKkQb{G43_q9R5rJ zUq}_b5s5{^ ze^dTvd1(cz;%vp5icp2JqPViSaz^FG%4?NPmF<=JRk>AXs{X9PbZDz`R?VntsDxC4 zE5?>1%TARNOB#zsMX|!(1B#E{~W81 z_KG|SkwaqwjKDzOLEjP2P%q3a^jvo0-M=`%uHJU5qr&#mo@?DwnlqLdsHje+O<^OXx&0x zz3!uKj6PrgMSoUbZQvRn8vZeKZmVqj(e|qCaQnjc&Bjs2y{4Z`*UbyfUo3kq7VASR z#;&rJJ5u(sPL6YjtE=m&d$e2bndvck*LqF9D?Yuy!QU467HA3X3?2&+LxS+3(Eczk ztP7tE_ljU6&myZL`B7QqKr}BJiB5~oi=B>2V#erzcxmi)d}Ay>p^dFebdTRltdGkQ z=i{!#%Xm2PEuKt#il-8f;CO)Nf9V(c zg1)ZaPQKZm_1@F&Padg@?qRx?yQerCTvr^$u9$tlbDG`f_-vctD6ze?AGhY&6PDAq zX%>j>rFor|Y4%#yn0~T+HeN8Jj18vI?J47*Z6!v7VP1PDLv7n>eMMVT_qSoZ4rQp- z&ej)d@9XwxqFSYTh_+mPRkKf(R;!hh)Iw#gYQCaE^|O5RXf{d$&4Z-tTbiUG$#~f*$tPL0 zR4i|jZj{fJY2*<3=+?z!W;;Oi3jEszNA z35CPABY>DJ21+Coz!Wx}N`tcgtO=k3egNGCpNFi5E`tq*_dtk{NK^>bg8qy-h~0+c z;RE=k1PpNxF-+V?`axPxK1v=;=}v)D)s!acHtI!M8Epz(Pa`qT(Y1^zjE77Da~10y zQ^a1z`p%}amvCOPk=%KlyIeeX67OGbnm2)Wnh)ex2`=!P1o?s$!iNH$Xs}Qz3JMR4 zABrk-R*Jvp|Y}(({66$A4FN&4ilk6kqkYq#v@o$0wzX9Kfn}Tb^ z_QNVLl^8ub4{b#;PzfX)$wmg@0}ySn6>uZ;3@ir!coyg`xES;s z*asX3eg!NA?*~G_(|~$V4e&n@6ZjgG1iS+I0iQuhfE)w>8bEP?4dekJz&^krFb{YV zybfpr>w(pfVW1O`*PtLIA3OrO8+-$*0YhQ=kY8ZGK_0@q5D0uRbTYgFdIz2lgCefM zjw5En*C6L1&Y;dAf1uT<1cr<$#r4C^!!N@9K{$fHM|?)8CFzK7$Pv<2N|d~kYNPa} zsi-1)6Ai$)PJhej#~8zW!+6i^$P}>dF*mVFSSr?aRv$KreV@IN&E{CyzjKCgESz(k zpSd1R3%4(~Kko?l9QAR zm}n*cg9yeyDZ0d4DjLKaBuaB>qAy&f@D%rya16JX(8+l(n9tz~KCA^F2H!u2*7FJ zxojQaUB;CCkv3)wDQ!BFY)kQy_T+$sJ+Uxui2ojw#P&zOME6BrMRtZChxdnGgiZ(F z1uq5O1|ItrexI+zKf!m&SMNpp279-8zj_GX*`C`To@cDb>c)AVx*Oel-3Q$h++*AV zx5yoE`Cac^pIpaWpIv{tDDI5wzWa`Qy=S9mq<5lM=1J@X<3dbo5vZ z9z7UCMt8J7Y7gcje*(z+kwIUErCk^-~i5#3mAPe|4ZK){~x~T{@K2Kf4i*5W(N*hGIBBkd&fU((j*x@r81Fb}ziE%#Jhq9pa@$?&Z&svL zZJBN9X}M)SYL=Tmn;b^3(bLW}dfIxmhYT~@fQJ1BlK!c_K&R35(gJlqYsA_u>i(LC zs?}58!m>e9m%K)%Y3(9! zQ1oj3sO+z}rRuNTtL~|quPIXx)6z6ubP0`6U#HdUSLv1-B095SmVS8KSN*lNJVT~! zgW>1)kA|D=$hK(v__pE3Yi;L@fi|j1g|$VLW6WXc}bcX=Ypd zT2j{0R=aJU&0t?9YW z?Fia^`-2AG*`U$)DCqGu1w%e}Fy+IASpLqTk^X6+o&K$%NB;XEgFhTX1v-bT0*k}b z0~f<50UiY9|UMbp9Y(Ri>X>I|}?${-{v z4@M%EU|S>>{2pP2w2>X5LD6wxXlz!*6x$fBjh~I3NYurbCxeMWDO4&q%}e7m9Wvfb zzwEQ@aKLK77+?qBC{PqQ2>cFI0{IObgfJkNpr;|-VOVGb>@aj5oC<>>j=|0&KyVRq zIs6K;70yDHBMzaqBYj0KQHK4xGBcN-L9v}+D0lW;p0qh1|3Y3950T+WZ zfCTV8;2>}dpd8o@AOQrkivahs0Kl&7(d^1BIXgakJX4%yWYU?_>6;l{DwF;%2}>_Y z*puk^i^R?7ns`YhH+DayjtmZ-37h;gLcjaE1&h7;0h62Qf9zuV{&otz=N+9rZ|pbR z^X>QDXY4``&~eYR)zQz(bXvWyo%?;WT|7U_ecNB>t_&>p+zIf#IYE{8dT^JoASCc# z2)*@p2penf9acfl56KjS{*1`?(d?h-RZBKZV)9VM5lroN*MppRfY zWV~czSvd9(_BhTa&Y#?e+^@Vhyd(TU{73v={E7TF-Y4GAyd2)Y+}&It*TNac>B_mn z-p5X`l&tA2KC70wfXQUOV{B${7%uu&`a*h$=A+G{t)oe)De5@t7OI>ArH-bYp%};= zDaXi7U>Ju^-wGsIqS%6%GR3HkF#}I9Z(TKeW0z!mnf!D(SfG>vkgjc||Ff9BD ztP&o8y@GFpuSax2bVdS^DP%pe4s`@|1U&#f7?Vc(G0m8R*yq?%+%cR6HwM244<_Ui zZW8_@Od*~mjw1a-+DCSi+bBb+18Fa5PwDv#9&;~q1Ix~8We?|c=HBKWEP|HAiq@7aDR!2mibt0oD7jJ!EOnMnFXfkAEbUzOp|nStwzNlCs&sIf zsBCK4=(5#itIN)n?Jkp*{Z&RQ|6Vq&++KFBJY7~>E-W{dk1fxsI9$H4LQ(#tf?vU| z99!{w<>rd-mA5NURq~2~Rq2W~RjkUhRh5;mt435xt7cXzs^(QHs@7L3s*zyXaoQ_Ci`dwqR@SxjehLCU>YvF1{_8FXHesg45i; z_;k()-fb3$JB!)C?#Xz_>O#N397H?8m`}Y)KS}vS`$BG@CP@-XB}q%3MD&x^6BwkM z_@P7-ZasmByMXVAeTkcbF=O|lE3x&cJ(x}?Df$jF2c3&tfx3x!ge*ZM5!d0P5xMa5 z@Eb4{j0+>f&O(Pl%b-Uf?;&>ZKuACEC-6>CCHN+AAE*|f1-4{60L_`b0884DZA*>L zT9cPEp@cb2NK~i$#J8sw$Lf-oqB%)hWI-Yw?iD`}IugAXI2yj@TOZuvnd~3qs`2JH zsBWMw?J%3uwiYAG`mGIP{;J0rTebW)zq*4St?H}oso1VI$Oo##^0Ue}vQEmWGPMFK z+oO0V?WR~QwYL^YPqq3aV_RQIn5_pSR{1Q+TX|o}DS4%2w!Bi(K|V&pli!k5$~#Ht z$y=q@pBHb0Z}eh>`^{bfK@5QI#oZVQ?*YyM%}2qtS(h0 z)z4M?Gz-){wWXSbHmrH7d!^l=->n;Fn4zy~>tSHF3)+(H==L_Fzx{)$*?7VH(6rLB z&0J;u*+RFSvTn1_wJ&rYa{l3#xh>vGU*Ev-z=e=26p8eS7Q~OlyC!AHNoj6oS9TKM zCU7I@JNOvH3B3mcBAy~5$X%$bXbOgk-H6?UYsE?Nod{&&0b+lWiL`_~fbs|BH1!iz zPm9y48M%z3%n?jEYcq?+xyc^O{g*R~w~U+5*K;57%XxW%9lWE07M?@EjmDnbrh$jfPh~DzQ3v>8H;cnh^ zftCA+-+H;19 z%4U>N_tFnhk~9l>3T-g?GW9)4P01(WDQk!w$e#$qNoc}K;tc$KLK9Ag2jMQ_o?sFf zCR&W1iX4XA178on1-%TFg5QD3AOr9h01R*^Q<4d%Mx;6?mnT-lPsJWaUq?dWV5lrK zEVwc7*>CjC@{RE_yf55M?p)Un*EL5s=OBB`PPM(Tr7U}`CiAZrnQ5k3VO(y~v~M#y z+V-_$+s?H0G1M6@=y?W#{h%S@n;Kr`oK-seV;~RI^o1w|JOI1Bp+f*A=Csa>VFI5Bt*Oy2)pB%sx|Hre-8cQu`kjWD zKELgOp|)*L+obk&?Yj0=#xcfurdDHb^Ar=(qBmtM_2x;oVOF36W3PAW9B-O@ws>kNlScASw|T~kJ6f%bD09pL-s_Tj(bu764Z*=qL!ST9D5!u?|#AA{MUsm z3d)Lf1#^lj3bz++C_G;Dz3^@kyGT_uu?SxLSCOzdRKzRJEe014EY=mREPh*bv-oY1 zzW76ty?B4owPIXRr{eX6pNiB4V~V;Jv=`pX-&)uypHf(xcetQuUNZl2?)-dlt}5?L z&bT~E&V$_JVqmUNJSk_lXs~#QaJg_6KbK#_y~}~J39L7a9rRT+3bmZ_fMg{uAY8%c z<9@+f&^YuLynr<~^n!Yy;YVO?3 zXzA9nPI5s4mkDL(<#*(R6?_F)bwK%24O4H{F4qjvf6=1bnEDUxBMr+;%iGfCTkUhL z0pknXFf-SovHa^iVf)oR(~;xt>5}`fo@0SG-ch0X{&*M`JQ6(@62!|Q&k~QKBT}XD zy7a|_C<{-`0sNjm0ZeB80*wM}0UrmJL0UooL8y>L(4kN`bG79>N7v?{n+(m(T`0( zp8Vj|AE+;Dc-SzxG19obX+ZPe&Bt5bwKPeT5{S$t?J7^mHn##>WeSj@R7FuPQ1?`A z(=1dU)?U`s>hL;>;g)`LTi-TUyQF=BX^rW+SzxhQK3KW7pX@#CX2)p9Cf9T))-%)f zyLYNv@0;uy5*XvX8|>`!g;0J*8yd0Bz|o0H47F2W5Hy4`!AEm6=mOX7(+x zPu2^ZmZgJMXFGs)XGeqfXIFvtW>124WS@b~WMiOwz&>yv;8Ms=(0b?(@G)2g^ey}x z%!NQBAgKLFAv%p3f?0u?fd$~^s|5Ovn(o;Cm3C;06#Hu^sUrFe2OyGy!`Fg}_`! zLebX{F62A-XM_iK7hVWE4_gI21bqoP2uXlXfXl$QL6bpHkOwFSb^!@NZJ?*13*f2X z%@8DH74!>aA?zUZ7x;MCL<9#u87YC!MQuUMM)Qz^Fc**{ECKZ$yB9SE_aAC1?jj0= z>w!9j{SQgQc0w+~oJ8D0d*LcnH9U#j0HYw^Lpg{zLymM?wY;LqV zdN9I_JP4!1?vOY%KQuFl4!sUM3FZW52T%D?L8kv^;GA!Kfaya7ZhCL{yLr3$|MN(F zzj)U8s2;LU?!N53?H=oGbno)c^Yriy@mBZ;`f37G{1bx50_#HR;JI)?_)cVXf(<}yh0jIaL~KK~An&8CC_5&Nrs6nQ9G-}~jlYi5;o~?Xz8~I!--tKkkKjMy z&)|>XPvJ-7ci}ns4fqUhEk2C<9UsM=z`Jl~@G{&H{Ab)8{GT`nPJpYzt-`LwzQKIN zKrvLzF!U7kKGY7>1LRqx9B~H$K)i&r;8IvVECm%oMNlZDHzWy~08RnNfgS=xKvDK} z_Hep;CXoC$)jL5+9*ND1Ya-X91z~;oSg;5m=l zZ`ci1gbi!yYUyMeVw%|gOZz&*Cc{#@>zP4bST9eoXlB`}aUyS>54ptzUb8J^o+t%c?JTKCk?|{nMOJvp#nJIOqfI zL;v^5cVpf~->!R$d0YF2{s#1>r1qEE%dZP-_rD%q`}pq`|LFXH{M7Jq&gTxF3IDZyiG02F z74dy%ouK~LA6*)!G)!(D-t@bqOUor$ne$M~usIe<`@k6;rb@s9Gy#hTr1Lk*DTcJYu>2;R*Tgo>LaRG zs+e-JYOXS%)G8J#M=Ml{Z>=*F16pmZ&*Z0D=gD8Uo{;xaz*^;sJ+14M6$*jMqj;ve zuAHi#qvC0>YPIHX^&i^NnqfMCHl(|%{YO7cx7yII>)5taPj26D_{9irkDFc?uUL+n zN86TK{q|D(dS|mE<(lW(@ew{BE7!>$5^fdT2ObMf+OC$YbpxE8GCH`|V zmMl-RGVE+m00uZ86ayWEs30d{_hI`G%MnXaY*Z)Advpl98C!?%j=xH5C+sE9C(Wnc zB1>sYDEH`ZDI*wO$|pu16~UZI9l<RfGjl#WHT@`aD5X!gB$?@|*T;}?Lv(%&9Q8(* zMjE3P5l6Ht!jAQf{1h7$ITD)_c@sMr3B>-26vSUe2E{{>G4aaiFY%?(J@HS`x_C(p zmpC39o?ypcBy92R$+pCylq-2BolgCi0c8}~NX80qWgS2j;5+C!@G|&M&{oJ6@M!2P z2n^N*dL8C~3gCNThvC1#2O&-(enA3IH&ENrUf|2%I`(@WkpOwEZdsOkQ{90vw#qU*; z%26FGRlJUl4yumoj$1o@?j){0T>Z7WW9J#2C7msu7k63G1=$tU^;Oq{UFX%fyE<#C zYsPe2TXVeIm71sBUefHPdh{_$5x&ykCgQ&eNdt-`n_mn!7l~uyuo=*Vn)ti!Y1Jr z{$Bo5?ojS4wut?bDPZ2GSJE$1`%@p2XOq*!W5n5n?|3PWimS$M#H>Pt(RYylAc2T! zh^;UNyd$&@N`|b2L_sy+W*`!D8gK`&I=d;`lnG_znM7t-wqMqs{ggcbm{RH0y9fIfp8-V-;ej3$`U}1hi-N#1I z1^5lv%LE^;hV%=ep8SE>n_5KPO1n;3N9WNdFxJz1F#lzgvZTxswvvVBTw?FzRB*m= zu5sYp2xl0#i2EmZJlDxx&aL7d;jZER;Qq^_^SrzjyuN%L??E}XxZYgk|C&ly>2oHm3qk$Q<#PYw|}l7`~X5FTN{_yWvq>@Ac6 zJq|e*<$*6lh+rgG8oUAg0@wmrn61u;)6bI5#L)Pwm^yMMvM9791P*Wm7rkG+RqoyH zuZ}6s1-5~9v}K_6t#Q8jP1`pk-!P~BjgHo~Q`@dzrFpMgrM{@0tJ9R8LeJGyv@r-BsNo zLodUec70p1X{)itEH*E*YAhK0ZQD7=aYu#glB>b}!86`#^~rsx;D!J`oD*_HUWBj5 z`bB#tKF4a4Jre!X50Xt8L3%3SaK;HtXP1H(0F96q&@ZsAkT&>D=mKOV+=04=$idVi zJ(wM+HCP_ngFT3zh0~&6;qoz7++2(YcMW5}g)pCRL$MEW|6*U^7`Rus`M5{854exG zJp6N9M|^)=Gj1!kBkmGr5%xZM7v>%6Cfb5*Kw*$RWHBNSIUYU}u?4mleg*n3tPau! zb%LqTIA}5i1iAsn0@WY}zya)$^#cxOC;)SMWVU1ax6B`@v*}dwUTRSCQF3GAapG3| zO}sJoJ!Xq4qlt(nk`C*_RpEkgBxDMehfSd)VNw_!SsXqR`4$F5nUQtTm61^NLu5n@ z9NizA9(@yQjp}0)Vt{x{tV_IKe0}^@{Bt}v(K&H1@j1~uIXCG_a#A-^ZK>($zte)u zFBx}+lYN|R$u0-%08oHEffs>kU?u1is2y}4d;xqNG9I!CiiY-peSrpGt6)3fF&Ggs zAO09&fKNqEMi5Yqi0`Oz$fal#5`tO&KMw7|yRF7y0C21u1+`7fuN< zu9wcK?q=5kkIQ||3wUq(;(QPN+5QKCet}oPIl-%;A)$_tJaioz0@%-MyI-CMehF8M};0N#>xE=loCq+KNDd#m<-K=yF+{7HPC(dBUA~~!cmbe z;j0k}+y!wHZbTGBRv;}A9O@S0EUFKZj#eOdqqm}v7zTPX<}2EPxrkYX-HNqf=ip}I zrr}+Xbse}^dGddjQ8~O%o~gotfS16 z>@BQSoT2O#kjF{mb_ehA8n`0_m-%?n1Yv0uCTgeTx;R^!8*PqhmY$9~6)R5Y6@NZa zoq$f>nlvv3ky4v_CUs(Zzx0i$7BpI8cOM01{kQ9~NIdMT!kA$m<1LJ=t z%#YK=pN>Vve~L+nYmrWf4M*>e$&9`zT`ai~eO~-X@;2(5xLjn5G6=gwB?|9~?g~KB zNWlRiQ9u;7@DB>U^U;E*{Db^Ud?x=e{{(L#pTmpiU*Hz;K<;4P1+baR2Ip}1aok`& zrv&W7QGk0m5m3%aP5Z)3#7k(Cg6IKYzgj!*Z5G(p2j1@f;b{Fjw4iohf4iRC5eMBxnmIy7Rh+>3B z;V5B~aKF$hyd$KFP75L7L}8H-5bhCv5lDqA1zQDif~o2xI5qAo4jWbmO$e+FZ>WO^W)B zI$2erN>KWgsfr|JPWwnjo_tb!x3-z`>8<6jxtlx4W;T0UQkpTcyyi}_ zY0Z0Ox0=H;ck}Vqf);Vx)|P8+O)V(-IN3nCT=uVgR;yCp(%P?mWZQ@K|Jp=~O!;{Q zwSBL0n&P#pMOmYsrnYICHF(_s9Y_CE-`yZGt~MSv9WlAgcg$m~m6nG#z!tX8wU2ZD zaJ+BJ{fv(DGgJKx{m*Da9krw5q6CP18~m>1d9=h z2nzWCVL{GCmZ6BKZRjPa5c)A{JjRbYhUtL5iJ6SPg;|Ndh}nuhg1L&`iD^Wy!iX_b zG21Z%FfvRQ2Eg($Sy&3D0Na56iAhGw&`i{4R4HN~a(QGZf(_FnN~k3KEpz}n790^8 z8Q=u@eyiW)ZSqxmK6-zQ9CiM+?{e5|%j{{k<+kP41J?VNCl;q! zZXRz=G5<21GIci*OfQYAjWlDkVTz%r;i>+rKBB|uJL$ISrfVJAwVH9-1L`}PN2*43 zi!!1LDpFNs#Uy21`&LB{`N{S%ZI|RzThFzPmF;cq(lTErX&%r*Y)WnpHZYq!^_WJu z4&5NClhlu@omh9i=47p*`d3ZAYJAPTswvfQWl7c0$^lgmE527sDtcC4D!*4DF2`5Y zmVGQ&l{w1$m9MR+Di>BRuJ~Jtu3TPqx{^>Wt-4(OsVcE%Y4yjN_?j`bvKnRW$=U;T zgX{X#rQ}I0Nnk zeTwV~bCAE_IVe8j5qdMyhG{`%;5uTK;16Lh5WeB=5nJ%rNfm_4z$4-@@?p|!${HY< zI*Y8Qj-s5Pb)x3c=`=gtM0>~hLEphV%NWI4!{oDPus*S)*?F8R?6n*#`ywZqQ_Y#f zp@Mff*`S;=0%U@-!NK4ta0{3PUIw|~6A%Hu1AUwd5Cb|u8i?g`Kr%NTBy)R$0dPK8 z1)c-%fnUJgU^Vy;*Z?jCo4~1{5$pp3+>T%zw*ZXe=7Mx?3g`mUz|SBFOa<3-;y8QR zM%FIYP39WrV8%EGl0J;yOr1dcOc_DFN$yQK1av1aB;^5viP5B<1R`-D9!Hpu%>k!x`*iDwzwpR0h)@oD0(q!ytAsN@02N`agw&=@^&vYbXSUbY7Mte;k(fD;c zHJx;R^+fG7^*YTx)g^UA*`nI6?5!dz?9 zowg}$J=+wm4Xq1X$G6tV-pi6@G}*P5D=l+dPPeRR`P_1;McY!=k|IONcFOw6Dr6UB zf>vbfveq@Ne_GY88Erk=4z^uvGqy41v*c&xzvZI#{_P~iXvI6FMcJ7SXJj6|!;yui-0y?6F>XxxijTJJ7Tz5kG}A$UEYg}#U6us+;~$VMR1FHj?}L$P=8 zUVM=Fkko@bnX-XOpns;nW&*4coH3kw?onn?(c^coV?9>&8rA)1&-$(ldUWX`>b||;OV{F#L%W3Yn+p>1R&*Mi6X~!k z>vFy-qif!b^p`n?)Qs%&DYDG${$U2RUAbA|J;6X{HJo_Dl3Rys6}(~fMX z+Ht@+)v0m1odaBlUDsVgcgS_dy~u5J``rbetDfVYJa4P#t+%(go9~VHy$|8b_0RS} z{;~d^!OwyE(D2Z~a1iPr`2Z7;+Y!~Mp{R2h5Yr3y7h8$nh@VC5NJIbu(oOOsa$o9j zs-9Luo5_gKmoeWkxGX;NU)C_DhqahFioK8dlzoXwGk9c`f~DaT0O9xT0s&~{v{p)`Vj2IdfX7gFziv>PxN!lFw_@R z3*rl6O5`Q{HGBz*g?0z`1lRjp{Rg~VeLvlwJp){8-E8Mt7t3+q8E>~by4wmI)2%z~ zJ1p;P*Ud(2iAicDo2FT&8qb(-8@`*|dX2HG-ej1iYt!G+;`B?k4|Gqo4|O74iSC%r zr$gv7^;`8T^>jm_{6Lj8pEq0D~ z|Klujr#jo+-yIF^p^oS7GW!zuRC|mYwkcgNZI@kZZDU=%YyubGrgkE1kDVUtL1)PN zuM=at?lf4JIRCYlIxtqJeXa$xSD1&}Mwo9|D@Z^=h z^tr}6x?2XiPGZ=teW>?oI_g(wzUh?e$+}5uzt*lguRW{E)8?qYYU-2&HM^8e>Kx@# zwM`LHeNvoNol#_|7AsyVV-;PL{&t>nf?~h2uX2%Uu4;~Yn|h4qlxD2<^Un&53;Yhe4fG0fgU^Hi1i{ds;I@z;R2`ZW zqC!VQ{h-&OwNPE?Jmd(yhXSF$5Dqdy6QG&lFcbsVhS8DFFcxt?5`(;i=!&|9oP_>{ z+KX|ZKVXxvR@_XSm~ax`kNAQxlk}eW5Ad0^lKdN(OsOXKqc&4I(%PtedLzwEucS9H zelgxLpD}N-&a$qv53$d4)^qlQQ^0xLzT7BY8n28;=bz?j;3 z946c=oGknyTq4AZHVAV>JA@-dM}+f4$AsHN2ZT37hlM4gLqfM`k1#fBpRh~RKSE(t zXW=)IThLc@M({!?5=ezN_(uc@d^`UhZ$3YdXXaIK=kXSDWn3(GF84X;1jm9$Koh42 zc%DN6=W}piUrsF8k<%0G#+eBA=Bx$>b2fwHIg7wioXKEM&ScX{~E8F*ORw^SIV_;dvX_Y--33q zE4UT>#UX*yITttqb{uCn`xTqS?#;f(s$&gcEn<~08<|Wdl__S#GsN^1dIl|-){&Y@ z&8H+%I+CNvod6<`OOg@uiEju!2v6{1@V9ZxaYwMbv5PS~F?r~|=mz8mjz1uvO-8grF>#noLG14*J-eU7w$63!=UYjGPzs3aP zQ+<~Hs5VZ!NG(+NRVFDj+cVqK+7jD3$+)u7&FJQJjjo1U_4>N%T3-!Zom3rNHL7w@ z#g+2Lvdv}EGF54N>Cw`+rISjpmv$<>SejmXvNW;ubSYSRtyEHaqqI-yz0&EWA4@ls z%1e)y63Vuh3d**W_9_dNLS>!G=T+D%7*$18f2vQ`oT{B)H@!ZiA+9mhsBNljKH73! zCTd;McB?I}UD95wIIY;GqNr$^?doe-GQee+V8Ao`9x8)8USA5+Vj+LlRMI(J#?2FhR@@Y#93nXT@E>>+#zO zX2L9@ia3DOK#B+CfD_P=|BzLbI}`wwF5hh#_ z7DD6e`v!a^!CLl-AX4XId;WWz)2l&W+!juGGgj0(IN!*VQ!CwpAt945(aN^{Kp~ zVsII^yzy_p(j$LVe{3bg|3pe+|8)K%`LprQ&_Cb)?EXXlTl8n{-^9Of{vQ7e|84u* zvoxo4OXBTgtAMRh3c8mz94jpIPyu;zZ@|%GxSSwV-B5&Dz>4wbgal zdVa&ahB1vV8@DvMn$9;%Ti&z`mVJ|LZ2i!Bz3o+7k^F_cuKhuKnc|A#sq%tyyXv&6 zzj~{hp_!y<)MRTfXkqOlU4^cP{*fMI*kkx+m~Gr`>~891Vw-Vhhxw2Bfn|n;V!dnW zY<*`LZ~bOjWNox;vHC4%tclj2))`iU?Si$yR%Kmb^H|^50Gq>>Y)i2Zw=K1AwB4~+ z*h2QX_QQ@CM+c|FVRIHbpSpIrCc87;u=~6Fxo5m*u2=72`PO^C_}IP~{1vDYd2)_>Z38UaO;V$sw z@Lbpz-UIiAufQkaXK*Y036@4G;iVBj{5g^xK_XT}h9GW6<=9<%nPehfG8CLM}k8 zMHV8SAk&Z<!9yV^5DCr-EDuKfU4xK+TChvtRPaVX6HE;b4t)*24vh=t zL8{OTXdM(47KJZ`tHW{dF8C>&9qApBMJgk!5%UokBo=uJxgAMEnULF1OHdkA1F8=? z2Ym;99*x2{(8DnOFqbg5F*b|~(+%4NyAOL1`wIIV`vnVQOR;@$)!5az_t+h{f3d4@ zi?O3{qp+#Cepn2yGggi5jIG7?!j@pWVT-W6u#d3Av750ou#>UJu|u(+v7NA5>^AHf z+;*H758+P}&l9@=-2e!) zkeicRne!p%c+S|IIXTwsK{>~>`{azvF353YRb&&gK4(457?3GQ|C_!)b!3_?xin>5 z(xBv~ginb};>8J$*j;h^V)QYI(!tWVk_VDuV!GHMIxN~K1cV{}7XAWWGq(y%0!MRJ zvCCNBnQ2TiV>^8at(bb5;vpl+oq?sK{lqrHFZ>8R2KOF20uzt9kGhK@BD0aJBX=TA zVGf)FU4)JX#UYpfRA9D`>TmH}@Xm7=co;5~tIn~(dCHD*jIDKE$X@_giYFL_u>Pl6f>X?$FoT6YTI<>Rg#d3ih)h24w$;7SlmMq!# z=Jb|*O&yzGHCmg#G%jtP)s)jRs+lWW*8;X)mcgyBTkG0t+8)YP@{R50_OS|rva>Q; zm7&U2_f_}QOwuH1=V-;c*Shoi-G)QP*QR@Bz*=UVWOv%{Iaw~hJHeCYi}no(ume*= z$k5QR5h{r2;4GvXfkQW-8ZfPxN4V!W0l|+4h^>S&;=jbrq-2r^s3sKwn}7*qELlT7 zN8U!spm3;#lv~uL)LdFBt&*mrt*5`B^B6lB?-(PPE11cw4lFF2#MZH0>^hE|Qw%nO z54mRUQJ#@EhhN0+D7Y$!2zCiS3CD{LiaJD1h!TnUVw>16u8?>oHPKLXgOnFj9n&ke zHg;xQW8B{Oy7=1(PLGXm)kGi>R1G8F0iGk&Gd$@r8$ zJmXh-LdMhd&*{U`Q_}xSJCTM=Gp6=T?Vh?W<#NiKWNR`qxm)t^q^n8y5>ZLm#JPzx z68 zi^6TkPQsqYJjVP+htU{xCv*qYYSdEX9pn?lTZB3CJ<>N~hb!R`@HV(PoDZ)Go5PIo z)$m8Cdw34?9ilc8X6sPg&u`QLL}%pv;<;?8=%$UY2l`@F`N&t zg!jThSO@QmjEis(-y+Wt-4R`pZxN4>A|!y?j9iZTi>yOYQEBLDsO9JvsE=qIIu^4E zeF9@cv#|eQo?)Gsp14`q_qZR}M0_r8Fa91*foI_R5Vqql5KsgK;Rqp)$RJK2o+R!e zVn~I=`6MLiFR24b3QQ&q0oIa60sBd#fMcY|z&X+i;3ereP)m9X_(;uw7;pfC01vPc zXag<-?|^&2J)jtP0{jJb0b_wS(oj-8kwAPzc#5BkABRKY7}%?r67(?iCKMah0cl29 zA|;Ur@Jsk`_;z?BbOM?j+7s#=TplC^vI6gX?Y^nrUEYA3={e@Q?MifZa=x>d+lScJ z*i06-^|<+|xxZ7)jIV( zHC|JvCTk!yQNz#xntTmjGeyJDtk9%rPH4Jo{?m-p{L;+PRBMK6zH8z%mo$)im8Mob zO7mHrqj{iaXilol>fP!(^&0h0^$PV}^=kEU^#XOKdb&ES8l(QB%2wY{Wvd%iht#Lk zX_|c+ShHTM)6UT;brbY%{bU2eINca9PB&Rh6U?pVE|zKw&st%Htlw$v-u`>f}UXTSHocZTn)FVp|h@9>740CkE`yUC2GnyTJR)|IGIa+5|ZwgK%inYY`~!8}&k56qP2)6t9!q65o;J zNlGQZB_YX(=yB11qklvvOQq6f(#_IG(n_gO%8lt6vohvl%%hmwF^gleV?IfjNlT(H zM&Fe@mmC&f5g&{?7PVJ&UUW!!UU*D!La>p)mp_}gh&P5ij5`3#11EBXoYiaudpzq4 z6Jm^G?5A640@_h(DJ6w+i2NQH0OXN4q)&tf!W8^&JOy9kCPf77;*nK^{c@K}t{yQ14M_^jP#cv>V+6a~fmABwPbMsGkL zL7P!1^f1&^)P3YTBpFFZZbhs?L?LJhZ{$xz9XS$dip-2OM+QXdBE2KuB10m@k*Se} z$ohyWvM&;d9F33=7bA4U

AyG$KJXL~w|b$cM=K$j}HYQVgGlrLZ}?EL;$N0PTY0 zp^^|j#16#;Cj|!v&IOkGoBXGJ68~TCEMLHL-YfF__Kb1|-Pc^5-ALC#*HUM@v))nQ z?BO`(cwkrB+4dgxBerKYv8|)+tF^*9$GX}Yu_Rk>Smc%gmYbG5i_y|-LE2($m+TMi ztDN(li``?~6TH2=dHzH{GDr+Q4AqAEg%5|VaG%H#gbx9t?x3!tCt7y zBIFVCNn=QhfTh4D@;dS&${NZb>KbY;Z5d5UA4MlJav6RGk7;JQneD84Ru#LL{e*Lw zvjdz5&f9eEX82k$)ZF@HUOv0#EAMwlhk3NfPpM6IHIQSYOsiT8-JBoib?NqY2= zXlrzu^r5s!nh~Rx!qPrcu~ZtZk+h374QFf50lCJ{ufeO-25{mR6u`|(3I7G-GG~f^6x%g(> zL>v`&2HOj(!)(D!$2>>Zq3x&%=s~Dv)H-AU$weGRbccVyy`UnfbMRa+(Lc-2@+Nz| zF345qsB?U^J+ZyC9JQP=Ei$b!6mxn^H=RMoD^ZWX7>r-Te3WpZg``lD1#7e$jru_%ZT(!gtMA{5Qnc z5ns0!mlX4gCl-G$(iP1qT3+NSbQi8I+*IiQf-l_j<@y)kOZ=DppD%vqeCB_C@oC?u z4xd~fjUNpk$9{VJY3XO{=g(iJ7RDFJitZOr{F?Z!^4qQNgMUbV7X3W=tJiOGNlVGz zKO6r#{))?1msONgD*mawSIMhhQH`lRR-08{SHH4R-1Mn=aSOHeW$O&NQ~p}fQHfBm zSLbPOYRBt8=~o&*8+VxRn)g}HT94RI+0Q!9I$yb8x{Y3oH#U$N=oeZM8XLYBo)Bq_ zj7OqSQ_*b9Tr3Z_2rnkAB!Z+J0G51$Vy5h)y`?Q=tYHXQMrIQGA?qMp$@<6!*fMr1 zyPe&Otzl1RSF@M0PqF8)2eOB;L#!0`XBLxvgXLqLX8mEEXMJK_VO?Y8v+9@uMgaq+ zCDR^JEad*cIg*ypg>V69!%o4xLQ7FwkdDZJ$osH3yg5V+^$wr{XdlMc;AVP0I8m-4 z_H*_P7L8?(G2XOKH&cI9eN=Nv@j`jFtx|rn#VxzmC~5w$Zg7LDdUI_;<(H~;W#o#I zKch;)-*-#i|5*Dg?tA!0r*DDpr@xB6)4opocB}a1*OA5SujJx=#T7+_;!{P}ibfQT zE(#T5i_R53FH9|5Q26?bq_FE3)0gBglEUv_$_hUfJ}4?HI#nDhKL0iL+x>6dzQ6rG z^+&^xhF!eeM(=K=9K*{iz?Tbvnr64oJvs@v#Mh? zxw?N1r)GF_9>Wei1Q+kWC^H%SnyI5x_3eUqB2DBo_f+$s@`AD23z(N-AYG^(LjADxuD! zJ*OIJ18IxsRWuuYI(;3(P7gCyF*Y()j5cO3W^dNN%zG?36Twbq&0#NRwX?soR&ui0 zAb5}c35?^6;J)G1bH{>%cz?iyJU6(B*9>;$odh9nK6sN`#>wK2<}`w3?A_onb`)5} z`ovkln#M_B5jcM4Pj(S=1$!$K!4@-DvvxBMF(17``N4y5N6SofA3(LdsFiog1>LF5%T#R^y@WOeK-0-4s|ImTZ(!k@uU7yUS z_oRAL+$Y_uonB|XeTt)tt;}}X(%nihzcTMKW|?&Qdxmkk9KA+E*Zo#c)F!BPnqR6z znpLVyO`^)ImMbgN7nGIi=}NsiM@iHGN{&XUKxlp`+SFeapVTFa>*_YeakWEnP>ojZ zQzt67sfQ>RsV6E2sYffL)mci5%C2~)x~$l*>Z%y4vb2j;_u93}@$Ijal=iL4zw-Xd z=koDNQF~ZPQc%<)WtOIsYN7TY^-JAXjYBWdw+*r!v2U@K zI4W#ZSH5Gs`?0goGsL~fhw=6deDpd2Iml+($^($G(p_8FN6oLApA6NAw!WEy)UTjrgA^K)hcxFzT*wkElZMP-y4h z5LkID`K8=+-YKvS?8TYSQLx;sJnT8K8nDbh*e5TGZCK!!+Fh)I^#4ehPi-U$IRv&Vp%xd*{8t>JC6H{bB((MO|I}4RHF0&x>NXF8P()7FO{B%p&p0p8Z!PHx+lGGz9ACnQuk;H<; z*n|o31#z=uhs7+DE{WbJIVpY?RV>m8J%UU@0)IVk4EH&>i=$`1V@X*MV*8V3D{7n9bPX7z9>>{ui?VEx=Tvj-u01M${=}Hxw0l3wZ&NfJ{U@M?8;oM)Z#S zjmY4^k-6|QSQD1O`@#>yL&HPD!Z124gBqdzP$85KU58Ymt<-*AwiA59J`b*E_XCsIWU!H? z<6L0<;7ny*;jmfLIe(d0&PnET_B7@~wv;Jidl-!@Gvf}+%~;BEF+i4r@sRn8k;%Ny zc)?i8=*;NJD5i@UxpX!C1nmO7ojQ&lM+NBfDUWFXk-O9Ans-nnkT79i;+* zgSsEsOp7H8>3_%-^yQSZ3<`A>^A&X*Yc{QbO{4QT@8~wpQpQ&>m3fTY&g{)Q$g1YW zvvc{^*mL+k_BQ??&P)DT4j`!IY!(nei=ZPoUbp~!DtrKXgkdmSG?Keebb)(ARLa$e z+PKkCT5jJcB5zaFa9&H)4PGyCn0HzH4<8|M@+U}E2~J4>VZEe8m>j)bv@V(#^(*>O zRE#uJyiBn~r{tb! zh@^w4Q2bLEB_1YR5mhPpDOx7T6~zf23P11zf&h;wuy7$>8Q8>qz&Q@?VCQh=v$U)! z%+t&cjBEyxUPFVa%c)L^oBWf!4Y&kgNZUyV3CD>m@X^EwE{_<&ttKYoUlHfxeZ)uj zd{O|vfi#Nni}aMh0qDeOzz*U~z(Q;T`jdp@R4=5z>0UF5<0R{OT zpdo(&6y#q(9k~stB}V`)SwhB8`jdH-y<`EUo=l;{Q0(NHluGg?$_w%r$};jJiVs*o zSqq>k9@0Vb3KEm-BHjb`5jz7c;vdp!!YmS*;3MwApCy9$VZ=8$HgO`(Mj+#U5YAvz z2uCpv92>m|yBArBRz>1bx$t_#C8!a`hB}2u22O^K`E-F&PnMtKUg}-%yzf@qZO&n~ z!HyS}+cvjJZXp^yCWhW>;Awy9$m&BHNHJRFlBX+dt!%l!h1MF=9NjXnu|w0-`auou z+WB=IYj)Hut2$9tQQ@m7DW_M=EWcfzUp}s!T+T0VEi;u}D0@^ks%%Lax2#v0t~9Q! zu9Q<|E)|p!%c9FN%D81q%G{;r%YKx8EBjsAv0PeaDt}t`uwqjAmdb>Rsa4^MjOwqI z_?k6Uz8X=rvG#j)Yu)yms``Po?;ALEHySN+Olt|-Q}H7f45WhkMxOUiC? zifWU5tEyVAQDwFFRqt)Tp;opdHA59MG~X1@HT{(`%{QepgGSw(;mU^l7 zlzNdisLt08)2KA3HM=$Cngoqe<5c@KGBs8ES{DmiQm8PTewx&oiLeocKQ~zndpq|m*U5#sRR+Y#Ps~*XxsxHa9s!qsL zR0rfz)owXgbwduS+T<9OR(?&nUM^F-YBRLAw3_9Z)<|2nETL^-%Y@cv&8K9Mrlyu5 zO<>EJ#zD>UhP_Q`4P}kH>luy8x_%8~>Q2^U>$2;~b-eoSb)5P&b#e9o)eWjg)o-aE zSzla#yB=)7H_UC=+HkeO(@@ZvUhhs?1j*)z{S@HGa)XT~8g?uuuQQs4(<04>o<8C`7Dar}MnpRNFLG_0G{h|@_b;$PAT zQXin0Q~{&|eaYj2#pF0(B)OIpM?Ofh0tKXpfSx!JxI;9N7893|x)E)}RN^XPIx$4Z zC7%7CzzOlhDtwr*6kkYi;Ib z{kZO^$v8BMf-6Q|!OlR&Vzr2!nDL0K=xdQXsB-u}q$m6p!3!5h`a%!jU7>y9;^1Z| z7}yfZ56lg2_YV%#`v&=we8YURywkmVJ^MYc-EZ7Tx7)SKHQE()zIFcVVr=Q~@- zEdv9*ySzODqTrPKq_1PMV=l%q zVi(1mVlxuz#(mmYmbX|7kxWdkb z&gsrmj+c&?cGzBK8*R5*i*0OcZ`(jit@WgNqqWAwu|}J!ENw=L% z1Z$gfijC_&VejU7<>=@A?9B6hbn*OO-EM!Qr#9g976pm^Yau~kF%%Q*9Zn1lgnL5& zM7D+>Bbs3^awsAX{S`S5lZc*+-Hw@xE5YXB?Klh}gl{BJiT8-fg z22kEpHPm7BPjnyS0`n?sCA$Zw2dDvI?qS|5euiMSP$5J_-H19So+u$lAYQI?#PoRK;tWoFu<)UD}9)2?K^OaG9m$SBXkX4PkBWY_16 z&iRu&Gxv1fqP$-DyYij+hda#fAnjPt0oU;bvPtgShM ztcTfiGF!49XGCXN(idh*(jR1WPqU@ZNbQz>DCJPvjpSdcHVC@nzCEalz=zv7+eTG2JCL=~yvNx+IDpeM;0-QY@S-h6Ssl1`Cdh z4)GrgKk=#rMlMb&GN3Qj`9b}b8T#8 zuywy;RVzzDZT;2$UAC|Nq^xKAKQc=D6q!rDTn4mP$=0`TY{e=%x9w8ky7=nVL-1E6p9%Xf0bE&~8%S(Am|!_4721`a;bZ16BLQFkaiq zcwYO(sL=K`CFq(=OLaTVCAxTvME}V$Mc>tWNq@nr(#x$G2A1uVp^J@YTx)x7EVGR? zW!mkg^Y&wAwj;}O#ZhHRcdoR4bBb&eT=lkA*9QA|H^EWnzT@cWDR92>{BkCF=eag| zKf5-2FS)Y4gIq5>GAG{C%Q@J6#c|T*x3@co*b|-iZ3`SM+iUv~E7i`hF0q}m$gBd( zMC(;^kwsumwmdZ5Fb^;R=6d4_({f|Ak#Ed2Rv4ZbE*ttA))*@Fy$zG}EQ3>L*6-2% z(u;L3^flV2`n}pG`X1UseU8?vpRDa-xTM`{@M@KY#kyWbt?pmr487V|uFp1gHq0~K zHyk(7jPFg`j0O|gBsE_$jW#EkPn#`friE^;vn;p$wd(Cvwgt{+yWE9#4)OGJJ@TG% z^Zb6#g}_W7IrQGY2I2I+2!e}oA~l#M)Klym^i13- zj2E{Adj{VDS3q#%DhQA93y2E|Zel8N0m(?zkZ^qujrC@vD7GN5%Z_w+oWHb>w4|NXHh8%_2fy~A9 zM#f+gkkJ?#l8Q!_7&+oTrV;TT(}4JesYO&^su69NdPEE6 zA>tCIGa?D|Dsl-O9l@j5!0S+7!_7!mI1f1<+K<>9`Wkr>G{Wit5zY%p!zcYiAhU04 zsJriD@S}Hd@RYY>@Sb;E@UQn=(B<_6xxU$<0$*cjvabWQ)wcH81z`I?~uzYSXJ zCx);4Md4TetZ=b^dbrMiKOFFr;DW$5_&^{8>jE1hi-WodGSnY&CUgOj0;v$ikQ6yI zJR12kyboCb|3uz~u_!`h3~EoL0@Vqz0nI}W!7x$Xu_AOXE(Vj1&%|~njKOs#ZpUYm z9ud-k24Wo9L1IyGWC}Hdf}u^Ox@kvf5M54BW{zggWmU1RvWIgTI3GbYm(JtyX7JPa z{|N>P0MUHm?5Go>kK(#0PIQW7j&xu2?HHBR5}O89@<`$6w5S$VGwu!Gmlg8_zn$+Ry05 zMAK{N%c#R>f5@LHxj+{A8SyD8pU{n1jBCJ8#csic(ec1$__R3!Mur32yWc4~+D6^e1?ue2_cnDRcdF-*Nuy+U}U=oMa#37+~vV?{4jE zE3o8Rdz(`%156zA03*pX(~xVtsy|_f=tzd;x&!)FEkZw5yGvKEA?p@uj%wK&jP{Ls zk*2fyow`uPQg>DDS3OX=l!?kQ%2EYZsZuOa7AlvjB63 z*2TR~I2!jNVO!jmgi&!F6O!XR@wB*?@y^(P;%j3wlD#_D6X z#eRs%jolKXi^+|-5#y9jh|#b3)Ez>5Q2;2ZXN&KOoFc7S1F9;3fz zaAb`QJa)H9z-l_etoF|WO^S74C z>SeoHUbb{;-qVa~8rNiQ$ZRy%6B+__^!f#LZ|V-#QR*}5Ki2!|H#Pie$Z5RMSkbty zX=Kx&=E5dMOLTKf%gN@~vV@kst-o4Ew;hlb$QQQ?+Pk!Q+Xu)8DVd6a>Q$;!TC>Kg zpR1p0(i)2_i!DUE$=267+qu$R=-%&*c#rt=1G|FDLu;Yy;pK2iWGnU30wK8?PE zxqvx~qrT(NGr;enmsKrz%O+s5r+e>SpVd+!o*XRhwXvSv-$lSneXJ)dlv&^i4 z>_==fdn#u)r=IhJ^-u?G!zRx{c;0ccESfY~*~>1)!eLhjbKgBaFkH!zW;e;2dZo z_8aOu`YCcR>KbAk@_eK#;vU>L@+CYOE{EoZt3&gllHlUd@4)h4yMIN1>|gF5;al&! z;yvSS^L+B~JuU7LZl`Od3vwQDdL1_$diyzhyY0FSYx7%oTF+QJTbEb?>nf|$a?mQX zJhpzev{-LiBGz4&Xxk*qFxvpjD%)VoLE8k&3EN`JLEC!EPTK*?zqVq_SDVxtWj|*< zZwGBDj&rtBhtR&p`PeRW4Rw5T1s%)WFPuEjYFD|ZoBO1f;2Gzu^^p8$y#M)o`}zeM ze6It;{lef2|Mnm+P!ik`2n6i`Ftk3H8VUvbhUSN6gbG8OLLBINXchD;^b@j#B2YBc zA-n+E9{vqAg$u%?;78#mSPGAh9EXb|N;m&Oo4A31&r0^&vs)KryUPSN5Ji%13maq?V z`h#7$alBR@Meu(e+=qV?h2jA4yCip)q#4a@OIJG6%nm5)fs`%q;IW4VnSxAT1r*Ab z@yaTdA_%mWr6Q#a0TB?{)q;XR_ima?a@m*U?y~Rq`TV|r$amuaJTZ`XC;6k~VX5UQ zI}|rkze{UUTu*1G|DN$d#{0|_nP}FYEHe9Sc0_qb>B#wC&O6mM)ot}!b-Cta%^vMC z?JnIUUB13Ww_h*RpVz1BkL%O*|J4ihv-A$#ApHwnvi@tGOutYE>j&ycolNJ{A-XOd zLsze3=zh~Nbv3&8+QGUa?QZRA%{9$7b+dZ6icsy$k*dB>X69_j7ArSr`Ln*t?9SYp z@g!qu`t9^dX+NgrDk>C`)Getk$+0O}Nl=n8p)BE}xGwoaX}nY%J`hzzGQv-SzXo>suTySv8>uCX|sv6Lt!h7el`9`PkX z`VJBtqK?QUUJ@S>eZ&O_q0QChX?N-t zYyZ)$)85g&(-^de)zzA4PKJ7+@?g%DEJrpsb3)cv>17!~MMGLiDpx@!PfyvA^g|Mo z=pRs>Fe5<|e=qK)JWsw#2FP|u=SVFQOtM!}E*T`5BB8`Q$=~93@fGoN@e%PYajE!( zc%68)c&>Pec!U@cr-&OwlxVx?rKnJJP}CfoA(|dr9s5WK#43gV5grg67ohw`{xH6r ze~h<~=ZpC%rZ^_VZQ#z}PUBwSyybY=6FJk^ci2B7acmr3ffT{ja4pmfd;ugeCIW6S8MqG`=`G+fIu9H}`vE%o2XHI;Kj5?IC}2?3LAxUt={J$D%I(i^T(6_)>Kn9}E(g@1P<`3O!^UVNGC(SZ&NR%=OGvW`uE_QOTIZC}i}2MsN@K z1E>c#fUkj6a5ZoffPp;VG+jsQ>4Eg?=%eV;=%i?Cq%kr$vO2Oi3`812hr)3oI*=FRGGh*`po}=8tFet z*{G$|aVm!zMmfn&@;O;aULs4#qvS@ij66hsP1ce>kgv!mq={@Hqa;pBC^@B|#!{J7 zDV0xMr9P(KP}P*5dPAxGHfn~yhg#{sM6L7}Q`7w^RIb0D1pMEVkEsphR_b454kaO9 zlL2B2*+VGFABn$+$;5J^)93W9^Og7rugkmHyU|N|xZYCFbr0fMr*Txr*?Ot`vN^>y3MXYo&XJOX1$)igz1aL*2{W8{O^hNA6*G zEWQKZjz7Tp9;WB2XO3r@_m&6urg_VK<=z2=%X^xb;!7oKeO08z_b2(#cZ3}5izQEa zml7_|QD2_t7w?xi>Urgk^~l{H;B#Cn+!fAJ*9*sCXUKluk!ycqUuYw3C#)aa0BfA> z9wxF~z!GhTuoT-`EXy_rQ`?4N$+iTHw(_wKD+9Y@bz7>fZp$TW!1B-=?@ez^G(~#lT9A;Hq%PWWz%a*qbUvJnAc#_%|ByRW*^pV z9%@au9JGF6X}11j;o1tZBHIn@giUWX*=npQcA0Ip{gmykU1A^XD6@a(03317&5je! zJ_p}5(s{tO$H{O%aPDxkTxRzoS0Vm~s}%ph{Sv?FPW5Ert31{CZ4cs!^{(`6@c!j_ z@6~!o`O3U!eRi+aH_11asPSDP5JE<7CAN?b!ba{QzokY|Yy64+p#j1V1seiygSElR zP-SRtxFpPv%#7TK432&j<w-fH6QZ*ac)Tz5u%z0>*mgF$Tb*7*|ZywZRY+i!6plBX^-s5hh%TOoCq{M_~rL1y-@u$O!gIM8W<9=|FOkqlgnO zMDD>I@L~8Yyc%8&PlfaUe|$vXdI*P(Lw`Y=pmWd?Xg4$pS_Y*<(;)^l47$eZXJxaN zu#Pf2m=?wa<`Bji#v#xE_5&ht3{U{n((7m*T_2qr)kKd)_C@Z5o5GDDeV7rd2n`N8 zgJS~&gW3K?fe=~h|C=bI&iQteyS;me&7NxC=lC=4=Wds0uPe<{O> zv!PDARqT{mM>vLH%j}CR<+d{O@76jKg}p(CV1#kEMQ?03Z|~1C|81CHiZ`r47xf)A zp6>mrzoF;0!PWh^Pt`5#d)d|Bi*-r*hIKFKEAPJ9M|KY|%UP5t?%rvCG$JR{qD*tp7UHa<5GK@%;d=o-sQ z^tmO~l!dJ{ZN+{ywPOnNB4R()mCdcU{hh8wpMJ0{SPZ{|I>EP@!G!3`QD*) zJ$Alzop)_JyqUH??=AjzL4*o?=-QTm_$;9gZ!G@L}gQ;|0-4G zAMPI!==5WOje%o9cOWygIrub$gqDX7hx}n-cz5Jz*dMWnH$ zU84;Au6upuoq2r&IFE~7dM}6vo}p_+tw6q9nkcs^=;$z*1E>d)|-v-ZH?>w8ah&F*{GU0{gs74#SPS^6sszZviLpFo?98%=g}ju|(NvDnR{F{fp$6}FDE zO|&hxU$B4cusG_S@h;Rg!Og=<@LW%wXM&gGTj*O%ln|H6Z^$pG0Y>MgN z9pJGAr}!g;*96OAzZUXDZ9<;tyzpl1MB$j&c0q%1fncogJ^!KLL;h$%1MdZYK5sa` zE9NS1QA`T2le;%&2Dg{{H)kYw0_Py-EW4Kd2XY;0gYUu)=qZ#0JzytTo4_S!kj zvD|&vDe+8mKk(xCOrq9nCQFI!{^67`*zT_mEevKy-h?hh=S5h6GdcrY0#q<=fKQkR z)4^KDlEUwxJfr{_&)(0T&w0*S!UbYh#1!$C@wW1(@#_Rd0!%ngs1;3(T`8U?s*$V} zw@dd+ys}Nw^Kz@SNe;*w zx3X8#M%go|S@ym3fhC{mbp7?T=tplBg)CjO*!?- zDXP4jue}%tJ0|T zZp}{p0&SDttR0zKsymY_(tC2N^%L_lb5G`7%5BXP=ZW&a%UhnGk$*n_e13PntbkW= zv|vPmaNw4L`2#l;j2g%(pbM(1{SE+iWb>$4wPR)6z`CU0gW6J(hU6{R8 zU6s|L3S@q!`ZTjS=W0f5jv;-yvM~K*_Vl!ttWw3CtU&6K%z>#{nZKrN$-tBErN<>_ zrq53rk~Ti^j^grwVCtKMiK)p6_fl@hyOP=QA0)4iyO3m%ze~)P&rWDj zHbiA?rG1@G0Kl#1F69DnxoI9g`x5{_Gr=<`Va4Wk2}xmbQGgq{IXsHJ zcvIN=D8Hz%$nC=WQ7!$@AJP9N8i zUXQD=2Y)NF?d~rY5Vw;bEJ>!9kSIYls&LMH|+wTW3WO z)ubxYtT|Bf_;;F5>W{B@eCn^UgKzF1HlgY*NFE!R7$1G^n)0iX=Hp<3yh&mfa@Jsm zoCV~$kxKvqp5@JKGo40L0Y6l-)KJT34 z`VVVyZJWO=urk5aNbj#Neb($)vDdneg&mk*c7vo3$IxB}b}`?++yPk*zTbl@Zqj0W zGi@kRi=e(MY7TavjlZ3AI*%8#Yk0CN?iHuu$l8u$AIlk+*J|)pV6h`)#r-TKvs-Zs zci^lHE{Ysi2d`aQ%yn~1;|2#8a(#UEalO2VMF3WzKlmgKp_dW5Qm{YNg3`s-S+sho zJ%cL##{YvjM+Yv2I9x zF=~)<42nhUyQTJWt*+ zFkf`9^I($wZxw1?pkS%TnW5;Ur-kv`x2Zs?@)gm!e%H!R@JLPgDrRhh3kXw<8ZVLdv)WK-BNJ?96MA$|7j{)mUv@KjU6)fW_@2rX`T2RL_zUlU=;?V7flK%69~zhLtVy@EHO z^$o#`6b9{BR@25weCO25%GUeM6E`r?rx(Y#N#P&2&OCRc(C3sqjzM;xr1Nm9D!gblPhaGb9 zzPO#&5EC7pW~l40T%SGaoYarRJ4k}&@A_#g$(kURC0T;_u|z(+22(h~%qn)@``psD z=eUi#8dg$x2p7JAU)O@oXPl6I&w`}c;dAZm@PUD-s+*@Le$7&(T@Ov)GVyck7B?`W zTfVuIZdw})Cgy1hT73n9UrlbS0T?tpLr9#KD`O_u`#hX~rT@+Q_l?vCpU|H=&#H(s zuAo`O_ldmabcz+PVW6`{@0A?$;lxqdr8WZJqLXlOSur=>;r8kmK5|veGrJa$PhTUm z5CADtjcCWy(b1_TLwP87C0^nWjBvae_+E3Qos1p|ds)6IRn*c*=i9X?*?z<@QcoXU z3k#JQ@HZcT-npeMzr^OxFIIkJ?Ke3&*_AcV#wG%&pX-7#c4uG}WeS^jaQk2vmj_Qk z3x9F>v1_TeZz&Ad^wwf=6fQ@zJ?9`XT!(*-q2xpQE=zwLzIn6qrQhL6oDq(9N;R)i zv{JqP+={cZ$yb#^Ly5;u!nbh`esUSMa41VRf`nEQN$ed)falwcwJ&>s+{Cd4CVfx< zy{9v1rv2OzBZ;JD%NvZtdc;7tU_2ExCW6l+cz8mi#Pb(lZ$rfH0F|GzL>MF08U#NN zWAhlW(QhIVR2dC!#K0{W4umXm6u6BM%{uHcGs{BZiKD2|%%#)CaDuG3@tHJsy?67eL1w6kTVwl@yP^k;V*lB!_$m~zb@1+IG=f{K}~F& z9?3twQ&Us(idZ!*KVRKJAK5`ig=Be8ck?N)+CBV{-WK$Hzo~Q+4|=v!JNWmBQ!6om z#11wFybHlvfzE+rKGNn42d*$CIs=*XbXeF zGIcV8!mBo|HAlR_a%Tu3@O(rkUP2q+D=uHUG+C1}@lzpl>UXu8A>P2Bm`wfp?!H3Q zQKOK>vL!cW5*}-oQ4NVww+U?m1Tpc^rZ7Ckna4`2Q)Qk(r5TylEvf(paeZwB zJ_~oU$Lh8Ij3Z}HehtW)yQwuSO{wz%so!lWq#UVV90DlSKf7+;V+F^|Ht=6rU6S(k z$g#MjW5<3{Qk+s#isz)#b`<~f+}Y-L&*md9YA{*BWZ-Xmtj?h3%9SBSj*jzW>f|rY zJ{A6j?~1CepC|9#y_@DYylmaN4-0q|xrOJpldwEik7(++s1+t-2ImnRMx?m&g3{`k zvsDWGPB~2{eYqhd<$8Tjlo_NxzZ9ydN&fLwG0<6~&~w=~nEL*~5rw;(+az}NEj=VU zg2leDy};|OZWNo$G`9YqRr5(`iwLsqo4o`8$YTQrY#o93+O;CRvw6$$OD;;AYT`fA zE3Myt{5Y!D_7QJ>jFLvjAHehhrac#xVlp&2eQ+4b8;45q8P=S#5}Nx`fp+e10AR8X z;egh=gT%yrVlS)cT;-6RUP;+0@EJyT^x+V?gI#i0y z&^J>->eQKcQ7IwAk<+Kiu7B$D&KMxz9~O3@AQ=j6`jrn3$zWB)?gRuyg-owAdsjdWv}Ca3Kw+$lOfZ47v!r!X6l zvK1>_?zvx_9_PDmCOo|Rk9DGZsQJL4T$x;Gkq4Q-#aw(R zdi+s*CZX6hm0oxke_809)cLma>}9T0Jy0bWB{Qdp&K5pP#C> z25L9sUXIS;s_nz;h6XxoxbbG_E)wo@5&{g$aiGP$AGJ$Hqe9Sd;`IWoZLxA;InHx( z= znO$e#^?FALu$z^kFldKlJ_F%OZQs*&o)9 zKDr7HIi@rwp|7~2S@=0*l*Jd3?O9Dg2QV-l!>HwTF%d|ROCK0U!jbu(NEW~Bc;T4Ix zb3`&LYZzFoNXG5pog?te8~oK$?&t5^+4N_6c=N7Er0mVOy_QJ=5w&uqPo)Iu#xdui z4w%LnoF}l5nC+gt2|_^MOKTdiB6)IcpVcf#$o1A-{M>^7hNaj!U$*=c#V&&uXWp1& zeYWGyr^L%i@#c3z=kjaQX@&uh+N}2-e|aDflSMvIsVdlOFF0z!@f2xkqB`d2j0-4} z7+mobtg4!bx78(3k-AB<|4am0iEY&9SO^>KVDM%MNJi^5;qhVaZ1S_K>F-nIiRSVg zot`uM15XL9y1Hik6&o}gWZUpwiMkogqtslTCoI55wip4j{J!>7{M*MU6W?B{)P5a2 zo^VEKHEU13aqR+3q6Sxp>F_1yOlOYH+>KU7BHA~^5WtXDW5Ckc(P=qR(59_~QUmz{ zd)^#1+evD>gxqmqUg0<~aJCfuXWn=rZq@0mKNVRuB;`nN^k1yu|7?hX*x$Q~y9*#Y) z8FK2!B^WFB1gZa-1r~dJ*lYEq_?p+2f3*Oo9~P1>5>9i44-xy7R0iBbhDiw|LxC+h zFAlB_;ChmKe01ek%+SM*EcR}jeQC91-L!ukJM8Rbq+5NL4`3s5HN>F@=PUPJDYrI3 z|1g8qXsD)%{FsY8V%6&dnZqc4fcr0uPG=g|IGYoz$)3sU0&FB#4G5C5sXb#WZD^$vkcsHfjr8rP%nGc1QgJ3=>+A;0PExb-S)v1k<@XHkY z4)`RL}oApIjkz6DjRE3E&uJ&=&EfRS-=J@vXqMXrkD?@Yz^#uS(C0>QkR5 zGU;QOzgq@%z&Dz6a=1cT3<)z8IVkFH6X|5ooqJgRsfcgKI>hJKzC=Z4K+@ohY0zYQ z^NP~C)tUFu%5{jZGIlx$u?WJSi)4xcP!=aLj7?GCfRCP+G?ilTFKua$S!39uN z6qSv18H$=JqU-uQ#cM7QdZAyMw5R$+Soj@3MN{cUfI(=q!zRpPOlO9ipwQ`aTKNtK zZU=8lP!;ruQqf9xU%&?05j~|Zx~4STndU88FQyaOr6;+BgqBjYY(XirN2jXq6gf(8 z*L=FN)&1%?c|4^LkpS24_Q9@LHl%=Io!Wbh*~A*h-@wR_h* zNhzt?U-LBUnIEoY2Kw1wb75-~y&f-3`xBb!Mj*(gen;YT+FZG(JkeE#xdy!RqK=ca zD(|DB2Z2ZSVX8plUkv_lIL>?tO%)S-`97hvL=)|XPO}pC+V>blWj62C$u#H!e{(1A zm76! zt@z4gZP6v(dX{FzcG^4{KHGVt&8;bjKN+0vi)2V$c{BIWRMiE*8_2rdbHlzh43vp^QjFFA>jpoczs@l6S;<~d#Pj#x+^l14{ifjkz zn}@L^_BW@A_fLymYmZa7_x6i|b5Qd}wA*9NniJ%BqR(zQ8*KqpJ6`1#$ zv;c-QAG(EYRhenI<`!weHkR!cQ0N^H5U|e5&JMrHlFmn7t#qakOmc~hD*t^);v226 z5)k0F44|t{#^|}GBvYJY=(fA5&N04zeyRy|Oo_Kc)dKz?#4GASx=D$54LSY%^)6{s z6iF5AbeS55D(DG>iRJRyE2MAlaZP;Fe&(TX?;^B)ZOzuA+83k5KSy)C^P3q{{DfwzGd)LbQ!+CR04tuXmD7$4bk-0xj7&{UsW~}0sGm$%vandxurt~VS_*M2zWslr zjkM@1h5@&!0)b@EN!nCNF?E91%f&wrq84~g)VT{6t^wLIKH|gJpH>Ss{Bx#8E5+)H zK(6os@*dfYfI7RYqnNhv5MtGtZ(rX(M28{<5loN7dgsoaxp>`ErLwLE6Mt(A{NK<+ z9U9MQPK}euGKE6X`QyXmT-5=>^<0MSRh*MpS3nlb6MhxUwi3jp&gJ-`qd3u9 z%HOcp+%o+Im8wAShipFIFnm~qCCcivP$u3vMLU>++{Nwl+KI!y{yLA)i0An7?~RM6 zB*FBJiG8OVR`zlnU`UZqQCNFLq>TZnTEvZVGaJt0=9bOTUkqz|QCleC3ZR-L( zvAaX2C;I$xt`L9Qwa1P_Zyuj)0&HZ^2*{I%_FJyKv$6d~jD~}!D3i&Jc|@gBWu-`7 zUY;Ruzfpb9tY#SS3Rb86hv1q{hBZtdyL+>GCL19w9k_!(gL*zKc1?u}7i0Dti+<*# z0cY~_)x|e&I;!dEtu?&Pb*S2zl>(TpKmQ-KgH}ecx5!Ljpt1y@U)-OI2$tB;K)RMc zus4;(Jc4IGxqp98ZC#yP#*rg-Y7*ZB+ciA|kN*_X&|g6R^7GC7S6DWZwW@1Kx8P9z zl&M0HL~ZYVHhYU9*0!0yVZ(j?rcJcMtrX(WY@$(kX&NrZvQ zVE~WZGndaOeObO~Txm%do7%n(S180Rb#8z0;tH8U`3_o+S?`NAJY@tjE^2cAA@bEN zQHQiOpLXsQWgQL$7->Ag4m7YC%s)~yP-S9}WS0C&)S)w-DgE0Q73j~Jr8FAfaLU0# zZ+>FH!11`Gs54a${a0p_jvd=UJ!r4*_-#Gmj%9%_t6uAa^d4lfC2gkmn_h>VRh^iu zHoFwm|0}NG_Z?G@VAQ%Jg7xu6)|HzhYky&Y_%z^l*O!vFj;N( zU@lm?AZSZm+!|{en{n5e)-uJ@6Z5gpJuTPV8mo!zs8ZnlX&E9Ac$b~c}Y8qHJI zPTO$Ia@30O4<`WWP8T)YcjUdo;uwx(U{85p|MaQC*v4j9aW(4UBz6V6E$o1v>ts#b z`v;1YloUlqMh5o+@0OPu^y1(E?JslhzgM-&wboLa@^=j8K?q`HXc%!UE34`0$&)7) zpPye=D2Cp8NwIn^+dv|E;D9nm#YJ-|F4AjK?QehcpO%qT?#)fg%uxV&EXIHvdhd=E z6r5SIc=5!vle+bDtD@L7by6`p(5Xo3e+4D2ME7*fzV5`@q{ayRIG$EV{o7IFhAB}Z z{_@nQxt>*j8FTN(<_(TS%KRPQw_FJ9_RcpWXX;oVj5$}QpGKZ70&Eamz<+S}Zn<7QDY35&vbf5px$~6l z?d`w2N|lDyf=s`0y{KNh_v?qDM2Z@(4XGnB1Ed5!4wV~5-kUsy-9iyXN%-f2OMumZ zf?8E>XsAx*oisZtx8vxSmlcZ363LiynE}84uCL)OA?a(i8pjL{!%$}OShZCA}Pm}xZ zkDuj0>IHnUN%r^T+yuWw+YJ5D$VyPuq0R@^K4nPUSdN~skw(QPF+U#ng}H0|e|Q7-%) zpFtCZ6K%_twO-@*e-YQM8|iJSd3}55JYsu$%|j$A)U|X<&@hz$?NRMo5B5~^I^q}k>f*;cVxPqKGdx!~uh`^RfsqUP;4WGrWU&Q&b;(&oP<9PUAULU;I5#Bwo_Dlj-%~_@^Tgf+; zAo-aCnu3G~B~Cwt$!aBoo3H=CwnjzC7L5`y#ykUXguvGF*OK*{5{z*2tHwMx@F7zV zzM>;bmx4mZ31!iF$p4wQo8bcJdIa(M1}t+3Eq#e*?f$KU_+SKGM0~%aEzd#_Fz~e4 zb?W_$B}fi4nciy+8RQ$jLlW^rgM;s|tIPdl>A)AD%@!&uDuZVTauEUj4sl>O<|UU5 z6|g5=QYU}0r8yx~TdeKy2&2%3@GztGk^B(d*6ady%0ZKm+b& zt_Zh;gua5Dy0$1LC%O(>AmlejR!29EjgJo46PgM~*UO9C2KHVoK#gWf!p=fY$FKV%ebB2$DC%?X57}=i%WP@7MCn z%I4t}z-SZMXa&cRaj!1*``)_+W!VJ1AstCR!VCA<$A0&5x5awlHD=@G(?XRd4`byk zX0zfMPvAFK1nwg&yV))Y2O`#=-YTqmJQ7P@b8F{l-_XJ@)u5s13K_9tc+TX8z(ukL zj#tG`%mT_a>-^KuNsSb?nq(zKKesyd=B%UvOJY}q-AMAUTYk)%Xz?uq!Y+Hktd!&#R@^ z>a3lleY(HSlZ#e7elw@#CVv$>9!>l?d_Ts@Wy!t4jT;jBLq*9rMnZ=)}&qj9rmOvoR-^z-OJZ=krzs!`?g!;Q4AiY_#-{tV! z(eqkvgRfmy|8UP5`HQOz(|7{({P}8HbYY9lr4}{k zq~eHtu4Ch*8gkRQ5@HZatZwaAtK2u5(~tYjeLODVLOiYQ6zs5P`#r`EClk_KlSevp`?yj^G1Z+Fr9(5V zx2gI)(roX2?M$0w#qh4?JNMjJYkuy)$@xB@>EcFS#~kRqF5+m8VQIF?8rZa6b6JiQ z|K5YQ6Ut}7j_UbrRAX)3e5HpYYlf*gwXSouR^c($CfK;O#Q>d8Eu{4V{0K7 z`N(ay!;Qa`1gj(Ep*2chXTsn^KT6>r4|DPSlEs3!MVkpS zSNCo0agQ@uJe$Hiv1hfz9Jg0QEc()? z2l&SWYtD;SkRwJ1FP5Jf|Niawz5_tZlnibwk+3T$8U7OKs?tX!V diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini deleted file mode 100644 index 462c2c278e..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini +++ /dev/null @@ -1,5 +0,0 @@ -[General] -Name: an old skin -Author: an old guy - -// no version specified means v1 \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png deleted file mode 100644 index ad55fd5a96f702c4a6cda17692d78723b2e8c487..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17758 zcmbTc1yEg0(=LiTY!cjpdvMseTW}2|xcf#n?(Po3ouDDOySu~2J-EBut-Rm=edj;- zo^x$gt*WV+XZq>x>F%nvro)vLrO{CcQJ|op&}F_zs6s(O)k8r+Eg&I4K|$%*VLm`X zK_T0J`R)h>g^Km}g@#Jc#D{`{$F@}e=JZWLp3m40#B69{XJpFk2C|3x`v?fT*&7;L zn>vvhnVMVL3R0Z6c2ba8ng~*8aw)JX*o&K5Sbp&Uo2q#zsvCP)8}ph_2n&%3xbeAx z>_MhZhGcFa8(T*{H$jSjs?EvnvO)9oU?OjhB~~g_WI!ot+76V#eg?ZtG;|#$@YA`ELmlrjEv7 zOM53vJ6p295)F;)oSg(IDE@5%$o@ZKZ5{v1O~!U07B@qC7B*(qzb5@-tBLV{aQ4n% zn}5hnj9E-=OhKl$PL3>W%xwR`+FRH;**RL+{clA7XZe33a0A&ZDEx=xf2j)y^dAaG zCrKBl|9iBfy1Tt8i>j%koio_jRMN%N)`{}3HTHbsU{gaUJFvQ)oz1@wO8MU^ld-cg zvy#y%7#dsJ{xyRBzily#L~?D z-#B(wCRR=+R-if?FCQl>9|y<(1T}RsG%<8C{C@Eq%i(_{H7)p|21v-|Jwuq66hJJBg-VF*f0Q~?TN6PFFom#C5jFlU z6qN9VjD)DV+wy4^Vk&We(qJqZGPGFkTAD5#H%-m2nqM`YHSfo4@*g|y zEtj$mD=v-~D)rhu&V`Z4-{WOyUS(=Tho}Pi66M)TIED*a^X}d#(@bV2sfdF9nfcGs zzXykt39Uxx)g!xQX-+DoSv4bx7^?j4@TJXl(Gbo$4^@i##B`Z)YIuDbV1m{ihtFi8 z)R*}1w^p-VzP)o`>if}2p)-xWjNtZpVQKUKOlC_IlUXo~- z37MPm2SNHsPnKmA5;%$a-ZSu_-rA>JGtf3>$>W7WUD!6k*e6N9U$gS{{Uz@8Nf3R} zz-Sh$Ka&L^SFgRNDEHsPX3XE(AK277t?_;N5QtA)Owk|?-x5W}jN`8>l+G8F(UvW2 zG>us|h;7}YEHbngHzNPM_rr>G&Wde?NQkua2~4jiBIRXu>L7T)laO^@T0DRmc<)$V z-Bm>sXnv;pgFbGQIzQ>v-tgXL-`wjF9U2&m!7nme`g$Fr^v(F{Kj8nyPW)1dY`AS; z9f*-vuJrO;biUY8yfMp(ck^sPNfL?!W78>|b@#R+L5Nkcd4R8W5uv3$2iz@ac=QHg z2qR(dnIpNLA&^89wuQ16=k@I#>%uflQ5^DppF}Q-flC;`&p|r1yev zwT36V&D~tA<{_c{a1Ar(%+L7MmqXXpsjE`m$%AQdxwQFx}VDddTu>?O_Fg26!%yHM{Lh{CfqmW|>-$#;L6r+7UM#d7)=zr&praDf~r=y9F6%0RwEXGv_2`Tr{*(OBB=hp_=oYof7_cB%ilgGAyW5bE0>DV>|H$5VbMz;CfWE_M&OviBEq;8v8 zg=$jvP2ABdvdq`K`8rN-nCWlj{%0aYV@Tdr(F;>!F;jgwndHTZ!<4a8RnvGP!E#ix^ zHzCMl(OyBXLkftx;)CfVQn?^3o9Q^I5`IoBiR>l}RLJ=J^CGE9iqLm51C+j^@@tK2 zsy_-Mc(Dzk{CP#VMbhyQ(IW;*6I8lS}O*`m_xz?+IvrL$8J458psb1KL_-XEW=dOrFL- zv4Y2@986p0a9h|%>D;f^l=KjJ{%P7ua@*W|LE@*bW93i~vo!yb=t^+Cb17PhT%Ko3 zo2kyJ-EsaBl=F@A_CAWE)?l95dR(9KY_;Jr6T|BBhuuT^ABBaZb*0V5gVYICFH4vnOqhQlgDBDP89)_{-a&WB=fCm_I6o#3oR> zuv?T1yhw~)@19z;KCyVG*Loeh1xec7^Yd^jErT(0|4 z=ibu-uv*!RL~Fd0kYbT1G}hdC#6S0!G3K>2|1KWtr?^=Rj1DyHx%yxhicvCNFv{+$uS zMW`RnI(FmvlLx+`piVhYtZd_ANi+#)i(itSwdTUp61B>g{Op={S)jm;myH_ioAhF9 zTF&=tu>G>wVV617P^HfYS6BF+$7TVjzWL!36ta`h(gJiV# zx10d31;^q5d_spG8c3+mE^xm*a>qh}rK8wRTF&v~{6vwngeJGx+BXD9$B=ZNst5>1 z+ZPA16~xOtRur-R*3h1w0L#=PCmB|1Dr&*WO-##nG+qc2*)cMbSnX;+lnkakfA9}W z;KO)1HL1@@ggM=VX7da)8dH;Y3=-GH0ywir<6sQie9eJ`4M#clwKgB4Rq!24w4@Uu zZvvzJJF;vYUi$@09JG(70gX4x)}q=;1h9yJFbAw6Ok@34&;;DzPP(p*)ruJXR)MTC z?UELN2`y`zCt^lotBAI=sW0!d>bC8FL$E#$HbeH%0QN!ze#COdQ<7#Lge2 zdO^iv11HA;OIx4SXwsf~()}5llGT_w$%iW4ZU3KolP%|5vrR+y0d))w2C^;NT-+Pb<~$sBh=OMHRd7|xG#BrG}N zJ7$Rt(r&D^yhS}lzfdDf5}CU;*Sj;g)db!3Q8_dQ z%__#%vkQA9(yc2M6D|+k(2CpIv|=ZA*0cHFH^SI*d?G?xa+$(RtB5EQmgH?YY~-CJ zJ~Q)esS)2fj3EAO=ROuMI!yFwPUPZj`^>%n)?JNBAb?ka%?g~zjA~e+O6TXf2&mK0LWVjiV z)e2zxFLFz6+abs$EG>Cr0U5N;)QmVb?BY@kXMO2*jANSSyn^9IaoKngTEW?nc9`T zOie<0twSv`(-rO65r$Jsf!G95Ka|BoPyc)^Sob{LzZ1wnTWSh9aqo&Z8M*iQa^}9n zdo>@`zZ2-LU@|ci^Tqxr70MA^_p81BD(R_+=TwCtfF}ZNsUT+OjxQ9F<0K3}x~>Te zkyWMe7Vc)!(48PLs&6)(oy9z%!hJe`m9}8ZQO` zWtP#Dz{F#Fp)yM3xQ#<)r1!+b=bj%3(pNH1`1d5hvz6}>dS2cL?&pb8zj$Z;zR8p* z1i0jRThK1IPUdaUh+k`ayf&rYNuQU|bERf~XE>wkxJ1g%=9jpOChE53&Y+}lYLxNU zn%{hZPQdFIHJTB-5u5%INI8>K^c<8Z)slvrK!YXD!Ic12?n$4k0u(&Ltx*{-${yek zc-cwbNbpsB^;>R>Oc$=i_nHgP!mYzQm3p2A+#tS+y_f8ngcI{FqCn6uweOiJ0zvn2 zPWS?!%SCxuXXjg*3A&iGJj^Fxd=<8eT2^&_f;Ncy{VOXU$dC)?zg7q&L9aj-1)F=P9T>S&mr|+(tI0a_`X=D8dt+>zu z=PY(H^4D>V1wyrNdWb76@x@QI!5?QSh)q){xRrt|dvfp85F1rxs+6Nf?SDvUyY_#k z(o7g4QTTL4U@Uyhx{&h@JxuCGy#(G59C8+Zgm=G7I^6jw$@De!cWYiU{HwU|)>ned z`aqd8qED-MK0ATqHFe91g*%jr<@?)0lx-Br?daMjl;&SjscK@|z6#zjJ?vUs)r1*!>!nIm>TGC_%*-JUZz%^(UD5j2lA^Ajpv}d!!#U9*vxI5x1y4n)poy zQN3{A$!F8mk*t$og3*kBqI?TQ+qe`St{3h|>(OD-Zf+E3T3Poa+Gg__e3f}kJua=} z)_El3T`WpGD?0U%|A5{J@(|o1~L@1Yz5x9f9TIdv36nd*`*A#)QHLl#edw!w@s3T6F50L()+a}Y9Q5GABABo zDLo`h2uMwUy%^f(&%iSi`b$P|y4dZP-938nN_N5Nr~{~x=27}<;rH;>R9cpiIws#E zSs}d1pLp<~8^*b;XLiUBzS9Hr%4i6!7PgOoRSNc+%GQhxwXUb=z{NOX#}}EMjzPVH zq^TR1l4gS!));kbB^@iJc4Ey&NnXaK-|+oQy_&PXORlUvshpE_cX zH$4Q`XrqoNe!AXM!b*l&*XI2g26O$22@N?aRNFBT9MB!yv9srj5yU-8Ae8pBbfR%< z>^GUdcyc%n(rdH}LR6N3dF2W!<@y=zh$Fj3aW`Wc5jueGoZ80njsD`KBFeVXiC z?{T(0AEpOqt29-9#gbC&QL92V*oZswuO`GUbIYN)RL#=0^rbJ`H+&YFRScR3Cb>JV z{uwamUbRQ!eQrBlxZb3{#?y_`@P+ytQSrs*7(UlbXK7Z)F?^sjvh*~o= z+Y%{vB<{b#bo17bd+7MY>-A})_uhd-XVNL>5Di>c%#Rmup6&UezLvgerkeBD#REMy z>S9BHyt|BbC~jPgL2(FQxHav5oXG(8k~yrlW!=4#UDNcy&WN>bF2Bjw`(2MNX!g_( zzYswAgiguk3}epqGi&=7)~ECIxsyC64qW= z#8~Fr604T@)1|21+5f9tfj52*fwIMB`>A{wVB1#T(k(282k+u+HAsPA`cTD>zDYQl1umprt-J^^s zGD((U)8PHpKYLdciUkf4R(SI9cdaz8nI&F@Mdf;^RS$ZV!|%_&>xkrPRGBG1d7l)R z3;B`K$i=h=MNW5$W7q_BY909c4dtjOOiGU1id_>G8uR#r=2G$f$gIs?(1;pX{A{?D z=7XMsd^EQ8xYxb-2{hE?z8og0Hgyy~EMDeRZO0X%U5{h=@ z#e89r<3Km@d?l5_RTZaTzRCr*aCT8KhegI0N+}!R98X>bL1a>9n?##EnyX9mtXGu1 zsr-dw(x>6)s5i~0PboTg-WzMQ4ZSNr)V2oN+#6sKczlOI$3e=213nV4YZf+DI{sXI zE4~Jrj7s;|`f9xZ?~y}uO(#us+se)Q)i1h+-u-43LF3VH`5UWLr3FNo*G@+S`%jis zZe2pP<#XzNmxyQ<)TXB0P3$y`WhnJ(2c_2GtkfIdKSNY=BQtLY16$rov=8#8(@^Db zh@9MzOw?dANc0g>d(9U1BV+;`N+I=Z`iQZ-2@nzoEclw_EN{s5PQN&b(8f1&ozF+g zt^?wys?{1Yi{=4ZzxF>K`DFqlMcWUD0dbXHw<7G$Pi-jaM5iNEKHuvcf6OP7UIB3H zG)|UN8&&kzf_FN-V|~r4#FR7eB60kePX*FnlYIvjrFNja7+gw$xjTzIm z7#rs+EiJFRU8LefsTF>@YbEtb-nnvqTdnOX;whOUo>NGF+=6j7cTeZD8&d!N3F2cv zh9Dl2P*%}i%}x)K-ifKb5*BoPY};bh^3E#cHA=Fr5q|7*jwnoiT+Yo?@Uog~t7xdk zFLvtjM{*c%j9s}!{4}RRM4)>e@snD-8;1NCf6fmEA(hv!Ch&d^Wqodjp8)(vJi;bl zht;*h=p8}(C&Xav|RNng@Py+I@fRW`#(-w_C#H)j51K)u*Hr3A_GOXkF>3P*ccXq3Sv7b#d;%Oi zZNJOkExuRPjAH~@jEHD5V%K$|4k#da%9MI>z}X7dTjS)uT9XA`ZWZe@d9ptgjohFeMEEhqnl~?qi@A?+WO>30=dH4Ncfaf9E_2UyAj)^S4vo5ndTD77{6R0{p_fee9;xPIuIny~ z5<&7*!gQE?RG9G2IvZ`atA{(G$-k+-tUVlHFq4jKG|~41*(16ya}q!eEBg|uOuezKcrj>zOV*-cIUj|uwb78D+p(6$7*`^ z&uX1bY$2j9_lcOJ_8zeiH^X!{r8qK;@sGbq1TJV;eC^{?H9eIrD%F*xwI z&4_?*wB>+Kap!rG3~;5mzxq%f@?|uC)U)-xmdqi!Oj5ns_j{T)J+hvs`W_)qk8Bo> zm!(d<@BpaymmWq5BqMr)qQLya@KSnUQ^Ergs*9e$&LhU zWw7sBSldH=l9>0aku9KFqH!(hD-dD&s-y}~cq+wcAZ~U(`u$duoSkKP_$g^Ap+WK3 z9R`uIVL5~ef6l7SVJ<~zc#7yH#e&zuI2^C@S~*aHylp@qCs>{=Hs)$>4r5pJRpzR}W& z7xoDdJqiJRA@lNBy-vYc;UezqD+!>;LxKEDmN{wG>?8;MEmxX2hw!7%F=r@p7i?4R z${s6uosJg_INSO|+w?3Od?6VV%eH);EnR$i2yUpGRXbVymu_pA7ICfkCuP8r1)clz zPo>d_zQ9>u8QksZGPvt*ZsQrLi*^oAg5BQA^*I`=5jtnyU7fv~8#LIAUBuy0WhJYt zF6IGK!KB&8Y#y1IXHh2EY+^i}k_(4CJRs`9FL zf1qd)@W-F}wcN-uD@$kb9AMkFK^=S*Q@f5_y;<=bvZit@8TEeK3l zs0j|{L#yL)?h<=DK!I4xzulPkOMY^zuun?%HgCSpNsrmpZ*>;!$S`z+f%$6Z7s3P zr`ZrWH*$$}q8NHB-%*?&ft8sH8Cufm)x$6ZHJej)>49hgZf~$th-P-LAzmrDQs!iS zRLA3Wr4aE`*WdieOuOy$F$IVyD3M{mQp9|wnhIU6#IAa?#fyksy&iks8RspBfD*AM zY_1D^?K=Ga*1x~B7Zv-FVf=U{&EX$YNoDdvmbOF{l;XKBt|JwoA=!`B>m_P^l4ovS zglT+b{n}eD)Ow%IR(4)JGl&KJP%j+zvCfgqx?(Y$Gn1KTi{`oN0rh}}%2Gs-XH?Tr z<@pn>yQ!4QXWYcaK>>dS+$jh*OnL%U12m#nJd%)Y>ppz}EL009+7vHD z59PV3d54A9d_?O@H7TzxwfM~2~jc7M6k++ksWDO zX7m>{sBQ0)WNtcWi#wJU&?XQoaDN8AYPkRP=(c3FAJ+a^QtVwm-tQ3+aT8j{4`5G` zd-o78T22Acm5tf#!ZuX#5Rf6wojCo>T+=(@8Rw~=DxU3e%<=R~=4g2|VdwlE`KbJCpe-vrS`ASZ%fME4wUlnwuuNOkWI*lr51gkI ze!rgGi7$;jETbvKqmsj1>ij4%q6fJ6M}w`g-?hU)3fW2YBQvgZ>E^*^h!MNcA}nx2 z*zE`bjS8#t9TogOdOe(I$4p&0@i~DYQZpVN8E90a-tkJA#Ii?p9rNE@@u?vT&N5%doYpVUNN4e6zq}T@?@ar$d>~?zfyt~XIBuUk zW&gwax>H6njBC1l*nD5E9DV!#6i-&r&p9Ut`oK_eQ zyYQb%o%#3x-4Es@cMcm<;(g_+$(Wb5m}28kneVc7CGMvTyh2zo^>fz5c%cBBMv%Py z#M8kw1t5M$8G`dqKwTzEcLGONuM(FtHpBxsOV>pYkw-XZ7vkx!T39l9MdzmI^nGBu z>S9Da?AfHY^S?NI!EwrfeAPA&j`Glaaheg6N;Mzj-g~8P5P<`1QV1U@*Q^oTBbSsu zif)IQln^C-4CSRxvQvo=WRS=pEfVlB?BY^5edV|Kkpn`EUfsIkAEMDdm!{Ew*9LEl zq8`kKgQmHKY)#Ga*6Qm2)K17w-2S%XGaPOfk#n7KAAAvhSvLA7a#0}bVr+u}+b7C+ zb;M-si_sr;*=VeX=WajZ%cAp>TD?R`Ih0H5 zSe(zyoQBtN?h)Cx4Q_ms{p%hWuOa2iV@s>_bMXKz1>T~ftMO|AkI%x+oDfyZ0L)EU z0yHl1Sw5mz_1qAf`VF3|MsxG{=lA+Jp+Yw;Lv8Bm`R@e#*(n94nWjw9k9%5##D0kO z-2~HI&Wf`T@UmWZIlV9PWV0sPFJy^y(yIz|Mmj8rtWF+|*-k%!<_xVusK;UhVjSt) zmC(~jd_5OHMmIrOyZ11M4mZ(U17mk7E( z^G`~MU~P2DP_R)|j}1yn+Ru9Zo?wE@T3a6x;4)b0Dq5{$za6|cUHrpS%aNM2p!<@VNBL&^oqnCZqZ1l~rtY z#XnppJd5)3%8pn&F~#Prp!T?zz{m0p+}5aJG1aX`#l!>tL?`46NzUx;WS$MdOyI)3 zoN))+d8)5HS;i*^<$d%d*dMENf37#Q7@tWqzL(iz1Gxib-l9U^!Uaq$E&eQkut3RpPCS+Uymn37fdpOsT?ml75 zGpGkIfv`(AtZAeXMagoeUz%@sI-8qsh>sIy#bG#$TB2cW13h}jT`5~h9g)3n9}HLu zJl`ifY6Yqz0X>L(pypcngmPon^$N4llb+reqinW~RM&qT0xW5^R-Bxn`UU1y4AG!RUY@>Kar(e`x(wazgPQU4Y%nx*5Mh46!UoGrYOUp(_Ja{S-t%GT{DtQ+MJ6-qIyR zn2^6}9NIkb{dJIY`upz)(wl4xo4hLpf{hNE34TqT+O`ybUEC=h(WVf}2N%u{E}VPk z;oK=Rk2_|FaM#i9+?SZkGj_=3ZM+DS8#A_2R zd@!&5$(&SdD2^4hlcqh!yt+59lN1ocYLp)K`jHj+X#CD4fUxFpA%;r0<&=CJ?Gi6$ zQKNrrj9bVykMXF$;5u9kbE0kF1V2@Vm094+;ac;s|D7NSlT$XUK0vir9G>tx(mlpl z^inOdMCsb!?lm_8(5Y|9>-u=)cHL_dkcuG)u39MHSR=4HVK{ZUkpM4TlM;0gpgiTw z5)EfKE+0TmC_y#okF_U(rc9R96qaP!m<1{$%1T2nb0+-;+6;V}ip3G1D)!B0ALm>F zy17M{kDm&l7xy@7iOuxq<1%R#^NMM@#+C6TLTKyb z2ku@M%k#ZpkIa3SPHk>-7QF9Yr_PFROwpqsF-5GiF_;IlU5)7mC~>XagqhXyG`Jd| zt)N{#L#Xp*r!9;_bIl0Sb@KKi2ei-J)}c>;&_G=J3cr!Wup~jw@swL^o_oy8(A8Vf zZ~0JbxTu_a9JUS?PKV^D-)?goaEzit(#f!0g$U7*I082$la42!X!^cKVLR_V8O{JAk#K|!3(Fd15G z;tfl}b+aXY<2E**W$ihUY^ffktD9uh^1AdTaEEC}pIpa~o(3JA3WgtIFZ+3n+t|+V z-@8KeEHYV6h+EQ0$V)=cXWP@4Pv#SKJw8D)P7_hAhRj-t6%?(T`e9r)ar~bT>eD2T zE~Lr$8Y~hQm)y`hY3mv~uW}ZeT5{Zh8mIuw`xROlgrLj5CL=Gl34FV#*74Eq-KgQl zgsq>kG6MH7R#du5DD}_(Tt{F$m!jqiND-El61!nM-nLgt1dMZA=%r1|H=3zz5OPT* zEG`M)lnY$CG2J`glNw}K2v*6ck~=;VY==AaKNw{`4#;9prE-tyr@QYfQ}(8uy_T>- zsJTlD`62=vcw0V~xbp3Cn=JgKN$3=~?7*?<5DjGd-bYBbwHXSTcw6PwlbGb8V#&SW z_(*n#lRJEWlQQuOG#+@vRy<`mv|Y@NLAOkf6gvxSj)|=yQZW z8`yg1l?q$3uyUvleH*j=mi#M_C$9Otkp-{VL_PpoDyn>K^hnHEDqY z#ioQv`?kyo2KigGqDH#Ob-$5#D7UeRLOCuX3D=$Lp*$w;jnhz*bvZ1(r&Lo`be9wH zjl=}dBBLPki@UK{L-S)kcp(4oQ)9b>Z=JZ_i$ezQID%RgzK-}H`u6=V8)0n$;u>PU zv-BZ@LLO+^{6D7$j)y>Am_#0k&|=m+Rbi8rWB2g~L;;5&B+(-s>t3MzpQqV1Kl|We7j9uOgs`|(c%WN7#{0}T z#`#kd;A}8fWB87JIi-k!)pc$>rH*3&efz$EmHXt518)p48JeQe74GM8K8#SJ$tyaJ zml~dj!LAGp$t?myPN5-#KApcOt{&oyCtBIS2Bwa1XEYWB*jW-!@DCdCVz`Wt)b#Qn zAHM>KoLh>zO%-BJ%|t?i31(0X5)rkgZj>ELE?m7Zat8110XotPeKqd>lLcJi4`P_x z{tn#_2AQmS_}o$Yna$ns*MB}Pd_b)q{QF_-&pjpb?pAWU2q3j6o>HqF-pbL$mU)Sj znu8@Y&9qkX645bv7H45@?S;&7KjF9R5^@i%=AbhLia6VRB%}Js=(a)isMGX5@*QsG zPx7ZAl{igVp1vmiX3(B3Uli;B?DpT=Zo@X1b18wT)Yx&=rZagMMTw_}AUSNON;-*wKGS!`wSuWlN%hbh}f`tBQZ;bl{_N{qJ z*wThudHBV)sRmX6CVXgcrY+y*`=#f9wE?9PHYpa0nVF8C^sdSbLjRZfpFBX-SYw!D zuzNpM9Z?l|IGl}1ue(l^aU%>%Za*?XL*i-mZIt)LWvaMJCZCmcOs;ifOTP`iVxxvm zuQ6~}eNBWLM&8N}SUc!llhp4-rOIgSw8^;c_@`kb{4IauKzIOpn>>Tip`o@-JIuXg zr!4?0e)gr#yrf72D^@Ev`A=z}S4`RJc4@D2S=uTuq<%y9Gjq+M=3s%#v83cJiN^rI z;@i)%`GEk$C7@|jUg^bCu&~QS%^c@9#AOGE(iVGhIZZ6HjlPF~yT2VNQ%X0QAq3l7 zEO2Xe90Nq#*-pt7@4j6^=74`7;dHW8y&eOyx#bfRmWhEI{byK__(N)=RRurs?2ks zPoD1q?NbJV8PWj3wrj{0~Bpf5rP~Fq%3m`P|Oi=x!j!l<7K2;!W&AUX-doF znGK6k2=j#ppQ78x>$!I#5v%JogZiS^s1Yp#?c3}2+{;884yPNUb|3adfSs@Ahy*>J z)l!{E>y%I()~RIcw9gIft7zwx@C^qm2QCdEOBBlh?-`HAk!1cLBF_h9O5adFk0cIA zDQ*Ngg4|D!LMWm%nBq{7-uh zpuK@7e*81)0M2#>RS;*XAE24p9slScIREJN%jV4JcL{D`9!UNExcbSF6*`jl%;1_N z9y3osP9HH_Gfdw2CbFL=Y6C5l|3Ggt+aI#3sQ_SKzV)4XS!qb0rIlZUNS7kq1>a2@ zVZa;z+jlR(#-Ovkpbp-`ptrs7D&D;`|J0{C)YEHGJ*kyfy4jJ63f_!n|4WGp{wO=H zeWmK+c1fd{-RHPG4AR}Bx~EcOjCEl8@qDl!Gh7kk=!efMb40PL#V1*kn*5%V?^a|B z>mGK$!grRn5r}=XEd-j{F{RN~ura$EBe&%jRi&7)7AqI~pqyA?p~!G8{XW=o>dz}f zLtf0^xc{j&|H}?GujsaK(7e$adjXd#$4zo&M#32`AG;CFg9r8)l?P90(jzS;mmv*~ z;lIjase8@tI0adAu@k>X+PGdkekCPqIpEm^dITK|IocuZV#}0uSC{lKz8LP^BS=)K zRX-zS;P|z_7;<>uDrL=yjYB~_B-4H?QOH=48iz%KDkNf9?|gB6;1>#=-1dABst7#Z zYH1QW3w++{XcAubzZ3XjG;NL*g*Sk{9qMor^9Y0YOK`1d@42ZhQ39{@6ixD&I^s2s ztF9(&hZ@CAlwfaNJB8%;d>s-nNd4@?bHMS{bE7$sx^HP@y{Zf?n_+k&l4&>V=sniZGhi-3ec*0SnhqsH5yQ z;ip&!9`H--L6d=(hmqckTWr_O1-D`Xbk;*uetQ$|>;%k#)NuI%l^?>YTj2$&ZS}K* zAiEpP86^wt3Mpqc@og5-VL{8;mpi#edA@&~2!FF6Y8`x%Y|W%MeDW0>z5YHgOBxQ;x8p(u5a43x<&60PGpGt%+SpbZ=vYz%aaaBj`ItodV)- z4+pJZVcWEnf3|JOw-jzugvH4!i`ycJ~pm+z>;yOpX)y?ITk(ySB&X|0T+6|C zd0BXRmmJHsb)=E3H^tmB*D;|=MZ|EZeG?FoVpidkB)F@Peu}x}-w2@quCh6{!oc5* z{Q+njyF+@si%&EYccD68Cf85FeQ@BbhvA)c?X#kl!Kpv40)mNu|NX6@aGixdza3T| z){$5M{^7H*YXqhLAn%nXB}x7CjVw8@aC8CU&Pb0u?|~ z%}wBZGuQ5OVTvwf5-E6a=}WBNWBuAR%S?q;(GmRQZrJ*EczJ&ia57;oMB)1Gs7Q68 zqWphNV|KDUK?_a0xgjaF^VG7#7u4D9-YW(j(-*e=C`q>b9y#bSC(JNS6X30wgL&R3N~CEFGE?P!SJp*HOsmL{WuN@N*l zUiiBBbEj^srAubQHJ3w`v%Z(_|ASYla$9#&1L+q_go>QLnd~<#XjYc+yg4ri=GYIq zry2`0lAZRHm5#Kw4YCfuuA@rSwPS@^-bJ!x+LDt&ma(utS~@Y;x%S6XAqAA`kS{_b zI3!A^$~|c-Gj+<_*rjfiwq|YtCLqBb#@0j?Y2A2*hZGUk$yB@Va)jv6{_nqU?I;An}X=kJ(%yF34$00IABM&2o{HL&-VJRmK zTx33)pAT)SCT|<3@A7Pdv<)=}34R+4L}O3V8Y>3z-F1C~FkOHi&VJTAUKPq&JRHJV z$J3|ANbfDz-}c7=t;=3~CRQnTm2W!{ngrGhA?$<%FcC={fPE4d`*e#R5|Egq)luSc&f9 zPi>8X86>F-0%I5zW6bC$X1D5J6P=n`@xzFVEBh}Z-KgM?_IOG(@m=j6G)zC{UZWid z_`*o`xCp$={!Z1m-X zBmh+WPM{pC#_3Pt*tBK(43w;}gJYq92^(l}stn59`rK4q!v!_rmL~j!&^BJD5(`RI zIV?%9Bh(I7vdE?yF~|tOI-~;Q&XiMI^O7v6nCXd*5ZkYub;wp$5t&b%&d(PJkaNzE z6SpH$4uhwjiac(mRF=mu4WAcne{nxgo+kb8KsLChJ#kuh1YZMNZ1qLz5^JNp807m% zZf+_~beQZqCRanBuokxHI+Sic@kaHRg_YP=e%N9>)IDiE{EFJkFA2iilAV3QJ6oG0 z3Jt|yiCVr}=yR!ws#xraL?QwLFq{kICcpDD`qME?^$7lKwzv`y|AoMnlt_y6W<8|w zoSob~8wr2iyKlv0w+=2A!hs|BermxKDsSPB59Fp>Ofb`^39c4juT~zFo>QP0bRII; z4;ogfBSuUdonuOgh-6QLNC0`?^rqv`Lt<%GUiQ-nMB0^ESyD-bG`VO6#g6MQ+K~vX zPzqt3S7EBzZ1foc{_6b!qPiInnQg`j%1;U82BBrh&Qq*T!_m6Rx4y1OG72@vOQP@W*ikZiF?`X`}U=1<2JE|?OHF5|Gdq$U#U@FeYsxUw0l3N+l=zX)85M!CC*>F@R_?I z-z!s~ERsIj$2TAYn+Zu?lTpQ(1(uh!2}mw9=e>ggM)lh)bD z+>a4-nEz92<;+VjPinEgndN*QFaGS3)9sS}s#3Z)P4A??2tS;=#QwC9$+h+y`6AgfmTzqL)tQheJZ*Af z^8CO{Ovw)JZ)%gCiN0jiXFnFpCZ*)m3% z)MZ*%KW_Q>YWXco0VkuAOX@w%IA*2D^){fIL`G{le{uB1%-j4D#ZJreEP4) VV)DP0ML;K@c)I$ztaD0e0st;F6YBr~ diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png deleted file mode 100644 index f5c02509fb63c4f5d68a81f626b9ae07fd22e800..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4661 zcmV-563Xp~P)00009a7bBm000XU z000XU0RWnu7ytkYO=&|zP*7-ZbZ>KLZ*U+5Lu!Sk^o_Z5E4Meg@_7P6crJiNL9pw)e1;Xm069{HJUZAPk55R%$-RIA z6-eL&AQ0xu!e<4=008gy@A0LT~suv4>S3ILP<0Bm`DLLvaF4FK%)Nj?Pt*r}7;7Xa9z9H|HZjR63e zC`Tj$K)V27Re@400>HumpsYY5E(E}?0f1SyGDiY{y#)Yvj#!WnKwtoXnL;eg03bL5 z07D)V%>y7z1E4U{zu>7~aD})?0RX_umCct+(lZpemCzb@^6=o|A>zVpu|i=NDG+7} zl4`aK{0#b-!z=TL9Wt0BGO&T{GJWpjryhdijfaIQ&2!o}p04JRKYg3k&Tf zVxhe-O!X z{f;To;xw^bEES6JSc$k$B2CA6xl)ltA<32E66t?3@gJ7`36pmX0IY^jz)rRYwaaY4 ze(nJRiw;=Qb^t(r^DT@T3y}a2XEZW-_W%Hszxj_qD**t_m!#tW0KDiJT&R>6OvVTR z07RgHDzHHZ48atvzz&?j9lXF70$~P3Knx_nJP<+#`N z#-MZ2bTkiLfR>_b(HgWKJ%F~Nr_oF3b#wrIijHG|(J>BYjM-sajE6;FiC7vY#};Gd zST$CUHDeuEH+B^pz@B062qXfFfD`NpUW5?BY=V%GM_5c)L#QR}BeW8_2v-S%gfYS= zB9o|3v?Y2H`NVi)In3rTB8+ej^> zQ=~r95NVuDChL%G$=>7$vVg20myx%S50Foi`^m%Pw-h?Xh~i8Mq9jtJloCocWk2Nv zrJpiFnV_ms&8eQ$2&#xWpIS+6pmtC%Q-`S&GF4Q#^mhymh7E(qNMa}%YZ-ePrx>>xFPTiH1=E+A$W$=bG8>s^ zm=Bn5Rah$aDtr}@$`X}2l~$F0mFKEdRdZE8)p@E5RI61Ft6o-prbbn>P~)iy)E2AN zsU20jsWz_8Qg>31P|s0cqrPALg8E|(vWA65poU1JRAaZs8I2(p#xiB`SVGovRs-uS zYnV-9TeA7=Om+qP8+I>yOjAR1s%ETak!GFdam@h^# z)@rS0t$wXH+Irf)+G6c;?H29p+V6F6oj{!|o%K3xI`?%6x;DB|x`n#ibhIR?(H}Q3Gzd138Ei2)WAMz7W9Vy`X}HnwgyEn!VS)>mv$8&{hQn>w4zwy3R}t;BYlZQm5)6pty=DfLrs+A-|>>;~;Q z_F?uV_HFjh9n2gO9o9Q^JA86v({H5aB!kjoO6 zc9$1ZZKsN-Zl8L~mE{`ly3)1N^`o1+o7}D0ZPeY&J;i;i`%NyJ8_8Y6J?}yE@b_5a zam?eLr<8@mESk|3$_SkmS{wQ>%qC18))9_|&j{ZT zes8AvOzF(F2#DZEY>2oYX&IRp`F#{ADl)1r>QS^)ba8a|EY_^#S^HO&t^Rgqwv=MZThqqEWH8 zxJo>d=ABlR_Bh=;eM9Tw|Ih34~oTE|= zX_mAr*D$vzw@+p(E0Yc6dFE}(8oqt`+R{gE3x4zjX+Sb3_cYE^= zgB=w+-tUy`ytONMS8KgRef4hA?t0j zufM;t32jm~jUGrkaOInTZ`zyfns>EuS}G30LFK_G-==(f<51|K&cocp&EJ`SxAh3? zNO>#LI=^+SEu(FqJ)ynt=!~PC9bO$rzPJB=?=j6w@a-(u02P7 zaQ)#(uUl{HW%tYNS3ItC^iAtK(eKlL`f9+{bJzISE?u8_z3;~C8@FyI-5j_jy7l;W z_U#vU3hqqYU3!mrul&B+{ptt$59)uk{;_4iZQ%G|z+lhASr6|H35TBkl>gI*;nGLU zN7W-nBaM%pA0HbH8olyl&XeJ%vZoWz%6?Y=dFykl=imL}`%BMQ{Mhgd`HRoLu6e2R za__6DuR6yg#~-}Tc|Gx_{H@O0eebyMy5GmWADJlpK>kqk(fVV@r_fLLKIeS?{4e)} z^ZO;zpECde03c&XQcVB=dL;k=fP(-4`Tqa_faw4Lbua(`>RI+y?e7jKeZ#YO-C z2VzM?K~#9!?3`_EQ|A@Of7kDh-{ZH$!Es{a#4$LDQxZr?gD3>ZCSU~tL85ehmDasz z9ql%$)ikZFU)Fsp5}PJgX%a=Ft+WqlYNu^l+fg<~E48DvgNo7ur7aB!geD{b?mpbd zk`)meh+iM)NLT*gDDvI$ zU>282Tn4}hOaMc`8ABO(o>9Qq0XN_UYJhri)d7CMBffV6HgQvOt85-jF92-$#fVw2Ln%>MMf?_J`=3cqG^prQ$I33probOZfBYlVsEUTEcc~>25u@uqI7nJvIS% z1KYxOYvZqaTIdV=@#MfwO99p(wv9%h7x=c@Y;6DiiZnx~M@YFR)plJ-|}~@lg8No+Kfg zMXusqfK~%bfuB9HpuX+tB?>HRI?ww>LHq9h4ncOh9iLR`h%lz{4r8W96=83kg=4DHqe-f~rQ5Fl+_}Ug}?z*K(|Aup%J& zMS;EJ?SWq6ZX4xhGga0OECPPAf6X1V`<#@c>1Lp+NwiM355z-t{gD9WYPuP0GhUI{ zZ+v=50-I4UC#37 zg;f;AAXHuV-BbguUMDixbjnr%ZNRqgwKgD&>84q$L7?x!WIfFuyS%Vy?ayXl>!Ya% zGMVlMRZTUpbWy-%O-Zs=r&HDdgn@57)*i*6Q>2Ei0+K-Ms(L@N*;H5;aL0yN4c;;- z$Zrc)?5(xHom-pgkkPW0HK9G;Z(CO9l^)g##OfW^vgs+!2G$P5dTOhY)pS{Ps|$$t zO6tPd${NsK=14BB^+*qE6#1|(=CV};tiQ^jFZu)HLcvx?99CpDUDh7!sx9FsaJg32 zQR+|Oav6=SE(!WfGlA6sMzda55wI$7{kr7G1v0SLfGZ=DS1ST`0vI_zHc=6|3v*MVW+_-khe zrB@c|3i8Xf}P zdj9>>T$V6To?6q50`CE54om3DQ(-5?y8ZK>W2fbY75nWV@W#I5XZb|3T=_<=iBaGv z@ao>L%;!9BJ;Gcs;W)`9|QY;dE^8~2S=n<7GMN84D5dJZ|`v7%A~xoS&{kW|9mlg z>`x~@lUmtK>30;^_0ZqnDQkEBT3C@)T>$<8{ASZDZ}HjaxU|YygkrcvIi5D*^VO20&WJj0zW#msfWcux0JA;jX`> zF%|jMqkO=EHiE^0HX>;a@c5&t2G@>sl*S?s)yWrZW*wW+210fM+kka@SEcEX1d6hj z^9h?V1KLEP7~1T%2NqO2nszTw(G_qP)U^u%n@L_wXm6{x0IPwA?}^qppX!X$>T{wm zz|7S`!Dg>zi*W5$v7Wd0h5eSt(ovS?Vb)?XVY8uaXosefXjA((I8BE7Uo38B6+v0$^;vsG+WF|^@4D}d!I!#=hr!mO(I5wM!2fX!Cgj5e~jR-~RCXcIRVb@*MZ ziw5bB1W0-uw>>_hRA93MnO>-!0gnn-Pm3$mQ*g4oa*g4n|u>TJLH$=ORPk{uO00000NkvXXu0mjfh-Kpu diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.png deleted file mode 100644 index 53905792cb2e40cc5e799a146c1ccfbcffcfa369..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5585 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 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png deleted file mode 100644 index 2d9974a701fa4cbb8fa5fed9174260611d312751..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102010 zcmY&;Wl&s8)a~FdgS!pE-Cc(Pg1Zyk3GPmC*WeH=Sn%NP5G=U6ySu%*_rC9|_x^NM zSD)T%?X~vqKBwwLDl1B(Arm13001;u8L%n<0FMa(K!FjV0RVuk{0;>GDo$J$EUxak zeER0`n`tO#0|{dG{Xt_P%Oh{d#}R{!LJS7$wrb>ST``Z%c=nTz$Hc_aQeh#XwV7tc zSs`f-;kec8^i&n3vu)rfj4Xib2ZBs5<~gCzMHR2xXd72;fcn}mE?^=sL&vS+JeF@) zQgi*+*sn3RuE+k1hqs=9r~V2t92GE73P%OfPH}~Z2p1(y5k*%JLG|zO2QVfN74z>h zn6Q#+Q5n$J<_Y^ht5I|XkM(qLY*D_PM0VnorYf4iE5{hug~<|LPxM3)M^Qze|i23GhH+QBFT1}VP_u_ zQXWoM5W%5oo+nVb<^H`LpO{R4i9CvWGx2}P{mG}loXmYJ_C$uM+38rB4JP@AQK4Sg zx+vTlC{7oFm^zS};{KiGe;_kQ3*#RU8R=pj7)zfY@%%@M;*osp*J&Rke=2>Jhi z0OCy01aP+`2o~umcK^^kRU-Vnu~Qu`n7vjwv}Hl?Dw6>E&mb^RD&?D*$NSO_G{KWk zund{VMYIOvM{ z#|_7OzHdUjME7g3tK!f6dOsridB=nLI#v_NqW@RBD7tSf&;(wMo0JRw6V@2lm$d)p z(a2(9L`#N8eZ2^bGkQ3(?!RQ8h=CXSe4a7|d_ZJi0R77x%phYzMI_oi(SK6^SD7S(UfBRhki1`xpl~x$ttp7T4bR+{ zIJzl}BI=>;BO4Lza@XpIhsYuP_Ld-w;LUq4NIdS}`a%C*OFPD|0_LZ%*9U;|{0dIU z3^$N1AO6%y^>& zb9#oh#q_(8y~w58$N?7pWXP+n?!TONZ1$00lu~}!WR}W(^CN{O@TzL27Q#shPc(uQ zkmwRi#K7j}s%%aQJ)*Q_W5aoBiFDOtC|FT^35GJnM&FlyZ2$RRAXA5ZBsrxNvGR;k zDHtzSXacW_9&HKm16$o${v`#(f;uyVZfCm7=es|CAE0lMpm_lAGW1Q1%D=_-cb8Ip zeuHCERd*a3DjAaVe@$iA4yDK2yNm56Kbr^i5{D-6Djjmw1rI^4d_9kY%=jqJ)~#@> zdF|EtVmUL^R<_Hbr;u5ZuwH#i>ALOsDbUk~!ok)!9 zl<4q)IWf)qeV&3^Q^}4qyD<27zHNdF~v^+@$L^VJ0kTI(*M?G@1SYk z!3%-)RHv_2%2b(=5daFg!28i#=R+R=+Fw5X98EVSleASfiEq-9aQ0f^px(sFW2YYR z4pe?hq$@ZzmA-(&t#v(I{OliLxgFvg>o{m z$>1d7ATwKZnB<-+U_w_795>Yz<9Pus&dC;0M>a0yY3g4?Z)HXaZQHf&=D;7AyNoXf z@gM3!a8)8xveEkDs?I#luRHwhl+oRL4)z{4naS8H!$8b2nG_X!iivoATe$WNDCwGO zgqjH+b@g40c8d2N0*bq#AXCE@u6=E-Gq0*Ei>J0tuT6^I!o&`mXk za#pN$>!4-&Ib)8z^|bY{d!D5`0z~=HCUVM-EZ)Hl6(i&t!Sq2y?5r+u_awnbnON2Q zu4#YbOWHg|wPi;ANBcKWEa3`TL@4I4!4Ky+fbHs)u4aQnTQTHuoI@!A@#)bkFb5f} z@>%Tuc=xTDvZt_?LfTQASu3WuAZCJ(>bQswrZl`gI(RY1|1mD_wWV+auQeWp5;mdK z;Qc2?g4<6DZ<$W~he`66GEd}~)bt#@j1S@KEqM$2&n;i;OAQ0^WhlY7R@^E_QcD{a z+nFSok1{QPWMm#{kzhlkf}i={Re<=k*W2>_(Wwa+oApjd6#1ety`%AbTPlBdt@23Y z;3=0G22E7%T}b{>q$kTOkdoI`88;Q5EH>0x;X*uLayd$8H3FWX5Fqt%csD@mY}8-% z_xw*-ji3vy$ql(o%lhElLG)P^vb^UoBswHHk*;vbtY%HX{GeYra@Ov*j(Q{OoLemY zGpgRCbPypLG_KdX6pSnV_8$0KI2h6EJ+d9cUW|K)_0&-`KEM+PmY+yGyfBF70oYed z2SX}thk(M4uM#Gd=fV9H7!N1$>_3=OI2@y#AUUbsWG?EA_bpZh(~DDoW9{R8QK>&c zUNpDL5d1_SJ3=dxmMn3ew(i?hduO(SGl=|W6g@R=AcP;x$mz{8_g?GQ%^u#aN1-{`x$NL@CcgG3M*#D>sQ{JGR4q&G^w zb-LfW9h1Dj920bQ4=KJ0X;F|(a09h%i+DPHjcdxHhPNxO_+ zd7Jz|LF(;K;Ng0Cp1e1FeNtp->a7#v6Wl9lj#g4Qg}C4W$ojbJ;X(xdjf)CAdt8J7 z{U3h~MTjmefbS|Ya;)G}lhTo!aLKzS$4=+-l`9An~-xktX zc>aRN)_TzZcQ25Gnkzd?bgzKfWelIci6b}%XQ1Ivi{egf{E(>OFw>x&rES`rpeSPl zVx16bT%hk7n`KhHBfj>z% zj=8y<(ktnA7#}O|P#~5b7nac#47q|)1HsxvY=Uv2+~J%EZ%+!|FAPne8G3QW6o1g9 zC(^y3$LN13u!7@7(`a*AS}N8b{cGC@q!1+gRIZ(KL#BSv=2K;HcYCV*k^?H~l4!~y z1}N^LjeCZJp)d;qRVQNS0}=UwSRhf8AZdAYL3v&h3ZCt{VPS7LiSx+V^t@;+9Ommy z-XY>bW`-iV7#^~L-<&ie|E6nXA)Fbk8DK!aVZAbOv@FZf8sh0r*|Q2Dk~wj=-|6|R z9p~I;QEku|KjnJ3UM(-Ew797H^|iE=k%SwTa#onu57+%~{FL+imAH%~9K_!&Eau>3 zX(|3qsr_7PKfdf~xHmj$-G67Icz<6IO^+>;kq7Ia`}BuY;Fty!z$L!8(a`g5qlOlU~ZmKCi#)Oh;rJ*4mW#cd7u}vpTcoE^* zwCm|W3A#&V*YD<1zu1pLIbtX|w===%ALA?oaek;>>-rnYUpv|>Y=;9huBw$MUUr9A zSczXZ$b7s~_!Do`i=Y;Z{v>EGCLA z$z_u#^z}|w-;Y?P9~qDety-0QficR1!pfL297S+AI3{FRGJNnVR{Y^R$!y;SA2Fg35gvcRm*v^N z?dgQn`I;=r-OY@7CKV=pcrbJ?s8O}8&no^vH&q-KBXHzzzWZ8z?;fcLN&EacE!$Y; z&}hP`gI868pFcrU@}&l9avqt0tvGOB>8oOwsQ1fWCh1Ka6Kb|efK8e`T!a&Y(=F)f zw3c9Rw_)x+Coi?l9}>tf55*R~|5ze}03u!a5vWpA8o{CMZIyg#*i&SPg%aaTTc{S_ z@B2Qsy>~w^hz4e9ACpOkD}Y@AEU6(75oZh>PS@*vub^{#8+?${)k83wVqsbHP>O(R zamV-50(JieqXPJzx1a%=_pDKkZx)LER(pCYK5W?$BVmXs*tKFTiuVT#U?QU`)WZkF zd*ECu`BRWI*>Iv|IEZd)`^eLD%&RuBayM?Ugj}A{?QWu?%|_3%A7bLq5@lOiKc}>i zPo0T2E}9g%*Eg3sf9$}heJ1)(eGoB214v%9MLIBz35}c9?avLZrxf6{)Ny1-jzVR; zDKoPpWt=a*rUj@iw`-q}m$@(+`_Qc=*9ct ziBpv~0+EvtuKY($8(BL=*{(sNmsVef6J4%Ph2)bSvqNP`@!J$QUL3vew{qoP=LeRr zZOyw`+ZhJGum_qj%Wv)*o2_SLM`b_mhLD_}l`%C{eOQHaxJL-ke^$QniYoTFHEp@| z4Y|^7FgM?c{fXw)HUYyFjA<|gP0^f*cOGWFML zDv)Z{E?Ho5G2#;e+GchPwT|wq6-Q#${li@MCgH!2OV$m|MbU0}fzD^7{_34=lom~cc4louC_$stg zS7Olsx4G9tX%~76BUpfP`SNmgR!$Uz!!0I2lAWF(UvWG%xgJO_e}bmDLFdq}Wj@QZ zUH;T=-DUuiHwY)oO8FFh(ehSuWYx}~?I2kX7D3BoLb}Nt8TU;5lo&qr+$!`9@+TZ z*k7nvx04ps;DU`EmaCAaviZ1iQYU@QgCC+sMXTrM1scoAew+*L3SU&BUpeK!?JgUMn}cw zF=f>5?yf!^KfZ;5EqHT#VPQoB_cG)Q?DqMV@#+z}-VBD>=ccTj@~A~S+|g>WKohQK zhXu0EQ5nGE4vCKm>yU}GNVa)X_2-2Z+v+*G=Mn(w$mDP;#8`YQFsoI`==Lxk`oc;7 zp`I9a!t(PKt%-FplR{<^`8ZO{c5?nxJTp2>M*XF}-bkdevHUdJsnZa$fr4b*sL7hgH`LqBvXDKN~MKejx za}Y^h)+8Gra9_kgBi7D?PzJhT>ga5dGXiSx_U7v=qH}j);4m=UQjOOyq*5ly>>~Q^Ewi`XBoIEEKVE zi!tn>mUY&w$p}|d!=PzqY4T}kc(+(%OCoGg_8IKF7~v>ZojM_=kFs}t1io6vzdKs} z9{wgSrkXnh3I1DRe=4~2?dvX^{wtUv1(DCpaj)cV@Oy9qz6Go=g{3iX#5NQ`q8*}S zH(kJBG=Z;=R@;rU)Pi=9igL6}BY-RGfvPN-a^UBPis#40q-_2>=d6b7maQ$sUx!6K z#I>ExEU=W0K+%j41Vu}2LIGy-jL7F)sYZ_%hsiR|zUz;tyBvO|<4RvyO6>?@ zX|660*jS>fX7OdE`;g@Q@pziMo|qG8{9xDgmx7*olaP9O&(_goH>gxtf%sDSUUOn6 zutj!0IoGyM|Mbb>oZy$@A5)n8wkd`|;76UT8{yd&?ylnStL(~}pNA3#$#0Gt|rwSR5=>h-x>Zh{UHwJWe@5g=W)JpM8qzxg3RV6@AuNBEYw^wVr` zY}{>mKL{I&2dXe*Xs4`LdDEfNwX*)seTK=~K8rKiVj@syOa{?3dOH>ms!@ikF#FL? z%C?u6;Dw1=x6jJ zsY zPBHa?)wxK&nw9#NtMB^=*O8Q+!LRC0mHcse3P5j(=@*r$1ijX9N7U}`8bkmJ>7tpu zyojQRKC3I;oAZiKTDsK#d=0>p4UEAw&-H|s%w!ZM{#aqY2@+_&{K7NaIA}#)M~!$5 z6NBkY?x#94#rY+jr&D|5boJSKibnJAO93heO6`~lD!}ywUkWqAjBL3}0RG7Y=lp3J zOwH`mJU=n8Ki{Tcnd*o>ub+=Jk()bJJbZv^zW9pRM)uStaxDpI#KB=U`B>s<(V6}- zK8G%s2naajLorce4k^itLEly=V|G39(con6JvMydMTF(S3SG!GYNDhF$xruv9`zC> zT^l@!y{=SLZoN1y9iIPK^i06U$Y=xYR6A?8UOZUtM)=LipB&3*tUp(47@V#8wj7G^2bn~jN8>qJYnCt`?IDA{V$@ZVFvBlO>J)&M`IUOk1%;$(gdC8ua1Ka@wG_h}oOl z&BGp5gufUqd5%Ot8p-0zc6LDKip%`*dwyA?7I)neS>v-IQXw&=^`&6ASV#g%>UN7G z7j~4nTiv(f^9orV!mfAX`B7xuzL)3Eix($ zz?s(MMyVL8U!}66fa~`|S8b@m4x-~ago-V8bdfaskb4K_!6AAEMuXtv?CY*lKYp$| zK2-ZvUqK>ctV+Ek8_0N#oyZeMeHI2nE>r9W4I*#2xSy2UTTwXuU3wBBjHqbi4(i3A zi^Pu;l;`#jhP??*B4P+LfTjPEN$m z8-HnezHb}Pkpqi@kXL(rLuQX)6vumrIgvG;god>up&&w+!<@{=TBL*mlDxGb;d@<0 zTF`w^h7;{R2)2H*e@E?l(@=g7VtI1*ossSO)U01yJQR)c6sa5A(@uvZp(tZ0=}C<} z&!b@WwvAPbdmt?gzoS>MA*dz?V`S3mJkidAl4v|+k1n9P!-Kn_vgwbFc}vUl3WuW} zmAecjd~6haU*sZXA|oY{7A@$VGF7%B;!pf*FNj=3ItE=j-PPRPjS}B(c22Nngm{Tb z{YJLtH=Pfcz=!Mdr!aH?pnKq>uFTE#euN)7n6M?QQNKm4eTlh96wbs^?KeI_ed>w` zgfkyIZzSu}rHuK3v+3O(@=IovBL^0IF5_RbTec)>>+`ZAX`|h?Y3rzeTE=y5nmEf% z2w>@sgG}ih^tF2c*dt^xP8ON>x=Z~s=Oz{KAX;q`rD!z_`W)o<`N#f0j>^8sGc??} zn?8Kb@+;0TA7;yrpr2g*Svp=!T+hln!Tn zSG5t>U(cuSryQq+lRhJVRm{jeyAMNlmng?yi=PcsXZW_QKWA{O-^Q}xII%)&+phjj zJ;26@BuAUUY?jAzyWPzRluf*R4iAc`{FYSTy$VMea{jT_qAfCJ`))+6SzoIEI7n2f za$-RU+Kt6#XQ$v%H2V`Gr!r0BL2sA%HIUK{%`<9|-2VYW@`uva$2E=d8eaqwtdtN- zl3@kxV-^n&HR|N`$%7#%O=zeOo`^#+3R2mZaedFgS$h~*Y`yxnHmDgsdN%db-jz>U z+nu`K_Szdo3?3>p%PT7Z=K2@~AeOXsrL;#l6UB&jcFotvPiB7^hXKc3~2NG)5h zMDrM^;)rYipzE^7iyP$|o+qH>fQi}MpDjR26w|dI|4C7Zx96R+iFLu@t>7>yc2W?H$Z^>%YHJQ*QX*KMJ!HyMmpH%ys%Kh^>&FasA0B?zcZ&=(Xa7dJB%M6H7 zGW)W4(fw>yY1)^? z(^+*PILZBZ#p~_3sPy0A!Q0@VQA#L}BU@;x?OYr5fL?h{KYh-hQ%{K)qCdowK~|km z!F|9GOrxJmT-30?(50Cc={g890tBS~NbLENS9Fz~8#+t_8!H;8C`pWUuJd3Ck|0f% z1g!mTqE$M{`uvHt>@V8oj!M~Y)7lXuy0oFi9%3rL>i0PZNf*h#)MPlNs%y; zu?hF~z?OL1dWN z7>;G%7rjClJC1-ZaeLo*oJ#=#6hBGKpk z;Gihw4(S3HC0dv^PS|-vYc3v*?@U1~ z#I4aUrD$0Ef+Qugx?H#dZ!JeMs`p`006t2D!A#oC*KrO^*e$$hs8*H`}*Ff#krpCoeodh zz*MjUQ|DDV%(lh+K-KiEQa8{$tsq8_vLGNaMLlz&15twS#Vi;;241r=rPY0JMX(W8 zJn-09Iv2qSgk7)qEt-v`W?vXpPL6yGUy&vq6BjUE;Cz*JqR6nD3-b9w%&j#+g4A^E z`#c)z$U$+256iP|rFpM<=t}CV|18#2OnTF_8-c?bV%nn2L>_g}SL@q+@in-6S#U3v z_jBNBFf1a23}`O=5eA|e3xgl&IRvrc@qXr18|-Vjx_0+cB46i!HPw3bk*GI5S|w)@ zd-}0t=#8-ERaRyDeQP7Z@g{-yPrXH34K;enTaRLNpvFn7o&~v_g%W%QMA^15HE%XG zO!4ZsM^I7gD7*fjrN%9`O!S=hQ*}Pdn}UL>wVQVj-qud3x0S#4qBCJe@`a=z24^>V zXAK;aO|Z5ll_n*%0JTZCBp-s^`2u^&{@|aaA6cE;f&N>GZ7Ve%VWNH zHjk$cLahsA6eunOCdVsB{4S@m-wUsoHu4bbS}AJYvn=kuhR>o&46|hdU;%+x5f+kD zzeq+yO`aT?@R4b$Gjqbb+X8B*%PIofUBNlFG2o4DQi82;$Z$~XKKbZ`(BkBeqQ={Y zybWcd%;dG@;l#?4X!G8O0Jn3oXHSRs*0n|DoP^8zmhel$#6g&&a~x+-M&wHhZZ33aY9?)Kan_LN7mB-Y&j^OXGIttHb~MDd#I z@o_4?x4>6L^&-J!lXSHbAiSrCSf7}qkTVsF{$4z$7scndf|n@rT^jL5D)Bf@F|rV_ zK)P0&fS5DbjgKRE!VY#MR=Wqd#Shm3rciQXW1K8cWO;7jUNj)+>P-NF z0wlr4`*LNaqkDM{^BOMO?5}wJ$LhgUV0Ko7UN3xN+XB zG)Rb-5Y`=kS-2(Y_c$QN^}(h-%$#=C@g+0%fLXq^b-weyuktHyQzRb$LWKPG{9#a6 zBW9c&0)``3)9>k*ocXFS*Iim31vZe%>tY+(_EfLaQ$K#)x8}1yXyz|#cteXvVp7V` z&fp|dHk{<$Qkt?l~VNngQlm-eJ5A8B%>*@3Peh& zu)s}fompkmYVulPL#lB?XFwBS9gL-C8yXXEjUG=h0+gkUT(gx;8fy)DvzHUgfQ15J z(0vs?{9b`Tri&$%x~s!0ZyBxU+f&*;K#AYM*r z0`kW``RGLjweNN{6Y_01?4Z2M2zFCeceb2nUXeSa-W8X1fsIAq>V4Yks+!wDQe1uv znieUT@RRUZLKPf_ai64{tt07eXB_y6NX)yKGlR3i2aj>L75O+jzv`-FTR{ivHW;i# z#Ka||+;&>G-7;S~hN&r>dyIvE-v+H~{aku$Xar|!ad27>R0~wHs#~tU_mM^6`Gm^h zu0`MOw21HA=B`(NB|!b5$V@Hd_qy0fAPMsSDyenX^f6OcMHQ}u?FtMJVp8&valn3` z3IBpxq(!Adc|c|C0ZKv?zI<_+u&jMoG=H#=jFZVPWs?3ulK79~GS2GdRZUR~M2+3Q z&mmjj>&JR*1h=snfzpObZl2k}pXIaMzkr*}ISi%=nQS2R__rSOBUQK$Ymmugu^OyL)84JdvqzLH7MUC*k$7+_!-V(mwtU4tNe0$s$VHsLQ({dosvI1 zy%`pPpB|Gz*EM0gBTTE5lrwT0lpWcSQlcpc z6QxPm@z7MQ`nOCk9w)$?bdxho@F@rYK;uO%d{`FM< ztH4d64iGE9@x*tjpw>NNq!Q|bt&O-={&6uJCF6+zMAkb7rT0AGP~{PtOb>%h6m#gN z{2P&tR{Dg#Of;7vJMu<9y(QS%=uK;UKg>of!x0No=gh?@U8l;$7@n-kYOg7lw(i~K zeK2RRFb7(B?d=PNM~>eV^*faM$VrJ_O~WN~`h~!e(_7Pq(?SPZ>q2EHQfupmptSsP8LWf*#;w>VTf=!yl!u*7U zhX%7^*K8G@pJPw7<%w-5`Pc^LC`py2P!B(SN1;Uf&45hNCTHpvoh>5Wp7}W@SB;4( z5VKcVUEq0y$q?mRi++G{lYt7b1prKU-$doXuq@oYDc}V4jg6ATDvG9TYAic9v@MoR z-(7r93ERJA=GX!GVEk$u)r97$G7ST#4}M+#=+@fD#s;!T>@qbp-0xl3y9dy`P@`)n zLti+E#$NzFnUD%TU)ebi#!7it`pLi)Gv+9hKSL4;Jp30uvkVdh(7}pXALDm5{A`Hiph`kj^0Q#z zC*-GwA)6MjD4kzOeGZ#uDEBAXS&D@>mL!26?IUKcBgB>#&50~3y0X=5-YcBrl{>l6 z%;vTcskp0G@Ubem)2CHCXUQjXHEK-9q)w< zvO#q~An@VJ(6G6nV|ubScwY=Ze+J#`)q#}kz+}g>{+4o+1v)tz8*V2dkR7g%FG+Ig z;luN)Fo{Joe>%``5nkXTjhA)Zt zq``}LdcuBK@*0835$U;M1h?cBA;=|3q1Z@E%nbeRkteJUX%{@>&{T~`db|z&8Iwxk z_x&;GlgcG-%BGTK4s;C-?j==F0qYQG0wneRPOd08$X2uKsY`}aR@x4ETvcK8hPJH+ zj$Tb|89p=*VPT}&NFh1HUq$<6rxW~v%2DpgKyo+i3o}z1{1wsCeW~q z*HV5kNvU4RFpw#N`$jzJ$oO5)2q}w~G;JysE0@Z+=O8N(ymX04{Via>;zdUSsI@O~ z%L8?c(%NTt*zQYkN9>4C(okN`Zo@S}cI4_nA`|C#g}w91@(5i(fI6T5Z&FLUe!f5H zyyq{|`X*G?+fzSwTMF36d_T4YNT?9!2>T|9NXQoxJwp^hBB|D68I@;cYrh>v0B4chHY zV}=PM_uoAuyo3T3$|UW!J0Q~{5AF4i?DiiODePrl>743Udh~tRA@JoesLf0z=VhMi z_JzA-pQtx1>y|XmmpQm|Ng;fd)mv0cLOIz2WRX%IQFYg_L;=}!qvS=&$g?a%tm41! z_?kc6C8?bgoj$?xbDA>=CFXch>_kPiW)WX#T)bQ0Ya2wlB4BZ>2n;>|X&wmCaM2>L zokCi?g2DNoTuD*V(^oPAz&Po!6cbl{hUzL0n{ma4r7|voxv;7ucOG!82r(wV391Gs zHkGAJ=F&VDN!$HJ2)EH>6`q>(&xnVIJPx2~% z=#_}@r#cO;VC`m&dSq#RS+23x6;x~235~rUqam$zq~e7Kzp`kO(qk%ygo6jS114kX zrb5B0GI;hGwIYnM?ro|LX(u^0g=uZE2`w>jWK$!FGaZQ9aFHZoi2K>v`I5xc(DOw* zn7y}lr@5^1HyMJM*G3?kW7nOhy5%XL9%aVk>L{maXsqX2bnAy=jx4LNsrxjnjogaR zm$eqnyjEWB8!>p@(BrQk&XzAWXo7wPqVn|)a3~BWX@?dM)2U{jyDhHN*xIa3Pl_F$ zEl0{9b4Mm=jV0i^+Yq>B*6&#r1$fb?zc>NddLJ(ut{CnzbzELN{5Q(T&%g-Of_+yd zmA1lP9h$sacBJ_67J<}bzS2#TP6U*RPdt}&Bu3s?r!H|t5(n=_LHJenUn&iJys+7t z^ml3a*nW(Jp7ho;Baf4$(#WcAAbt6P2!GA#=hx9tq5pgzmblmc1>=DTWWA0? z|D>8f)YRt*Pvmip6f?FGwxZIdD|4ABs*@yPNY-+dQ?ap8Ue#omLkedgj)*Usz?l6x zB50gWSFgIN!PfTdk4=S|7L;Ti>Qk$|&2TVNzaPcYbLSad9M#|9Oj#E>OEK<6HwhcC&XO6%*-W3K_O^kPn=B%X%2a;Tf z&LR9Il4m`HsTPUTjz%C({v>M8?VaQnQ>U*h6BsjSC~=yfdOyi!&7c)9r&cx3UyCHl zk^xPQ!`hUg^n5nIBg{8{3>#$WKv*BRY8HL2d_Av@^nH6POjR7Shw0kVRs;|9TB|^6 zLYXcwYFml*FQ2RlD7Cz20?H93BV=;2GU$Xa=bp5APY1p4V{cLwCBI<~8zX%dLza{c zOF}KKQD3oQ?y5b>{&6Hw?<$Pk?0-E)70HvzRx`D#R<=4)N^7&DMJFH@;`$aYxVI6QWe-DzbuCP5Y9DAJ^eFgY)`Nv$-^qFn$4sy zYVrX~a7$=+WmOhBe5w}NP5yFsamt_&+ri*%WZz3c!eI(0{zzyX3HfM4Os^=V?#pn= zD0@k6hoXYn2EV-#X}?=nwiIehRU_dVHnGNnPI}Y52orECDPCYooGe3*@LJP!pT&&t z^#|S8&lvBrMAsVEvd|p1NTZdzQC<~E2~Lfxh`3L$tloa}=Z*E3NZ6(FBYx)sIGr8a zyewlhTEhcmeRDR+cixi`P~$g6`5z20;&X+iGO1-%WpsV1c>Q?BUq1gv7x-wH`^rW% z*oQc6V@XXY@N^XE==*xjg;se;RBr#zc0Z~2yooet7sU@-f)~|E{>O4>d`&qQM<6Hd&%h z`7|Z-tzzq3L*JgUa?Peq-Ck`i?zE2)hTr+8#W(qU8^{ARNrKGrgXmwGlDdPxETbq& zW+mh@iLtc2K~xhOf_5C-3gUajXKNMA#rZm%MN4~}bc=|O`uRaSDwBk3(XGUmBD~3& zaU6zwl^@M^jm7}uj5RyiB$|~JJ!dHyCOOvqM6T=vyga#)t<8;%K?mZtg{)KKr1!b% zP#wM>PCeWUnD=^D`pp`e_I5bh-0jb$IHyl|8C9v7aV)}W5qlE)tR6KD-LB5XRU7Zd zI%kLQv~?h#wWU3*{exi^&)~(&RL-lGSj>!Ep{i&xx;6sQ8?R_abJI4LqbF_ysn)oQ zo@_YjZ&gw~*Eyc& z?(xQJE0L6dU4+RH1dvG2h$eqiru=hY1Iy~~QfZ))gH(COYg+d;HYpu7@39{iXLz`h zD=LSP)t-)qlpC1pj%W=OE=CdL%=G8V72wb<8y8kz%o1Iq->}bQM%61yXD3*(poo;% z9kaCL-KehWQqvS{-P_U&joPi@dQf(MUcY_W(5rT63L39KTw*57l1l_h5q(k}TPuzr zn83rK4Q9(!1iO@ZijYQ0OQDdO*;|&M%gpE%>)JLgGidnONO!NrM#PiwUt-R04cn)onAtNgI zM`dl<`kPm%oGv3Kf{QT;yr6(5_Vnk;feBxW{qvJH;seG-4{&5LG+@}+>wRi$w!=Dk zZ~I*r;c@@USfkXtr2e|1Tv&=|a0>fC-ZB&%LASK0+9#`&1dccr{)S5aQvJQCX7;Uh zdYZ-8w821Q{Gg~Jv{`9hx!a9{P8XJ`(+*F(P=>?xT5btyS@G{pp^>ojf$q)(tH;rD z4XYqj@4cUGHvGPQvt+|G;ii7 zqj}WLU~}Bgu-Ybfa$Ah}V08>bl2>m7qlG<^+-Mz%csoc1nCY3??dkP5T%*#@k1kc6!zgKI{HlCkqx3Wk9N`|s? zqe)->f~h}ocAXfzP~8kQEZDX!WQK_*;*xC%tOy@@h^5BY8e|s0n&E~!hdomE+8&d; z@7LEq6-J(Gumm_1wy*XZ&Q_@y-FY44l+|`|i|Uq6_$wuycJ^sco!m;8Z7rpI>~n@V zVf!c;NTqeQER|K9wy@f{tvsCv?@AkP0bgk`L?+!3FukE5`|)KNH1xUkrwEF_n9 z5^obDaLDpAq7HP+8}CLZ9f+VYySL_0N$URoTpu}yMnW3Iz{6b%i9%JdwY(+du_n6Q zR1NXGjj_reu9HR%H+gbxPl&WgzPzNz_Nupl#_PEM3+ugGI=x$QH%zHwH&F_2w z=0P|A-V9G83?k;K3oDZSZZ3C} ztQMNDARWGKXv|mJQ*ATyqx5M zZ%2f9cEa5cqoA1>9M_$fw#vFXT2t_$gIlz2MV$Gcv4mlWp`F|}*r)?fTOS{b^GTAA zVQRqUM0Iq9Luy1k5wOp0To7myARW;pQ8%mFBm;*en40j5eum~eR!Nuy!v^tYZd~GA zR1%SAy{xK3qer0CSUSKmy6-bj+r|PQJD%z1MxMxJ`%23tH++^w=+xZdmZ)J$AJ$AL zp!qiEZcuTE-ZbQnlf(SuPPgTKXMDS|6#zjXAYkWFbvL38FS<>X zx%cGKB&Y;%IoKKwdYrebE_Y0^eJ>4^rJFi6gT3#MiE&z9J(8TRIVG4FFNGrx42uaM5kv%9TB%S`ubaIk4b`|O9_A5I(*Xw{my$E}6^N-{f9?2i1 zKBvB>tp2J-0!9Gm@&?$vv=|LSVJHR;j951A`SeVBt|uaiG<5zOtDU;q#S^4AVoat^ zJUdl5nRI!}zF#Zi*z10ZR!P0~(vGg;>bcE?!&Ywu0Nt4mxfqxb_Y#rA{|Ev+;0q_^ zNB_oB@kag14>oL9BsDssyphEM^!S>r;yK*$3Z_*bzDNbL2l4zfsq~SsJFW3Kx&`M_ zWd6I-)GmERerE-~36@;TPZ#sIwRumb5z-r6EU%yPf!$_VZb34oi{nSFNM(Txo1x zs;btkRe)JL2D;@;8D(t5n~t;l@5$$;vP*H^zW?rTikp|JEOOUQ--?sZP34arsO}4C z=*IUdx}v*!7%>K>dB8@(Rva=dN=`-3eqq+!D0Z&ZZvA4%DSC}vea|0FoP2Hy;604| z$|rNw1;ls4;+4kAYn5I-ch+`i-PY|j+?5mA|89MHc+P?_9iUT&93YS=L|SE~`+%`o zWgX5?h^0MxT`lrXi%86fMc1Oc29!P$nS3A~&C85!#`}opZA^sj`<8!I-o9K%SX|Uw za>giLQ{vI^HX$!R3IZO%KfUkUv;JPN z1MeMX%93V|N`r>iu(Y=tx#w*Z&qYpsu>d8AcP)anaUW9Mk$CJb>`9=}kc-zF8|N#n za)&W0&G!5Q2lw?(bCg}sk3Qsms@+QyzAofoNO8pTzC*xV4K{;togMt z#(v?_<^oc>gj~DbaSP+|t&)1>`7K0X^u{Z(b-4yY>c36M z+EIAvL@e$_d!@YG zyz%ljtzB=I1m zTQ3*O%PmXVK9g4wrl-Ysl05Nf5ON&s4<5mPeDAk-Wwi0S*OEl4Oy)$n-3=Qs(p7Zp z7u&nHdIl(zVP-y-U5sasBqB)}0=Ry@eCIbiz!8i_s*(!PA z5f>!#o|>JxsI5+8t5o0G)jBHPFtB+NS(B-m4>QNInH-8Na@W`tX%IqLmb-rq#pKgp zD#%gE4H7~7U)<~;NHGqW)gtY^%~pH$opR|;-Brd2`*Do05toM1#L3i&k4*W$x9zLd z8$aJN2Y|Q&BM9Nd$>i~mOdX{DM_19!?^Se-BP5N*4&)Wa7Dl7bmI7Q;@<9+whZmpB zXBY4joZVZEPF+tk@cjQnj3eT&7jpSuzMs~ z%Abr+KbVMSgR>`tlqKQ|`hoz|*4x*=zvHAu$dO(Ek_3!!;pY%CW3K_jBlwT+{g&rD zBkEq0(Rg?Bp+8uPBzrVbscY9>-mGu8v6Vhih-BvDwT)J7s|^5N-ruH}h_9O6HAz`o zdN#`^fPLWLkH+V{3w71b{HV9CsC?c%LkMg7)+aJjLm0b-7jG@|d1$tlUSpELRl&xy!~NwSof z%q6CCkz`ynw87CLpCM_(XGY!9F4yXtrDm~W@`AyTBb(y2`LI0?BLK|HOM6rZn6$3V zE=7jwfuUqHd2eFou@swc5jQqshe+($X#0BY`cF3jKQIam8x7vVVb7A5JCZp4$(cZY zl-AMT{);=hYIFN{0Y(tQqaU1@Je@j7z0mrv8@06#yL*w%2qb2J4k($LC>~l0gfJA5 z;)O_}5RK=h&LQwyP?@xiOlJ!$>leJsQg&O~fa|KCQc8 zIgzEdzg1a%t8AOG1DvD)aJcQ5EjqyOY6KlUsUz7XuG4nx#5)Zk+Ms~w`Q7-NUa;yG z&RMw}lJch#)8~?rw9GyZ4Kmr8Z;Au~K;Y_kx4Rj-x^>SXUbI&gF&vQ4d17ox97nHjG6`!ve77>XRfX z9FvcJA{S0b!>MzlRw^s4JFgZMllmqQ@n{Dc7;!lkNgKck6_*!?E%{la{DCg%A< zt%}mv=`^<6_3ch=tEDM=Kb<8N7#Vlc9RbgNem0(u9GG6HUD8Td+x2xtQ*EEgIU zC$i3{DSt%Z%8Q%j)rP%3Tf`_$HP#_xWDN|WLL!8DiCD>Mit`amQZ~@%3%CZwJV9f8 z{=BJ{xoD9Tk|xh2ryop4QZoOb<0)r+paUNvmpcFel&&}L{Ib+1l>o234xBjNUTqM2 z&iX+(A5q{D{Ksd%;rU90m|KF0V2W9L=ns~n$CQ!PD)_z;rzFaeccs+<6icx>OtPQ~IUX+-KPM%KYjwR&C z0BSbczZ1AFQX$YRsyDvB)2Bsxe__gukbp^oTDQl*8%+i}uR{zOV@5^5sebJeedZ-& znsHbt8c9y)5>t6OB0|rCr$vn0-E#EF6ae7%YcSLd#fZy$_3cjScB8)Cu5Y)sjt&Ux z^Q6pB02dY)&qdCBepU`2C`67l(AZRJD@tv(W7Ib;V1%Vbj1EV_bo6GzVVlX)lM}NK zWW8O(*4CRP{86+CxS7#{Ba_>DTmqDscRR$v8uFG zfPgJ34OsOkfpbBQ9}+ogHN@UGmJrzlu(21#2Fek_(5Ov}x%CCBg=*}SL(=5g)XYPv zaFRdy6!g=w2K$}vvAT*be`~9QlhCpzTefwlIJaUJDnxiJ5Hc-dLCE@(?fF9f(b;bR zvP7eMO@cHM=~O%FZXf^9bi1amy<4LE61tO-d9lb=l}Yhv{bfl>${vl)J`r3B)sZ(X zS|#<)E4$s5Op;lEygJ5b{N*Bh;$4DAXdnvW(v`4 zQnaZ-Gh7yI6wAnGKa&Ljy#G?URnZvn&?xlYx~4a_OZAPS)8WV_lHFTjvMC; zt`nq>k_^v$ZZdF;az2eMy6dY2q^sw*9} zt+A+I)7KP9ZbyO{1rLlsZ|B0J`T2))o-fgT3$3zp`Nj2av;o%n4vSOwHY_+GUI=HG zVwuHAC@Jxpe_mKnF>CzBJo*0tqJc$swhQIX( zR-Kspf~j#KzIw>gMZ4-2$zf^gzVz(5v?a19IQOC<_!ha2s+;HQYj0L;k!bo}Yyy_Z z+hmNegFg1>F{(~8x)gE2qaHA#osbHTtU5+S3s{l}i>RiP*q+p0qH6b5HM4iI6N=kB zj0}y;sdI_^>DT}|{!ud!z|Pgi+FO;bz8z^<2?+P1Lj#Pu8_9|2W_78Mx|N8OsQTiXkCIsuVG#sE2I8PWX*0CRQ^l6R6ziZnJ<6pDru)A{6d zUJ`27LurzvowjcseKJ`%76kwlZ?$i~R>yY5{_|cBv~;SC`bM$2Q#SeM+p$r9+}_(V zu#wqcn3yF^>J9O!ndpg6=K-XcP)YMVEW}TLa@uz0>U!(SOB)ng1RP#P6ozY?@GO0xFmca;=H@zRY$=uH zc6GU}wsmhx*u2b07?56o5yCW$>9d)mPfvJmqy{ary%%FXg;j~|>NGpOM~q2UVltdP z9?cw$dbQJhaUyfM^~(18`7*EuqXD;&G=w}tBCDl`oH1y&Gg%KnEYF9Igv`dLpQr83 zy@-U0LJ@iT{?zP4nNVEvKFIhuh0qu%<{iR};rRus) zQoPpfx`{m_L)ZJ30m%<2P*j9LLJkImK|+*cV)=A(rVz`gMI#z1S_B~sC#3s6m)$cX zz{MBJN<*hpq0wYU003Q4>zljv^`fRISUbTM&%UQ*BpDyABX~bBP1-9E!V{k^BxfU# zNPHmKDgtC|1Zm_5^3{j(N^|Y)Qgx$c9*E6;>4rx-*8cHX5BJNAVXXF~rR;7w``J0$ zn04bPo5fpodRy=ejO37X`ZLqseKZFYur{2%z8XprJo!5XN<^oZ3V;|nktye_ ziq3y?rM}&=L886DAmoHZ7$Cy~iER6wI@Z`Pn%urHPYM657}BVDwyfD5LY>UWyd8?l zvkzuw&Si)J#k}G{9Ev+E%`M2IS6{1aU#eTC4n5x@{Jh&1iG~Q!gAEKZEMs|!T*Jbn zJzzw=AY~Wbk%Q~L0J=k5M~$Rr#~?Z<O=aVyqP%I*v&+vf}%f{j(iOEy_rP}Rl?d7)`uBga$ z&bFU@I6}>0wZ5^_F4t`yMxd`@)Qdp_4i6(EPHoRkxbI;EAWMunOYC3lI^K-_2rJ=q1wYxS<@5(8#a@T;c zNFK|C@A>Ss6p;=~w}n7syHmN_uH0>Q8k!$wBy31uZ3Hppq5=RUr=q7nF%yc(%%%@) zm6Xdbtt(9xSYtMw8b;mYG3wVCUlPm5`KQvErRctJBJ*amq`dRByGCdQmJ=bo|23Ht zQ6Ukw!y){Y;o%^6+(y3#J|mgXSDSwb$-PM0%T3WmMRN~jXDO&?07DNXCAuFFMKT@Q zE~{6c-_bSQ5W2R@Sy*L^oL!~ej~KeAEE}Nn5fvW6e|Y+@$Tk*}+Y#0rkt7LoLy%~} zV1JlX|H<5`A%u)bH(QSea85=}B_jx7EEhiV(KM6?U|{XzxUZv)cWT>L>PTloMXt#& z#7IAmzz7RcAN;+=cy8nvL6B7oxsADdyNXwuYj0MJ&b_yxJCmeYP>~TsZq`jSfR1gw z$W`vz=2C=XL-xb`pdy4oI1x+DO(Z6BBJtV2v`5B`P*l4A3zp0bx&|(Mx2(2xpUh|! zx^bn^s;w6rTP1*W%kT~RIV)_~$TX1m262NTb(ztbFHR+v+1^A=RbfP zRgpiIIQpT&A?(2#JDu{KR{3tTQ`0=i8woSgOB-=Gp11-d06;7gKK;p=ST@Y~kS}~= zwWphg8yEH(h+P94+I)>57990T_DF30=`_s>W2GFlKT`#sd;h)j+RmNVcPy0?;RK8T zu;L^ykcc9Im1E00PEUM@|>;*#{46>*>l-YR2o178OGOG4>uil`zPh{ z?IzgEkYkxp07RN-?~U)+HpswXAcGOY?|kG6kKjK%^)+^dNYbgy?&Yo%S2{T&t)v<7 z$QhLeOd3ffT7>m5>i(NK5}hFnV2>R;YFD)7UzeN34rB6EAeM#>0WdQDedG_1hU4<+ z+Hivxx(Cwy1-`;W+6~ZAQ7gWUW zQiKqQ<dFE-aNwCJKOV*!kM2u#%)o4d7*qSjWR zEjCTEU?al|slV_eSqAa^iNuMIPZ*{J5&GD)uE{BBiQviI@ak_>ms{)al092J%Foo4Ayvt3dA2pb7Yl8!}KM6Us1X?*VC+`=O{Mr-Qwi|e(Gmcz#ZVv@=m z9(j%%Acz_53B^XFdwzEk)6KXn<}qO;+HbU+0098X%Z>BjUN*1BSpA-!e5aY z5@C-dX&W(zo_U>@@wgY6drms9G-_ic5n6aOKle~hjz}O#J!{rO2d7m;A1><~o$D_Z zjd_Dz)WSLSwk{EwgUzw|(Y)}ZLBb>W4^MrK#*-k^Ymo?C%Gn7rYA=~Dh;I*$s%*^A z`t~J5R9g4nrROq{tjth#Mt$7bz24e*r;1IR$`XtwhWP^zjE+4sIej*Bm{C#pEcqI%XF|XJTD*JP3dQtAc5HU+f8YM+_SUk~9ziaN^nN$&;z#t@^be zZ%|+;OR^M3U?j=#)TgH6h43No)|$IY=}xnByU{FlxLl2ZSbAib#(NkwjDJr&JC#4q z$~5o3wzF}e;ut1yfoWE703qy(Hrm?@uuC072#1Enb$hF%kz{E8!Sw6{nVvUv_aW*Y z8#wek+%N!_0EaUKuD?*MZ*>qrPWK{PIGXJ}nq7=dRf3ZZv6uvIj?!H42>$((e@Wp< z>^BOX*+zOb68oDWi+8tE0?LVLyZx0-`r6nqVIq7;nj}f#xP0uhS$-xuTJ%Wk=xc9O zE6Xi!s7Ndd9c*A^vRo%!@!YAUvG_v)h}A*NjvYozy~e6zLsTD9v{)a&RSsL1$l zugU066WOp!5E&J@C%GrP6tUnW;xPz;P%M&~ok&dQC0P<}WPgDX-MuIjmEsvWnwDc} zIhvN^IXRNVzp_qUZ18g`@+K2AFtOoTjW_B!cNR=)+0&80GNb1x*O3{=+tMYxQ|L9>BwV+ z>H9Ln*6JDoR0IG}oBE9xch$Dm%Zx@wMMj94t?d{xMt0#LfD11l0xEJG5X~f0a})7G zMl_9)qPMmtIsZT+GaHGeWjX4-C2DQds%n+B&gz8*VDlIqO<>e5x}w(BcN&`|UDYIW zt~3ZNDbn_^ksOj9{DXy9CQPv(284}(SrG|=%5r=2y=rq;u`e)MdyoEZNX2>3zK7d< z95OZnuHNFTV2J*aZkb?ZSpY81!uUu}4&5~rvq$1fAIgi)%|ebU%I>ZD_O)uOtT5^s z9d=_91v7A%rMMu3kNwSK5_1}pW=VPP8+XkvMg5jy4N@}<3|kn7lM~rm4L~583Z41# zv`}QJ4FeEHT}=qzCpKz>VX9L**J+{EW=-$R8v^lzxf$%kizj zP0~mT$)RXOiiYJ#?|-3aSc-(?a0p5s(KdVXoOm{L>e+NSE(O>$dgQ1(i`9&69cfy3 zCt%9_z!Agb4J37+gRY^g-`Z(cRU_V<;$DORaJp-_CqMPWk6Z)OydBU5Mns;(>2`#z zsjg~AbjtwYdRfzv2rtAXNfg~{SvJL^J)4bBh5|HxhdKc1X!}BA=Srh5*6UzKHdHTY z(V+!K=5aka5k30ML}DU3%BaW-CnBSw?&pe{ zjy5<{h>IMV*K$b?fUfeF^N(c@vzQU(x$oSp-~RQsk$B71 zsyuY|q}K#h&LP8={4XoM7oYkDTpkL z)rjhFhNpEup8ouFJRjn+2Xz%)e{s82R&A)r;6!`-_5P$rC#;3*E}})|Wj}ylVhI!y z(FqY156O{mC>{+(BXTSfibX=vh#U(`a==ogj52fGb(;G=myf1~?Qw}TUDtG^X}YE( zP1n01Nz;+4;n!3=fh5RwSn+am^*2>}dS$EDwF4QMgVCHX8o3OA6n^B>`G^@F!M}I> zFIZp@7dhIaLNtEoOr2(HF!Y!;r}VYaU13IjvVc)IE}#5DVNi@`!0f0~)z)6CwMq)b zu}DZ431CD@Z6HZ-`o8S^!#Sr_F-$v&85Qwq`(b5ctIM60UscTU#=6$a0;8F9uWjiC zfl0~8DxgS|peCati`l_(3>_^&DLI`_%?Y8RaRNq>q5QgWx0!rx=!i+ z*$coMi{`~f^OC+T7|jw$#V;;7n%Ib!88O}ruhC+2&;Sg{QXqtqES>nogb*{rmPWC2 z;rZoml(%dDb6BiO+hAI45!a#X70;`qO$p3 zE9Z~~}hY>~q#H4`@`cmB4nFrIh2SE?Ey8B9L_hthjdr~u--w>le zk{xBovQ^EV$ zHMK53QgvO^`Wvdo6X`}Cwr~%txuahD?#|xSTekosgs>h))-KVC)SZnEx((*V}sx+BSvj@VHVi=L6qFB*%Atdt5!;ZSO(keV$>;gDzuV+4%4+tH+a z&vV&G!n;M-u4`9*Trp_T0Ru+ejZUpmTia4I&P#imHZx)-_H zTdrYi}x>v9aAF(lU8t5#Ao{I5mh@@nm%@vi*tCTyd^GI%nW? zKO?i4(aBFuI@Rb!0RUWjX|=p!kBU8vzz7A?;%qKOq|s#lq5P4@Cyd9sb*;Mk+uiPP z(Cu2F45v<~W*^MO(mmFB^Ou_&m&yo7Ow3nAy5-WkuG?)$U}-hL>Tr~th}`q}Y3wsW zxM#IT?CtD5b9NYh=J1IrkC`zNK+t)?MbWB?<8 zor42HvI}g@@I!pKoR4_n(ceA#=fJaokvlnpOuFC9@W|;{1WdV9u=8O6KsNvlBYiDN z$3B)%OojKOaeDu*)AYGu{Txm}}J3VN~r)U)%2P)^%hP`RbY(W~l0HM`l!nY;C7M^@9)m z{(LwtU3=BkrWtDa*hz{!y^MEX7gQ{z4U|8dLRjyGjkZ`h|P8}6C9@j;fji|9z)n; z90#CqB9>m5N=#%&Emxx`9)J-5Aexfz`B;V**LE8E)t^?{HI0Ib4gfH+P3Cn~tFIU9 z8#_mzDja!o!o|L^TM^LJP9jC$JM07q09bytw0pB@Sy!|OY8WFgaOO$rJEfbV&ul?& zMC381*l2ImVZVg8kTmB`O0G1@TBGFfRF@=a_TkLLJwhbTQpzih_rJe-NOiB#z&l1t z4|(E4)5)pW@*6v)!6&vMoq9QCO+B;G=5ZK;^Zbo3Xzgn#|zX=|HNXEa0)Cz3OQo2Y|OV{X@?MBoAIN* zgvpR~2u}tXnM>F9V>)4~T(AA$*T2&R;MJNtVNE#qG4g&zf5qR<=5w+Pw zn(pc%&4i5rHF%1Ejjn%xr@q-SyBA@7i_F-_f{IMd+u;y^NtXur^N<9Kje;Ual(f12 z&rhn}Sm|pXFinnSvezDnIi8(~TD7arhQe+2ICnM`NUJKT? z!{Uk4$4M492TR_x?)j0m5!vm@{)pD)M8?O=V*K zHD(}(f)*V>VC3XPbjuh3Aj#6q{h8_eQ*ba*%7}pjxmshi8cNQR*4R*P{bX0)D~30z zS;ollD#B)R#gHRh;xjL84&1f zxmerMI82h^BY%8U4okQ@>V4k4RK5Mmj_rO3{zMk-YBNPL+he4i0H1nKDKnU zbVwA6MIxDGG@XcMlcVfL6cjKrca^E>@a(h<}H8QJQ+hiBbnu|8d<6uMXsK{czkPnC# zGU>&sNHQ*J_`wE7au}Zba)I$G?|!SODd?~PBhXWQlJEOW9-0F9)Mlq$t!o{Hf{g&q zv&auN(pB*8e_Am?B0DxRRum0vWKu7(}gfT7#Izb6EQL)tH=?qGGpv{+;y$S21b9^ zej1BNJOaoRhGrkyZ2M1tVm5y)iFu9)000_0or}+}>Z;vC)~aLOr;z;^G#7s&jIuQb znIWt?2mT>oU6ABxB$`devdL&Bc>uDGu+gGYaR{y#uA-y+Xq*kAx|N9NEriiVLn z5Zxko4dK+;)ZAlkksV&)(ct(|yQ1CvL6NRo#G4$AX&{GV(82i`rw>m{|av};U>XxnZ_4Riu7R75*gqjh3X7?=BZB5BywG09kK?viM zx%B*GI35)g=3oFLN=pO)q$b1nd_2o|l`AioYugIR?x(BNjS(=y8mUL0OivugGzzuV z=~NqPyTe3$d}E{Lu5#&H>t+r>H<56F9-pX z)^-12BP}&{8Pds#jIU3f$(($4+Ma)8uNIyE=E|OQ5uW?F`V(38iR?R(#fckp4|YFs zKyW59t5%2PXf_qgrpDKk$TKryNuiU-ho;ZQGIOEB1JgI&QOnD!F@NYZTAg~c`@eRr zp|(2s?vbv0O`tXueiW4R5%y0#*z&jM{yo#3d2>4g-u0R2@W^RmN)krHh&1X;l63Nu zQ^{#g?+Nc2Y6Gj9N<+W?!j3H%4I3~CoLX_7Y0)V0qoC9`4<0<~4%n{UJs^7+fjupG z^Pj@;(A*=L+|fAjB8DA+O-6fDOqxf?)`H&u=H^~VngtbggVfAkMFu?5ZIX%zV}kIb zBI}~o-k+cZ6O;M$!el5K0iqlTlZ#?_85@%);!97Y7_V~Y_3HMG79hA3k^GEC9~c4m zz`fA@pDj@OBCV~ot97;20XE4Y1lkp~SyY<4YO|=cOG;`sy7b{Z(cYrUT>}Wpw;R`A z-m;IdR)-@1shQDC*a%pYz*?*_57-EdS)7{d%?CBaGp6LDNM9QZkdh=VJ)4V99xgGX z%4+lWtJ_B2m4gY4hR%sxnGr&`0~~&H_)A%LS^~A~*;h>-4}0BF9~B{Tcims1v~<_u zsPx$1JPsVDDFEQ=kJh)ZRn0f)3~cO+F6oY#M;&2dkWInbnab8&KW&y6AcW) z*;Fi-4ku%W&3XZ35k@Rt2LQygp{X;`>_Yf3U?Zi0ZvC`n305!_O;OvmMyJ+n*PCji z)oHZ$1lsMk8Daw|kU?w_1mt{V^B;nLYxdt!coOMrELQ@9j9DXv&Kg==4oGAf?<2|3 zy}vsPeV9nzO(TEpM{Vd=zqd^nFe0I%0gm5rANn$Zjhx+#yOI%GV48CbNYBRR9?L}1 zGVd+?7wL|^UbnQVo;$S7aIvByjMI>PP0x;sEP4Tw zl$_3|=O+)|%9Q{{t~6xv;nWPX!@|bp=JMM$z~)$V0DzIParCkDG((Omt)n(}YUR6? z#!jbM>@;>d&0PiPu)nYbApoFoB60NDyd*;wA^1_NzcY@k7oTs`Q|UT9IVP=7da^s!g|!&@81^M=Cr}g>es-zyaxaPAQXv2 zv#D4v70o6kSw0MHJg6B&PuC(PPo9qCj)c0QKL@z1zpIq)D0GRK-aJCnl}4*mYqskR zrQTE;ZC%rhkq)bWPam?O+lrUlpO}n?-YfoFv;P)<8xV4xj*f@;Jfa!gj?DG1w+e)< zyAsCBNS39kv+0G$b3uU*=p%BC_lmlrU;fsXZjPUE!$#2L2|v8S8xF7+6WEAk->`@# zQhIzqmHd=L($syaiL*&s@6VFh3~~Q7&hO(6G1&&?JFQzk+c8j4H{{sThIDgwOam3^ z78?uMO&sGt*jO`S60|NVlBLv4AvHHChYwc-_~9|RT%C+geI%Qmi{R_6qrsKeI@f+) zW^peXcVJ|DR;NFl%PmH+O};*rL8NvYZB!ZR6o&==CHYppxK-f7oVpF)`9?$fhYQD^o-*H9ZL9U(H}0Bqi8wfsJ@=VS zvRsnyVGcOYRyi)snnaDdb3w`Jta6}aX#WrYH#`O-A2yM zSGOaI=ykW+VuyNTq(A-X>Ev`2_!24FA9$oI`upG9u2|E@P@(kUmgzQ(#0G* zNW|OYJG*}1DrRIV@myr?v5eghA2HpoFI2?(5^1QZ*N8N5@#||!Q|qE4!<7{0X9Uc7 z(*Ry@(HAO`LUMAZke-{ALWg1rO8_HMGd%aj$w<=Ts!-cjSKg@u02UrfrY0>UvD47s zeSX_Xi^dTc0b;IbjFFm}2p@ks8%xWYf|_NuS<<@ywyV0XBCH9kcht)2R&%?IblstH z2mk!mw&^AyB9gXur&z9x|orv}Z~h~#bL0UH7O3Lh9V zhob-u3D^2Yim(9AtSKf<%$^|T{mBC?W~3=-?X8mGSmdgBFdo2YBsmcxd5cq3icAq| z?wrVKepuHVtj`Cpu7ZFQ3E5nVfJt1bYcW3hhbN2^s~zo~|8l!kQS5PHrVq&BqiuGN z+LHyadl;E-8_S7om$RswNm4AAj_1>{T>6k(V&0sLTqhBsm^67ZoI4tp4(O8T`g@(- zTMEgO!?nZQtJ^*QqfV`mY@FJUy5V-iYbtf(IP&F~$@5Anyd>NV^?%Z`yKLE>L zj7~=8x6df~X||j5Clb@=l2SjetV}*VTxmm=+`Ma1<;#A~_jR z#z6`*k|enF(Ofil0K4(pM*Hrsi%!QPM)#u8>VF1s<)D5J_B4=im@^*DQj>A#vP7R* z-wxKgvb{%hJgfy3;cnKs^Wm=^OV7l+=fC*k^3IK_1zg1Si!yUc5g~-B zoN_o6%V*;GbTpGX@L^N_XAmAVuJ@ZHAQ2LM`Hw; z!?W$YU6e|ZSniN3B5$Z>4I20Zbh?tUCryKthSXJXn2XLloD3&qK2F4giV(B4#e#~A zV=jMhv%1kVIFZfC$nYxaMilS0wLT{T<{&k8R1}ia^HZsrfkN$(nRnm@0+(7(0ZAm6 zqW)iz-5uK&Wrn#dPP(YSZA7aXqDYR=GY^~yScHo3oCsL6q=n- zJ{O+5qNur}q5N@Kjvf$fq_)u2mus5BEfnKzZmu-jom!()ZFXu6rP&6ijH?5JNAUkX z@&Bcq4!WrtfkC<*0kRmj@ngDp2<*8O*~pPI3O$kxo%?G?m~0GQQwDmqVK^h#E`EJY z?P&e9-@to3+WuR3%$Fc`VVt+aXu$+;*$j7@h8%e`yfGu1CYWitVDXu3d@4-(jU3s{ z5Zkec;eELTse$dw)!VP^IGv1)#$=91Ga}T0$rsv|v6Y+?4FCY7keptaO3f6YBv8@e zWkz&(1o-hc-R2mhgw_nH{`is!=j{mw!(9kwtU%TNpf z0Ogg&g>T<=2B4WakxlN}7L3-XMTV{6G?S=)-#+Dyyu_)2fjAru#IE28c&E!4QJjpr z+p`a+b0-goh7nTH`n#2#t2N4UBpnrzI-g&nVfV5469KwQ5tS45H*gX^SU)3( zBTD^ktCBR9=JXkrl~37Hm^oMG>@yn2J)4u02u?HzU=#>5f)GkF%pM8lkISL>LBK{E z=iA%Y+mtM6odVxz=f@L?w2HN{JW)ruW0tW1&T)E0Wga zZ9k30j`7lH_j@kpyk}y+j{snBE5-Ko04D-E`$#%>BF6hbnCKAJ)dCUw>bIck50LbA`dSOzaB4KjT@em6(A{iDe zv}nA55f@zZ#*d6~ud!XOEN?2U4gf%sVKg0j;LD4#Y#2Z%DcZ9OFO;wTWZkJ-1WbNL zCb8IFhieW-Lm0y*Fl+?8#;FkwU>l}`uz!X|j<|HModQONRVhjE)aNI=QKI8s8rz-a z*GgSMV)}%~l@@`a(dzw$M9AYI`12zdONpdXMBzjfRAhNsI$_i>MSGdlwUKitR-D&q z6j`o9{Q36Q<%;1|q-mOw zC=Kj+6**CnO_7m~iZTmR$(e%CQxx`=A5P?x9E~Y3VtG*B_z`xau~V)rZz_#803ezU zJ@~sv5`~DxfHD2&)~j20-q;3qP6Yd2MF4cW2~&mvoO_YOIwE2tKwAkMLY&&zlOy1s zb(b>mkwmXkGqIx|$&Gt|rE6&Od~N%34Vy$o?+=15atA}A)dTCsm~PSsO7g~uY@cn6 zhRL)dxfJPy@dDvdlv7j57J7!=W0Ewo+iE6PAhJ5O)uEfti9)eRVltPQ5S(btm=T0f zlBLwFR5&Gv$Jt+V{bFnLa%+Eqk-2D!+OE_)m3q6{=rq~@Ar^Rq^+7@gyoF zVicNTb339C0s9B@0g_}L>7l*8nuqylD*W&t9JQ{(c@89_G_%2e?fvhpHOn2R=ylgH zrLX@$KQ(gj8B*A1Ss;ikY2(O@xNix$%*1*Y0HLGr)+;+I0`|4mnR5UyK+wPGiL*)I zo`jCu&O#U(WIvd$qU|uM`v0k}w=X`wVl*OMGjy+Yu;)GGL`7!lYirt$z6B;n!kLAs zF{ zQZ^|TnaGiq6FISw(Fr-tDo7SH!fFlJ;<5w4MpR?$fRUp>fYJQpx%~0*jz?=2mF3q; zZL(t#qXx$S-HQgwB`z|UuBIOMs`bG0gVgt8WJCZ7aWYsV- zB#n$Rf39%!sR^>N!fYE2p0iMlr1!tO*4XVJgmgs9k7n>ATNMnjE(WW4>4z54ZAyym zGWt_25BGo#fOlm^W^>v6(d4a1?Bb{HOHDtJ@?dE(Svm|3L@Z{O#mQ*zukZYqyN%s8 z+LJNTt;x~Mir3~uX{Hq|-G+)nk#KrpIysZy=WK(bjE}wLZDP#Wt&KY{0&F7Uz^gkF z_PVW7wX(dWbz0|scPTTUu#Dso007?j&(~|4t-TJVH|+MP5X5$8HX(CFfk6}pA{sUV zoN;P^?EMXBxmS3xQBbx-;618=+gN}RHbt5wOJ}|~El0*R9t~+|>tb#Da@{qF3cGX> z7?T)(##{2!YYvDLVehQ#8%{w*L}X*Lvam60)N~^Kh&jR;po1JaGo#@Uk`4%h*1ePH z2+C5TkV#DDV%ZcB7S{+fBfusM2O-QX$kPvm#`?LfUuysIBa8M50KJGA<4+QV5Z?bgM~rwhNC>v& zr9Z|FT}9QEcKJ>#nhs4pn3S+tkUYX8D=>Oty|IH+)dDnCt>M%~7(^f&`)aWHqa;fw zo|##GXQy3NfY+t?ebDu)Z`y!j)+AF0 zVOt|IPi0LbJ%kWOlc7id_yhs^c!x(w2etKf`F69i(gs@ZHAgd{g{QLdLfGY`p3I)B zKiH^lG~I`GKpW17Hm7C34om0(=GE_Q7mlXRd~%__-8%oBJG$n+_C5;C$j`|=h85x_ zSLQ0w{}@EL&$XL@NE@s5?7#Rr>{rRiS=HdgP^i0?ovX!Rx%Rjq_ zr$YT8wZ0FLam-%)FS44~L&QcPfH*baJ5((|%fV0EaSut8T!7KyQ~BJ{@ry@8I@rEc z-?>sZqOd8z2>1}XcIkYE(k-#~e*4LZSS%b*p=bVGim(FJ=4d#qnzb!*4eKc6vEjA?2$_PE=MAU=L$?A0gFyEEsVIo%5Itlju^CwOv z=by*{_NA`@qauQCkp&eQ<^2~|cW%~@-NUFaW@Kb_MY?WY<^lkOqS5riWO6De+@VDY z7Jqo?SV mX4>hvoq!d7yPrf=3^XCvv*lYy$u`ua@6?{YGZ1pbCA}d@k~w_}gisDi_kMX!3XNIM-!7?ZZ&q7nqGQqC zsAmzn_C|{UJ*;W`A<^(T5!erxA`?SNJOHfYq}f>Cns}>>3ldY zq^w7GL?-1V<foQU3{sewzaVXMwb3W)7#a0t5j=MYD&EY|L(cx zo4b`(sfIMoiDLkyq95o5`Y^g3;c1jpX2mLcZKKaF0JKWV z?yW}YR-;qbDArQEvW~%;w^vjEu3x zOGdmQp|PVy0X{TusED+G{{GbTsc3z>`m29)p}yVd+8)MY4D3T9XxeFFNjPsK3;=?C z1Au00Ic+msXn!CMN8?3~7>^@a6wbK~u;xxNfKl#9;@F2K#;#?p|F&AX-86F|pXx<8 zLu)@epCLk`vExKI)?@m_29In$L3_O!baEm<4L!q37hz7WBLHE%kV#JEW7(uwlN$?c z#F8}4RmteY$Kv7G*uU=e_ZzF{>thX!tO!hnfBNyCcWPE-r+N`7Hj=3>?pLxBH&fmK7uasd*pr-yp8N7KV^61{zVppnYD;rRMGP5X z2iCt^oM%uYs#bXsKRL`Knd^v1ut(XK0+ zrrE;K%o0W2G-?2VP&}4dm`Y4$$9h68%81|(5NJr+!JtL{9~xJ*8vBdrQX!?;sjO_) zH%kcVT{Fq4b#0D5GeXa7-bRer$n`LQPbN#V)4(ttjs`%E{KF%5U__eL9Q)v8b}8=3 znA*>>@lLgPyAD=h9sLjy)b>jcvbn8 za$+NsVF^gtpusTH%W|GJsJq~DCK?PUB7fp>DWY*A;O->^2u?=8BzJ8bgz+-Nx)hmj zPX}@&oS4idr}LrM*sItGYy{YpjGXdt^AE=|^9KPGZd`6Gzgwe3_3oFOg2(s%|9n(GE!jWC?A5W+AZ$DvpOBSf{mnx)R`|KVC!0kkJ$q+9&=!pV4M zaVnNed#E0wh!WJ?I4GQSP#+psARBMg8+Kx>b(G5Lc5Pz^X?lN@mMp49PH!WCRV=bo zBs}^VOq?2VyeAPQBB|Lwas+sjYq0mP#XzI)hsr@C{sZV2{yjz=2? zE#g1bU?I^EI1yFr(S`VsPfh|oq*GyN4su0Ec5>trdZzdHKnX^(spNDao=b_)v#}nR zX$hB^qtd3&Mkh{%55ObRRrKEXc9fRp+Lw+!Fftdl)oJclo4b{ErDpO%WF|+zW6TIA|>sH4iBQp!VE}GS^{hDjRAtV;zGysU9h~b$0_#d5gz7Jj1 zE9*@;B89?|9FgRREQfoZa!OPE&40K8hVEoEyg4D__^vq$-SLpET|B0{mmq}cnb_%1 z&jiVduv@{QBBx#T_FvuF?Mj1bJuUgJs!=qZ$Sh9Ba_O;i9~I>gVxt3G8+jj^Pvva( z=Zp~26}7UqU0d5hy<9)Yu#2$yUa{B+5OlAx4o7_02vFnH7|0RMrD&wdkt;mHOl(X! zKwW{+iH}TW7vtmDjyBKNHZN6O-HW(lC<5z$_Ky&a5+?#Q2cw`u&j5!@5h=+%mK-@l z+bjnd8D@0^00<{y$?1G@Dlf909hh<1HJJdmcQvm-7mh||&m9nT^Nq`myKh$y3^4jB zUw^9Wt#Z9psEOe?WU2xF)RrnN{A01%4FOCOlXFD2*>MWoL+U)|ieRI(511}?JHLKhC=%6$fb zkN{^+G@@kH-oI0)GN+!MGQrq(;R?HiL(Xjldw|)Yu%307IzSzK|n8 zP0UKn>0=eTHkWWT^uSjZC21^OAvVrew=PwU3=g*aXb86wIj9+q*Dh;ZAdxF40#uix zL31L&kfF>qR~l>RnQpl~NtWXinbgcgBsG@QRTRcK9hW?uL5|YX;UiBCuwqn~wdSrK zPQpkMhLbRokiftclXMNe^X*NwqYu109YJ7ZE zzolz5(7W%gm+6Yg>v<6V6(DQ6G>9>y?aJ3(Y?LTO@BQNZfH+ZOyHmQ|OwL47Gf|sn zg#9!`#UdIi>i$hr^jH4X`)XS=P*F6KBnin2U?j?jkfTFh{kR_8pf#j=y@aOMHg+nj z+q$9xQ}#3wUH!8AW*<+Ug6_N0=c)i% zi|927a6Q~#Hx+m%=K&G{Z_Y#kaU#H#^$hT;Svy0|?8XwKOA+QKb<6RQbRsofNK9sp zrF~Hj*xhXx@CCtxHcg_AIvk|HS>NlIM$bo6rT z_44MmI-q7S9}m&%fBf7}0_l9)m3pgGYn7^C$59$b(-w!ekIp`H}Om((UG*SBpqP zx{ja(6NPYkKAN73B_^W~dWuaWr5ZQfOe@UkA(26jjcD zJ@IfDS3)|Zqxx2{va+SM6>}swlql{(WD-QhMnu@;28kFQj+{7%3WkCsM}ve%SQmB1 zmGILapCg%VqhGe)udTgX#(Ef$m=WO8XZ77X4NNlIi_Rmv;`LBD5g@q~VRpI-4*>>8 zAtzm;s~REQavnJxicjWJGlg*cfP@-}!o`e8T`SnjOl96Fhot*HpBcpMmF{Yr7dkA8 zY@ry8rle?Eiln4yQi`VKP}Jj~vvH+<>&-IVHQ^8eBXiL@YOB<9JJJ=^13ZExGsScO zHu*Oo=S_>m3g!d`6Sj~jHTTC z^~UP?A`9pZwgG!`*!F#L#B7rp34Kh@+;BKD^Ezx)S~L+p_t%y%CJP%U0tBC+%?s6) zH%rK5nDmw0Lm)jH%go0!bMaU%%!`T~3vnnEkE)%>m0wg`6A24oG=lU05n(FroAT!U^|hM6%|FZsnkp%UdTY9W_?(hkq0NEv!Bj*1~f!kbVuDF&>|KZE{9<> zEk#puG%dx_ax^7}6B0|MUVgW_a-m9@P>c~U`tjh--l;WPrE05OYgg(3rk z-z1Od(Ag3oq*g?lj>EtalDp#d-cx$y_f9%fq4D(>EAYYuB9T@w!$< z+huwYw$_BK_!T-eu*1>NBY$+9!6339K}lYA_cz7u%e6fb*`7qAxy~CaVBv(ET}Wi+ z7o)1z7y>v%ezt9HAW0)S9vNl1^;Q#8pc0bIbmGo`B}f%%6Di;qm$SFRkNR^BV# zcy--c=P0!wIxD)1UFDx2v6(|y#VCr0(e$b8sgKNu!m>BJOKod!ef>teu7Kcx(V)Q) z)725D@kH>HhM09}d{mmHy9W?LD2L_8|KtSeDs(?1=daTgbn|ChrMnFS3|YE(lbK?& zV|!PO=Ofw0WOgB*nTyMNUVC<_Yy%Y~V$n=e3WbbIFTYz^y;vhz$pRRSBPt3U&(4>UUq?p=3!jjjgOp;)7vBK@g&WLLz^h2upS-oCvtP6cGigDGo%g zE=8pDXIM@IAdKbGsp&#Imj=S6NQ|>yos5>ANM`2)Lye_7%KG^>$#;_8v$2pPb8t^2 zDaSIQXhMcEXxG%;m6oRJ&Upm;6%QRSvdjiMO0!gLm1?bWRaZ3*cx3gLhR!sxCSfCc z>NFfYvg`*mZ5>N&8l<-k+~Sqhw+QQe)ZI=_#!f#wnjDK!F)MJNMzgHbtbYs zc5_skr8NOd{|*3r>c2VVlM{6hQ<~b9A8s^u+F;MOS=V(lD$;eGV%GHjiU5>gW;UK( zOyrIvQj_ssm(qi%Fp`W#(n(3SuVC!lYFzu(E}*&>31GDE^Lf%?Tz>T20E5z2md>TlI>s$QdMEk^vfL)-P;y~ow zDD+TD10Fi-f`fHH|oZ4?W8^ z%TC6NjtnsR=*#Ckm1r3)AQ#FPRH_YS-ViY`mg5)X=2grnI3;ZC zTcAr{&p%K&`s9qvBVvybc=OM%wd)+wMnixCLr#T8w``zeT7W^T$S!wnY(4p3pE7i~ z5I&*3(~PogAq1McovS}sZ`V|N&#v1{Sj%N}-Sh@^%QNZzDHN4*3(4G4vT!t$UCc&P z2`J+w!**^puf1Ai?C8Y|f0z?^4>Nq9{vaUG4DRf=Bx=<7k|Xwar!-rYm955hsn-u; zMT;aX`l01V!+|8~VV>gDu;=kaj@aG> zSU!`QF2r(aF`9^i?V?FeMig>1buvExP{RLh?A}t=&Nq!P1@^@DET%r)+lb}kcugqa zKiXGd)PF|0-mcVJ(raBwul8QNWeDU+l?gaeuxXurz}9x;St^Xv4y|Un~?eB znc2iW&&|g&7?-@2cXzJ8vWBVg&|ViUY>O!#&4(DaSA;|iNrS|W1^^JxM(+98LS{O_ zejB%by}9z<&X9o-_ZN}B)3Uo#AY84`kcsArS~c}07l^P6MyrR9JYplk#4DP zw^%Hg@=EjCk2kcAuI~vb^huBd6`4Pp%^LG^nOW^f4u?{+6RFvWSUQrP31?;^ndwL< zYR5+J|8%Fc+VZT5C(0lu?SV|`M@_?sE9I6bBTkMyzc00|RM)m@8%2b4rzOz8CCPz} zfK#!E?tg^ePunvNz~FQukqV_na8Z`E(UvNh;jnNB5#GF#6G`-IJ!M?Mkidd8D>G6mtY4f+Jyb*qhEk z4j&`KBhxE%+Ch-eJTU8c5PJY1sQH8<(urqgXYbA1Y^_T_y1Q|C*9?I0wG%ScJa(PP zU=b4F(J-<1c{2`-+STzY&;V#`-j-DG`SD zbRIagND{eFW;U9dj6~v++R->$^!IBq1=h6SEJ95 zf_g`~s#e!_s_WajsyTzv{9z-!c|&nHatU4o9HDCd$X7M!^=5FK2$0pR2`)v(B4WIdO-&bK z*_0S2MBxQSjMylV3!nLT&L=HeeXrq&PC%aX*sxF}UPv@%!07)udceqB)K;feuC~jy zR=H-_QZyc9-q`aVFo%g*AQ8o^A;Y8oOLdIuW^&<_*0wHcNtPqYrcdE$`qYQ!;@L>| zm}aT-#$Q~~RDF*MLC1uafIJ<;8`iX9uvB~?BnBhTxyt8!p5}ia`Rd6eN%F~2UjK`$ ztxAVI+|+v;_R-`6mbhksM3y{&$ljFtGDZO7XJqVo?2k@HQ(-I(!sSG>RLr|?>@2@k zG+>Y!74@8qthv&hsK~5-ZJ?q^GM=8FN=#(CwbN(pv7kl6oR$-MCm3vlVt;*sM9gs+ zBBFL3wt_UhwppyLZ5!)kKCuyo9AWnRbvPOnIReaXNBr=}Ovfk^8=$9k-5co^&P^;m zJ~g1XueSBtTD!W|s%>^A?@3OcO$`Ve?cT26_{AnmJQ@`kdG@&u*n-+$NE9F^0{kvT zT$~7a4Lvh%ARLP(X9~%wyg5&ZD59_!MZD72p@{V07bkqsqV~$W4O_w-*Z3087Ne`e z=>eccV*rfIrBiRVO0{;m+O9Uxp5B^8wc2EM*tCSOnn$ki$bd2=n}%dRIpLAzx+Jbd zvXdk@dp4h)OLiLS@;h6dhH6fWX4!DmJfeURvw`IqA+b{=l0%mB5p`dy`A=NH=+U$Siyo&n9D-IQz*WY5fbb5X=mQAyC>#nNj0vHV$7(t99C*-0to)L_(i0U69 zaowI^Mxq$6ys=ZRtZpfLvH6fw-Uzrl91)zg&4W3`;RtYss=1LP;F4PnaB3Jq*4Yc* z(va|z834dTUp}6kh;i9A?q+RGtE@CDtF6X%2ki~D0D#nVbn&TtC?*A7zsf7kt3O_I z#-r_xr#gKpp1o7z=(>jCa2UU|1ca+ai|KjyL$={xCj4Zs}rHd-(AAAaEdiCpy2ubyP+BbdheMzK>_X_i+S zt+GPm3A7z+!{hc)%0y?c}7TX6bVAIq7w-fb#dDhe{fGcYnKLZRTZ_Zr6w`rl&}Bg zmBwzHU6U-}X3T9Wkxs*ELa{(1XGogyoNfL@{kH(1&6EG3FCWj$CTy2MWX0=drE~Fx zyUpDW04}J=;6z=nvG#S z+RN|O00P*!S`WHxfsi9cqi!sK(GLzxz0@_WU8%RrwN|C3)SG~i8I1yuFxCl<1eraD z&8Nc0m)XM8tPVuPdt;z5hR2hmo*~)KxS-6N(EGnGb*_tsgTm zGIJu}R0#zT0C4V0$8!rw$8SN{|5i7emtMTvX{f-NyVn+#ruXHmtsQ;uKXN3Tnw?C| zOqdtJJR~pou;w6U^24D;uBgaP)qTJtE^-6{axWTpM`ck4U9`4RUEiv26m?AlSYadR zz(!z54o8^d>3E2T909LxN5ECXh!6$u41Vv9a*uxXR3h)i*+*0K@=Bw$+^DQJG(~6W zVQeN)W-fl@0~4XB4=)c>|54p+UV3TSh(|LG!N}vxYM&GvJrE+Sp@p%pcvju0@n{5B ztN=L?U==p%b0R=+DMBccPNt>{iHRJPCDH4O60DiW!sX2yOOIrxPbGM0QET~~8rD0K zMZL(wtby0k8+RKI{)2-8jBHg?SJYOe-m28v<(krL8Go{6)N+MKmJt9F4Hu_9LGdnS*I!))JedvyY{@KVLV#P?h*`mr>Pe2{L}1VD2xrb( zGNwKF`D2BnY14mJq{vYn+HKeQQt%{YC?0TTv^-d)Eb_#5%A=2L`=R$ zv0nJe5nysV;xjO8MY213<7P4@el&SJd;h17vYfn8Z13Kw@7}F7ifvz=A+$p)2;uC5 zx!L=(zJXEe!V7m;;?aPob@;$NvON(Z#Coh(XCl&)8zVm&947+Ip=Su8crKlsF2oC2 zIG~(-q6kaR4H%K&QQ=7ZA4JBftj1z0B+mZZcf>51#%0fv=`^ zcvP;nE45aouD06T@W_(vi~}BFUAq}&vw_3I$T|KwX_1pC0mdX~(7R+cJ3^w*{pj;U z^TyME^WJDG(rG9cUbduq)`7^!CyHBXSL@Za?MAU|g8NhwBw?VB7l$KY zPrk-WjsPi&%`phEdFNXT~i$u}hhpr== z(=p1)i9*pxa=MV5%7>y6(UFQWWM<_0Z+9bAh}`$dNhVtKb``5DG6eS`pZ)~nW!J&K ze?Wl|%b0EQJhH$eK#(!AQ5(z2C5-Wb;YWl&IpGlnRPFi8xbP!eb@b&}6f8mlE*|3~ z(QE9^C;$2xT}S6%yw$ENEG?FIZGZUf+5dVP{se4Bk2S+F?7JQ9%}LChsIP<#;MtEK zS$JRq5aQ!BRlo4!o$Z?yOP8$!Gb-wZn^`}0{~C@*({oda$(*eP1^bXivh#fhMig3< zpO4@3!3jAG0RZLo_Uil9@_L)p;c#WIaPDM}J9F*pY!?d0B&Dr&At|7$rwL#r3QN)2 zN_Bm^wz;FLn$6(|pI%MEQ6UOQ0vIn~YN#5!^SXc7y$r6 z5&5YPoX^cGzLmaqNGT zFw_HMic!rb+ZT~T$C}QMY+~4u#kYtjFpU=>vAhDa4ke&zKw9>PqOzvwNJrT7^9s!l zITB>xi)m~g)aFgIM7lX?8oHydXA#>9y%zD2;C zE)BwXE}fd0h~?6Qdn3F)L}KbbAX)^-EBp|G$G*G}iCbh}E1T`R?^J5Vj_o`wFO(6- z(1n&|aylbVPhdtx?`kE(60w%yvjTP?5FyX2RJNqOiV zo%1LY&w}$0P0T%z^Kky=Zs+Z9-muAC1GZd301ZsT$g6r0@*LV06eOZ?B41m4I+vtqX3L3%;<@)&Py^R)1vzAw<>@kf03(u(a<2#SetW) z1sHJ^O;OvGdb?U{SL&U5lVt^rwqP!ijv=ZSF&a-4eO7yf8pBGRPi-SWwc%JOl8dZ~ z1U-Dq6%`Gd76l^o$YCeBx*Cy~({_DooS5RyWn)O0>MGhw()yP|r}`Kkmh8WCVbu_0pV@S|Ux!=A0U z++2RI+N!Dmn+JdazEIvNOZP&FiGvUpmg4jGrV?38CdYPNee>I^bmK+nu-|T$i#D``=F+K#) zt|@Q+YncqWWJ?Z&nG?YGLdpi4R`0-P?B1G2cr*N}sM|JfHsg{Bl;)!Nm1 zmmhU%O>dPZJ6y%}AQydfk&(T!rzxW!D{B<`>j zFf^fA-};Wk8}X-DM9%{POi^jTq+SH9Y1F#q=p!>{K6(V0z2Oij-mSj--J5Dt1?FsM zM&E69De9u4XgZmiDZ~p|Bl>JW>i{0Qaxr5M+<;(=Z%^dH4}Er){9eHJ&Bp4*n$pre zGPA0skAZNj8pg#xFH@U)Z9&ZVd09Kfiq zyz%E(0FQK$cMYS#Sw8IN84M(1<3zMPi;@o10+cLqR? zSl|(0jDja1B$HCOM4IPde-u~hMv5il><@`ur38;WIywJ9fgKpV{^wV)sK|3VA-~Q z&EJ+NA*JlhXf!!dP$>%o|6TC!J^O;F&E8%`r_pLx>s|jNt)mbz2U(oRQY*;(h@N8B zTDK6L(XeV%dqzmWi@cH5=!EQ69c|j?L(l{aD;UlF5cb3$ZuXF?Wn+A+D{<|t4d$>f zh|x}f_kZfx;)7EFA+4jo|I<6m?`~u5o4#lf0sw$;B9@w&NKWR>O<*4v0R%1DcVL7~ znepWBFH)k}jiPq+jm!1T>kXs>I&)!RF(p(=3(^7io_UM}s3r1BXZ0#!a*(ASC-4;pHj${HYmxNS5%8?nZ7gb?U>j zp;*ZEXn1kBb@Q7vlFmJ8k~%`jzUf-_4K*xg%Qh*4FW?xc467+_phMqT_<23$)(U6;hBOMai zKA4?}Eb>68FB4;rP16yCpl~E3h2`Dls@m4*>0qHGC8lzz>4``(<j*FSBJaMc-dw zWZpRUnVEPt%y@}TQ(L=OFWzZ!aUx2V0DAu;hvC$T#Pq2IW4h~|JB_Qa?oj4wLdHmx zz>C&VY8yMX&7ygIi^bsxcnVeHB1gcJ+Yu8yBJ(4N%N0WHov2+waJIfAaYStxdm@R@ z>5ndCW)rT!=#~HTqHE{FF~16AZHz>HcnHyuAQ6i-H55(+0ElJN$?1H;6v8ixC}YHo zTsO{sWU8>3003x;e(SB$=JlFelP7ju`zu{Ed{h+pkAnZxN4_VjaERoXCumAcTev5d;>OF3TCZIX(6qi=ydNYPyh^$k|@-=t#GSY58!5Mt1ii2;rFz zPv#f+GD){8+UkW`dA;Mac2WF{LJ?{DRATC6Tn=-L_qTpqTDx2gY{DjrC_J58vskWf zY_}^lK!`ZQNL;&xss%`nsJUo4;1OU>wLw_2f`CCqk=4%U&{twze#auMrN^fi&rQ;R zk)r?d-<$_-64#cG%(3A-{SyZsV%G=omJEa<<`|eW5tS1eb`&l)4fCgPJeHiwCnoYn zRuEA{8Ao6Q+@gIUgt@sGgs{5Z?nd#u8ttwt&;#`n7!WF=A2yuSZ18{jumDDbHO@M< zM!QyTSL&TwT~kyF-=XE*fUHJ{Mf=$UKMD>Z`JqTu2O?ISWN#i2vNRiyEsFVD$Xk98 zw)HeWkjVBGbGo#yHRIGUBCsU9=@W`Z5|f40R9<$NIG!i->>8qi6OEwV!0uiIAzXSm zJ9oy5zkPj2UA<7N7gY}whGOM}W771g#KiHK9OABQ7k;!=*=hl5Fo*y~q71Ys%~ox5 zr@pnTt11cNkoP<6jUdPopoa6)>=ajcL^e?f>enQzLt-CpF#3}0Qv!!*5q-nFT{x1y z=c5b6>}d%6>|eg)fmQ=+cpBHx%4-gBDM$_w5&_@NMCLQ-4o-ylIFTF)Cnj>q$wDL< z7d@CL;|q)c6N{&P#TELnyJ_#ES;Zx^7sxEeSHzqEM#C&>tJAL4y9i0CH!c1_kU~ai zbcP;-7C1lRKu8`a5+JWSx;_(HlVfcyK0A&8^cr34E0;8}Ea7I(Pk74-*$;NYAY%`O z6G2Ie=d-ElLM)r6{}z_$8!=%%48RDxF>^X~?9r^}>sHn~YZn`>s>VnsX{abJO`lFo z9F0k`mpjpQ@W$6ybxrq{qDWLBQ35PTM~&TbeRHQ(t^%6GwTtGFlN{lfcIe~?_;x!2 zB)uZQd^Y@&l_XJuQlXFj(50Vbq(!!m(Ny@}k1wTX5?CAJr~mRDXK&NZQp!8mVzOG? ztRvn8BLNU1Hb?|0`PBG15u4UE=1-C=#|zoyWImQjfdPlEh$70qFeAW_Sb(wjU|)gW zUxSLpF~Y}eH29c1_;)}2T~Uws$xzcf^=6MBRqI+up{P^4C=(I>im7vwuLNr3X3t>_ zvGXPmH$!S>Y!cM_p?`*WyFwS!2_Ys6iD(AtG?__Av&Qxw{vKY6rcSZTW{`CQdtXNB+9-Ogc`e*`qoaf zRPBxdSo%ZBj7A|xfUJCkV?CJt?B3P{>xkGXQIeB*GfTLVZ|q&gBQ2ub=fu&>+2@wb zc^lW?FJ654_TD`p#&kk>sNvJ5@+VgWSOkx#2#FgK0jgd#*_7hqL;!$jI+>WvCnxe! zNEUsPD2J9A0pA`*<4%kGhol9_jNsq>@OMNd9zB~4DJJJ5wcWv5W=#AD@c17A03_G8 z&1xCJAcV1jo2rq2|J-4A9DEV`LF54;;v^lo*n)epW(4C2+Aa`^MN?CS#AH4ki+X$D z0u>2hg!L`z7D<*K`@OlK{P8sfZQp2Z-)JGtrH;k3p{dic>|9{lqBmaK-M-a8rUG#8 zMFJR!vX4dUD7DR<`c~0wen5&)5se&C4HXtJ1bvx0L<ey1OaG!igfO zM52&OPUc+~+e8uN05c=tDJE-NR-k=80mz`nJD|7?0gT4MbW_w$t=_IRI<-cp-q2Nz zZWwtss&FnyEEU~5LSpI{U|`(}yO>cR7>J{N@`U$u5h4$ei1MSHI7yC1k`uYaMBcLs z~u64Mq5`~#XD_al_2e2H=dKHPRG*I0o(5H zy|~$|DgqdZG8Uy(sWrBD8#^Uk)12f86Im80IRb1l*ukWKg)X8J>_Ux!5G7Y^#5nip zb{di_g`#q&p)k4>8CgyFdLC~=MS#!Sh>H+;ghV*d<7!PYDq-9GiIR!hbOW#(ZFx1UDr0Rv?}Y0QN5EBp~+LR)I=!w-n*m!_SS81NA0gh4*)7OeO#2`+ey5@2mlvg zbnLO*^ohiPZme0>HZQlJ1SU^L6Zz19u2NoaU-{(@LP*zj8!d88aV?sLDEnSCMXhfa z8{4~`dXs>Mc*qe5!tDs~dj8Q(Wy0^{K&l)4i_O2yGw@t0tRlk1@sDh4O%P#Xv~BNuto1vI8pZrM$^eI zCkjPbm0aknox&0tGixchmtqG-4C+Na4I@dKy(hJJF70XhHY#QHVs+(W72ygXBuW#& zNR$yQN~2lZDmHdXds)>6OpZLkBjD}p$wWRIFJ#$`C}Av(v9;rG-f-Tcym&NjWO8_HL4opYWs?_RR#l~)>D{{mlZv@Di*=AFL zXlZc8(Q`Q;;m^-7=(!yMA*~7XuDAOMTktS>&~>J~XdeI0Yzz4!L~J&a#j>L65SjNn z&(_NxD@pNOI+4#N3Rx)>8dwt)g94}hG73*@RA(_Gh>Z?R1EKWW^qiRH?L+`CK+wN7 z{OA`J0RV4)XSrEX81WESHUly+`j7xdhp10gn(a>41*y^O>1Q+O7Liu@{7@(QUtBv8 zD2;aiN?%pe`Fb~34{d7nK7)~D+8>F>;`wYmpNXatV8D^a!ra*(gNg0X0lgJhU?f=; zuMJuRAxz}L_kVKA(^W_0C4slUyP>o+0gOaBX#J0lnx%4MyVNLEZ0k}i5|FdNg$nVSMaYdO!AO%5Vp@YPa4$mSq2^i7Ik?mXeJToK37sM~v`@7Z`m@*gA){sO^qo;z!*Cx(tHtOuZLV@EYXqMEKI8N&E0r z={$(ZICC4*7+2B|?OXkAITViP(y?qNmP^TDj+{__I(jkn9g@LBXO}}qM|fc5CU@N} z;jsM3mu9HO7h|02{=BbtRXzWMEknSF7Z`~qBgzpxokmk>wvxD!QgOcgu~P zQnOqkkt1^(h0O4Xrtct840JEe-lu&+P2AyqRCRa3b{xQ%If&-1>|sP;&W6S)X5?c0 z==7h)2Z^wW;cUGzgfN;;#d4WMJ`;||Jij%%Q<%3`DQq5Z+$gAL$dW>l?=Ob~9t{Ym zjy^DA**Nj|#Qd3zagx=`l^bvDxS}FDMdAWR-xQY3SRC{aKsnrI1!=(GE zm<1#GqSD~N`<;QJB3@gH!Bv|zJPkrAnn}g8=~yNeNybIC&qHc3A+Pl|7%fQC5C{RF zs29OLCo%%l&VF&@LteW%na zS8PjA*l2g?8j?nYN7f5Vc&^mbmto^agELM7$=c!5$f3JA`%Hz#$A*BXj2}AfE0mx% zcSc4%Kn{oExlAmVjpx#mEVEg;f#tUIjFS$O*@LZGJ}m7ZtsTz`-duDV`gcwmZ+J9b zRh`k3h_Vrm%1`|65lQZ!N7vBn-@L1|HJ{8#0Hg5%LxER%AN>UVt6ddsrP)$it?q`} zY9ky%@=uZI_v0Df53yQ!Fw_{1bTg`K$IGE`G?R*^ld()Hl86aNbg&^2qrt=&`8bGP z$~Fo@NCieF-=gluvBz^0M`K_wMe#seec)~7{h#dAiX8!rL^-T|8fulRjh%9{T<+yo z8$3MfHyX(`znJ4Qp~cb65+7;QAb*6uXL7`l&J{7voP!EmV@n9jXa4}VnST+;SXQQbE`&;mdiYUMCKr$olYUA198KkvC4P|9+!t6GVr)iWZBMXeM`^C!*{( zct|pHBJ;K^OJ-N2`Fqpz_azB-lMj2#<)#Dp;kt(2`R1IyU-8R!A0gOaB45eMI zH;UzEsjM_xyyOUwRglb0gywwIVeEOHX`hQI6OJ8MKc6`nQ%fRVP`Dt_X>}(|~{bL*EeZ zGBDJ$ujz-pPj+CyN*`*gqqJITtF1I!N~^85+FdzqZi+prKT0(rc#MPZamyy!=SCX)5*ZrupiKWExC$rE) zH^&N(An$n?OMRzv`R7HFfDsEY63j@HgICmcr%^07cPs5$tt)lJ0FNvkok{0NBAEve z+VBvLADKEmZr5h$ZoClWkeDLQ+|Lt`ed0NRj)zER%wKeO&uf>keOmTtzzB)ta442Z z$1>?yF3rB=Mu$PchNK-BX2ivb_#hGDIn#ij7GyA4tbLtZ?SkI8lpseV1dQnJMd6s7 z$VB3qa6A)Eq{GR4G@1%wPu{Mn_1$)(sx&J~v!XOA9i^oKHzy+jjD|~y0!h|5xPHoW z%+>&u)sU_$%^p9}+8wpsR$6VtDV;KW2I50Bw9e6wATeN(D*^!NKnlr`L_Cs=N0RYK zA}$Eg;pIdg4JPD%51w_7m`BZS9vMt98jcxBQZFtoJrO2bUd4m zB;)>j5(+J1fk&>sMXuw*DEDANaUu^k4=%G_kV62UJ`rT=l&GGD(AHBqZ(ER~QL;4X zoapF7g~D7So(YGdvggaGZLL{RimQ#=?-l_=cp5V>`ry~aoX!7nelk}F-mQDn_Zhyj z8q#&O-RZd?bvj)~B(>c!mKj}tk(WykqJ!jlgNR3g59vq>g~IV@BpweZV&OzQ5|7D| zFc9US_Bnnakq2vuA$ZM=YB9Du{HSU6Edn^XYs-c#g`@IAU!3;a%IT~6zS$7A+agpNJdZ)y` zaaW3jt6h=_^UyjZ&OC0O;gPxP_U3uSH?--jPDV`N1F_*GhGZa6mSU-7G@XiNQjt^= z`cnUdEVPIzJnY@m42$8e)&@C z%+SJ>@duCaaAEFl?h%n05ovd7UVC~*W<_Zvix zyC<$Hm^eSVGXI{)@+*?8h@u9MIk}Qf!Q&J?Ut2F@A&rzjz8*d?mSf1lT0v8JL zkq{RS^WhL52{9Z;y)ETLp3#z1z~rWxjz~cg008yELs@4^0P%{~3?{coZ7CrwXIk&+rAX~va7HH7Xl#N*=v)F6tf`9H&3*CxXb|s{c@{$XVRc zsfy5slMM=PlpKv3Fbc=`D?gqI$Gctk)yvY2w^tffskgxBUr|UDkP!K5)`)t+u)18g z5H^S<6#Lk7aW$l>iYUpFq==FtN*Y&EBw3LpRgx7+?(G7v=l4q+8w|^`0?!IOC-5xK zvmv1ci9!No89?n|qoSVds^J4BEFMB$aZ<3Bv~J5PhkqHI;x)q>lZ+Uafe@biK{h(Z zLvIh|mhbHnR}sS_A0e02P;dWYOM^u2%*Z&YyQdIS+hbHj$X{xL=iX{ym$&IG* zaqHX*#zc9ny4N_BYb*~kf+vrFfvnod{bwP>|9)I}*88}Na3GUyeEQ2#?FJ74*B)xd z@9^P}5RZkE@lYbhN5cK~Fz)3iPA5sOy!%RTFq4c3Ig$0|Bf_wUDCWTiJlJT5gxq1z zkXNEXhp@v0d>96E5?X)8scg-$kEH#R_po|{TGzm+#f;+ol^@N7V!@eFqas~@d%0Pa zv5{nOVDx_+QDD>!A?g+np&r1f?#CyQ3-GC|1_oG`A0eblvMS4pEUS{D$g(QSsw6A2 ztjdZi%c`OxRaIpfDXK{+vquXChGQ9)Wmpz!2OP_C9K&-g&odmy@+`w~oIt4%T>|;w zL;-fy+@dApZ+5FwBg@nF>AE1}qaMv7f35XO)3DKr7t@(T5jSW9JsA-6L5RH7j=URb z7gw8epBF8_$j*#_IlYJiqrpT)6dUb(oF+?BqgZK{DveS_Zq#*e1ic1>UDg^QTvJ!% zqeGcvWaY||?6qvrEz!USIU*nRK}N4H5o6U&QGqa=Dlzlo`#MHA*86Pq98>cP&1a|tdbF$Nx}}CJZ{(e%?K7*0#7#NP zm%8+|DYwX_B#mVcT}(`#jX5q3p`8x-^i{Yp&SNp^Wvh3p>kq0rA;KmZ>5zyvO4~D9 z{6NR66lWoUY=p(Url9Q#1~!jc3jyMiR!H|=XV z*M4xgg(7LYN8lHj5{zLKNRbmKGmJ=n9Mc9w#qXd%hS3^;XppEMX+(q~Hj|L>ks2fz|$%&l89_yFbw>1Ymuq}Vq z+l%J7A&n(ICQ{%Sy%<_SGNvyb$dN_34cdM+LWn#tqS$DUfe`>8lH{+vH6uj*EiIch z<;E|U>t)FW6&+k))brrO!)qWPt3TkVaNG^?^3R7nwx)>qkw17eJmiQPwLNl2d$z0Q zLWtZmv=Isv;uB4l_BBuEUqdDl06y+L*97EMu_-mM2JR&cvOf*g=l!cB1=zTgk2xT zGofdGYgYfMo#oo2FSi;M*?4hSo*6$HpE{L_X9NI1ttfu+XA7PUkM=joh-3^9ZjyO$ zB3#&GjdYv?9tfdhz|)f}W>nmFeRw@nil9yHA3_I~1g*hNyQichIcmwhQEar|zz7)j zP4$z}lyLcn(?Xb(8Hsh}#ycyuqG)jlJu68YMqt!GNaT|cd2NOuk5AQQ7j}PK#%<3E zA9VPwkM%{4sOzD2AXY>@;zS<1YEF>I8dPE4(L?0O+Yb>;GY}er80+k4sYf%{qIFV? zw5l`+VRAfl_Kl3A({bTPUht@2k%#LNs$^NUISB? z*mp?e8lg!IQLI#()oQa;lbSUZDA;$=vWghP^Q@i0m6|c$4#MySbp5VrkpQvK!9O_) zns7wWmm}cKMr5I{3myUhusqL4!$K@7#3Fn&!iPfx5=4?T%(gEM!!iDaKR9CiJ*t8h zZx@!nE2s*Jri7`}$?>CcV|$0?JH>}zZhEf$_6r#G02Nt-97j&%0}^3?6tun#&WCjQ zumGHVH6}`dc#Z=M1Q}qHk1Wb)me*K=AY}DN4jm*ZIoe-fWdAps7M}Wz8HVw`;;M?i z`K#qxQS?kH3J#3^tC#y89*FT*+(Xvie6uj_h9j?9WJgyIMw*kj_@ z14i}OUu8IskB0ehn2$yHNJNN+2eVx)VS17MONNE7{_PX?Usx|oQe8=oV@m37{%Uz= zrRIr>_8u7dLq*Q}WR`6&guFSC3nW6gIyL>Nzt`V2Rv&153-XZgpjS@n&FdEY&4L-h!dEX{>CxFclq>R&dH*BK!MTT?@$pEBI4H6 z$bG`~mBH&i7)j1yQ;fW@xAQuSOp_l_*F%kRA}>w^daglavC(|kl|_LH(GiM6^|u8GxZvsqU_L&HPJ zmAfz=#joBh36k+6a&qJw8zJB0Kd^?KCmR9)a3O&UhxtgDi-h@bh>wO@o*zbsL$Vaq z4_^J-Ct9-@fvJl6$)C(AvT9%W5$a(N4k{u^DI(`Yj*tjhLn&y0DVAu*NzBM+w1azI zJTeDOoXEy~%=`#i8Iu-9b<~K=JVdi#UJ}o#@)F@4yH+%NPLwlKIHhAI}iJ z%eTK+E^XGGTtxSd0{&Mo|MAE}B0p(FPVt9VLu+R?4mGQ>b6(UQleoRgR+?(?`1a35<4`vhs99igEZ4H(pmEYd}g z>3pdjT>42aoaFG!BE!lHt2hf?@Dl#@&@F(kv`@{P*&e7!@999yK}HH>JIQ8%>6 z6BPx}y(WQoBY%T>6RNpDBCjCLM}+1Oo>^iugpgEkO0~LFZ-|wuC^uwKNAA6mY+wl3 z@{FLPcY$_71VO8M7YCIb;xUNLT=2j~rgd_cHyTTYus;bQC-7`2%!dUo9Mb;d!y&zi z_^|sN>`dphAbjDcN65P%?JU=fir3`pz&`n+o}eOZKD9OYafy5oB9D3WEF1gs5Sv&8 z$qLT#*B0X4oMnAv8!e`R1kLoPQ@RQs!B6|3!hJ9aZ+0Si6c3}G&u1BMp%z=;+fo`b7{hNS-Mkr^wkz11IodG z(Z3j+$ipPW)qN&Wcfm)O@rRGXx9<8?yMMz|xY`Pd+~JYMp*WEv*KUp!8&MlcROBnA z$QUZMJsuG!3J8g83EM8qEypFCN~BVOUAIGsnGTI*Xj;Ie8_TlRk~9V~EOYGH^yI0S z=V}L=Vu{)4H|7x89h6hDl6CVQI+G&+0H|z9b6=DcxutlGELCa7%p!y7Ely}*T3=~V zml;N$2yX=L(SSgr?g^2%izAE8WKohE4Y?u7&8FNCWwj}Rh5{rND2|$Gh6CsUfLhK9 zcE}Ot1&-JV>*t_Bx`h}Uxer~I=NOJ-d7c$`P7qjuX9a;3I8G2)o;x7XTd+zK?cd99 z&7>zHgm3iuznia@MbNk0fdN8Amf**W6X9bX!@KLnXqH!+PTC(yM$ z-(_c|dh^{C5*INXzz89)D9yRn-GS+Na~bk9qTa?Igi=Rt_sRVZLc*bGJ@T$Q!@?t% zv?DV)B1|}<*ofM&E|&aPpAn@Lk#izvNaR6?5YBNSD-uFvKGxUN2=tU9XwNWWuwBwv zu8oaYhUJ(uuVpibA{b-?{qQ3!JnA7i!eOJzmh|{qQBhPq!w6Uk(;NhhjPgc-r}={% zkU7O?oAQ!J2ziEQeqyr#_Y0}2EJ%z^IR?$Tg*igcDgHkv-;Vrx%$5o)#@q z5irRcbt=>XU?W4i62c7+D^4;4AS?Q@Y_kDqhD)878Gk!6@*oIJ+C_TK5vEkl$ctQ- zZ+_Np)I9_MMs{ZO;vXCf$4r85#m)NM*V_PqV^3#Oc@uGNRHe`U%^cPh^a>dL^Zn;U zK6j{$vJT#mC@3Mq#V-Eh50Y#nd|(>HM>&s9+iJ=YAwk^Bpp69Ke#$pFqR^V!K|)2u z+1F0yN4|V(n zbP}U>D8hC2jaCkXsaeCZB+qI!^IKQ8q{r6^tvn;pK}EpUCJn25ttT8&X5<--0pecQ zpxv=ZRgqV1xJOpezElhL{Ed2>Ow=bxVI!7j&b&C0KN2T@7ezrg z-&rngHJngU|A3Ld-0EPc$h1#}It&Q+8UrV?hCQ~QGLTb4%%d?A<0s@o0v`#p0?$Xn zTu956<8?3!83wW}guv3LPm6Gnsv<>E6;;*Z8A(=TSrH{glvGKQo08mYN{yx#1vxi-wNAw2myk-vl4vfHhHQ?5(dC3?{RACc0CcWC>!dOQj~}t8QV2pU zD=P9+eP+!DGJe_FB6EBhvdBH!s?xBKsC81cI<9uh49G{rLM+Nh!a^*~gnXbP*Z4<&s4=NSTGq57^67*; z#t6qD=7A8JT?7mzVxd@6h)0E3RER}{ShNjENs~_rudQh=P*thkkm^mT-jJ$wsa6+j zb+Oh!stTbcE&>ojz+hU+8d9@uLf6n~hu)Y;)UG%~*4J1)0wzmgbg$A7TLjU;WiWX{+uOmJA#)vdyQ^-2-)5>)vXEiVz7T zatuCz#}1YAdB^wzVgKR)g=tRr5t58Z>yh+uDm5a{B&2A6zz_*>(LAeM#f|# z2(jwd#%v=Bi~xZ?k<(i4%u5qfCsPKZ*SPuKiY%)5px>1cIfY__NsjtkaYmN(9SjmV z!yrdO9@2#j*G@pG?CvV6dnaP_kE%`XTsL1n~gU9V#;ka@wJ?#vg7~ZT!^yz^SUu#{`%(&y2H_;1-*pLnKfwR83t;5Y!^<`DfDjT^oMDKJgP&4W^W`8+Bf@S* zx$aOQve3u9N451&R((f`m@&jSc_PyhbPi%V{K!3m0hrw|eAcwd6ZPKv&}fF*fTJRp zlpl~Opu6-U4IZWQ%+R~ z+!W=8C^ws`q5zY?tkLy2m!v^k)h%#r7Xa-Y(y&Cys*MfGTjKqXw-s46LLz&L5wz$T zIn4SH9%yFdo?hfK4l~oS%Wuxue_^d8-h6kZS(AH0i~4j^$bnRElZgmJHT+NY782q2 z_K0SRLS3RF>pqz+_%Z$lOypw@dqVMeG&dH_q#|Q!dzBgRnS2@`)hJk(Oqcmf ztk&wg#rkfszFU$T4O7h;Xk{K{rlV(Ho=CREkBu1U-T(2P)R2LmVbpdeny_~}`3Q8R zvH@(PC1_o{QEap)aWeU_Cn_SOMLvPii~qEjoCqQNUxln73F1GfL8)F(7s9U7T-yC~ zy8PfEk!SD$aO$vtY=g^NRncAN2zTc+Wm#>cXqa4;6#x)Yk+vLiCYQEvVFj&C(Fq0y z0eiV@Cu#wh!de_TB1$+Smp7u`huYwyBB$yzOzBxGR5HRKhn+Nh(Y_vd@!@bJn~r72 zqS-Vn2zz-Y-FHr01dN<4cZi)i$$&1NYgJa{MnkGM#m0k~>kI~g6GU4gd>0~Yv z9}7XI+c#Z*T>9>#Rb%s??tzit#3J`7C>ScTM>w4c5O-d)$XGg-&&0=bd^GGUEU_Pi zBFyoN(L|b+CA7OCZ!AiUno7uwg3%&!I-oSg>eg;;d#ARwtH?5hFq-5~y)Zs?D%J57 z=1AtFKYmo%Z5S8WS`>tkMu?EUnlrLyfIQZ26dUye7zN-&U6P|f!03g4O7=N?d>cZD z4L(|p3@tAP&@=MDmYZ3~5iWqkMJI$iRNcfM&I*ebW&TbRB0*pUUeo6{_A&JwD5@+Y z6)CdZ7WilvXozBarPM^Kb*1F2Rb&K1PMJiIlpNt>Ac~Es9SBrp$sV$wv}RueLmyca z48lZ9`j-sLMzfh{E*;B`@zKbj&4Pql*Ov66$>Z$A5vG^7TUF&oQ>r&KA#DT@w2H)< z{|=`VBs^e(I5uQsM>b89@>cWSuQ#>6X@;9`ZjlC!GaWKwBZtZUU@@Zrq*DL)#54Ir z=h9BUS*$C!-d#1S%zAR7zP7O=QCGFU#0+|R)@h_$NMz$OJ%mK|2+cE`)6cL1&qpGB zB+N&`TsYKfnG_PZkl@k;K$R7wDvBs8lB7tIT-Wj(8)Cg7)#`eKB-aFO8$2?;z(|ga zL6_y1IAtNZhFetFXb*sq{x8chiEKETk0f*9WIh~<1S}7~@$S;rV#Tu;5{dr_UV}!A zimV}!Eu~uDC&O-%VK@;33$*k$O?QY1cb7FZ%@m#kD z21dY7X_EF!#cDO17sn4Xsw|o!8S_v4`ky_h6`SUKB0X}@Pa0w#2=#3z&$Sy?V1#0$ zJ~1PAa^%i{dbo)N|IrKobFVm23r(726$}UomrUe0jTxDnqUkKj5`DC4(_CuWT|)}@ zP{SoIvBw{%-LjF7g!yPhh=#dHgb#boNlNyb7T1F?(_{D#BTai_LMTS799?g(&q_5m{ zztPF>Al&k&*r*#|1cG+xBa01CPll))F!C%qbBZ^x%0!(%7+K*F0@i5U;*YSqPnLo; z)Y_gkxho8@Yw zSdwaWOP-N?1Oqx6Y#!tYnmG_QjtKy*X*5G^&`=Sk(9Gy1Fg(cQZ3sD$5RWA$a`Ew8 zC>bB{pc(j2FJd|7*d;!d-MdFuk!7h?7prxt-Y~7=Jjsz$s*$~E8iW{ZWOzY&t9j>x z4OvzUQL|OgNS`#32{al;phLpZ$h$$mz{q&;^l#3_(jM|&vZ#Ld!CGaf>B5Hk+S%GU z_;cMY#Jzi8p+sz7P&-1XS*kROZ1l*=y^8)$t& z-dvItImmLods-AtFtca5cnagSR8^I#^=7pu)#}Yf%^fsrpxWd9oCVpaUfNg^`=2}FisQ<9svLpQEDlx6-v!=1*s|)2Dzk3 z+rynsd_$6OL{O(jy$`kFLPhv~AGj?w;8nCQ#A1obd}1;$#G}I)F_WYhaRNMfRfr|w zK8+A0)f!^8Cf4exEf0rfKz4Fu=1OMXN0XE|?Vpvdao4>yV%brN6{pk11u_|Gd4G^Svi-I|5( zfWB3nS%2I|ahY43h7HnOVmyQZ!?EE+Je*2|Qi(_^v0oLt?k3e6wXNOi_HKQ*pcSuK zGKq9@WQnXC6OQ~~Bj3VCG|KYBtO0?hGQ+?ZetJ94Q^aMWW)P!zDWEN?8x z%{n6V-XvYp+(!`~g3~9s%oOAKsHv(dR%*?1b!WNo(LaBvHAmB~x_Oh{lyIbf* zrzb***s#oe!J4Cmqs*zNLZRqDJg9QBsik=N5dtGmS`?ts zwPQY!4HBssDI9;2 zL61|QRW`Q7YRx&T2wLzEhAUY>5^xkha>7QmLakRbAn>YF0{|Smk~@0Y)I;s@ zH(T?!wy_?=S9%d}C|~UvDgq|qYD*M^S0HkYfAnJ^9!t$mrKTqsj_dxu5LPRnx*E;T zkYz9-u)C?O&&$=KO6cKj$-iX7h5`9Q?9?%qVf{T|JFAUr|MqcVX;rLL^#U{<6@eB) zg2sd+efg#d9$D9Jm`V}Hg*+DM^bZ*;7x4cwylFQbiP-m8w{AAV-F(G)HpeRj_8sGrIlWTB|nAjEy>j z6WX#%pkZRQjNws_!`zny?jtan3t#!+4EcMhDtPeO=FT#P8Ic2{0R64;@gMH@tCoRb zi_=`1f{}Vc*H8^uYX3b2hFn83Yylc3NMo6BG8q|5M$)OsSd!)W5g6EFrB+_wEU#~h z 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

FiM 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 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png deleted file mode 100644 index 07b2f167e05f2ece819d34d2a98fe091fdfccd38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96449 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%0D-B5nf( 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

6^Flu**!&0WZYm(&OOh} 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 vQxGXA-~c?QcaTcqiPz!rw diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircle.png deleted file mode 100644 index 26eec54d07fd645ca7d5a1c86932fb4f1b680dad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6028 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 bcvo8LkzCE~^Wg&f|I4k+Y)#9JJ)-^tTv%I- diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png deleted file mode 100644 index 272c6bcaf75c18a985b971284b11162a13a2cca0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20284 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 M07*qoM6N<$f+yAs{{R30 diff --git a/osu.Game.Tests/Resources/old-skin/score-0.png b/osu.Game.Tests/Resources/old-skin/score-0.png deleted file mode 100644 index 8304617d8c94a8400b50a90f364941bb02983065..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3092 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 Y0OO;-v8NaCZ2$lO07*qoM6N<$f&jDokN^Mx diff --git a/osu.Game.Tests/Resources/old-skin/score-3.png b/osu.Game.Tests/Resources/old-skin/score-3.png deleted file mode 100644 index 82bec3babebd59263dc486e5900a3a01ea4aaed7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3712 zcmV-`4uA29P)ifhvOHz zhC{oSeoa&R8~uM7%-=W#Zb1^@5;(PUqk@Qru>6f`==sgR{r1})oj7UIB!^~XGSCZ1 z6Y$?Dn&A%3t6@P9=mY}V-=KisM`261#=CoHtZryJ-~qCM9H6)SKR-{^yliU5(m+4Z z0ki|H8rtOZ0PYi__hESCe>PZ*O%g9=0et~4Pzc;4|L!|&+Oz@DXfy>$+;vTd!x3q1 zZS@~Ha-=~s)CaTxO&XfzbAIo~JuR=>dEDItW0Pgs>CFOQATSsh^1uTRl;3mDJ%zX5 zetS|%N{ZohIFJ^ksYpDr0Wa-=gOBg05ePQK3A*dr2&ScZ3Yb{c^| zz^JXQHMVZu+Oc87hSNun9zBLr&I0Fv^J09HgiEJxbRrMCBqkZ7MZgGP+=>+|Wwr}5F`u_XxpW3%?-#>Bczk$=T zvi0KafaHR`hv8jiY>s%g3@Cr~)mMMNXwjm~?Cfmo=D)nREf8*(9>2j(qJ7zNu;P;U<@!H_!B%)+tAQpVp+|e9x%p)F=ks^n|bx> zRkOXl-3*07*5|PYLdH60=gytc(W6JxkX69%#96swlfy0~obqM1u)vT}qee}B>7|#v zJ@qVOwWFiM07DoD4jeGB)CP<(ii(O1B&dPdH!?Fb4Y&KJ+cO844-+R&bgWyquKeML zAC4e76%G0&mc#M9OE#DUEC&Ai`RAWU{C+0$(Z`&PsSQGaoL;M9;2uuL-8Q1PO-u1`{gYLio{>f9PPIWR_y7Q{f=QBR} zUNOmR&hyL^D&o5vi#Pcq1#xPjEe0kgE z&6~dxL+Qbj5)O4**}Wp1s{XbeJ9f;7r^k6u@_a!-f$}bC*6p+j2eLzcadB};d3m{$ zKe9V`BhZ!c-h1zbKmPdR9r%dc$%=JGIv~`qfk+oq1yFRl-dKu_07x6yO9E98k*Qv z;gBi1!F$(TcV#kLqzDf7x1xsvq*7A{;uTx)ES zW-N8|FqHSvFTVJqhddiEKj>YS^>^QW_kw6;B{hTYn4qS2g1A{PyQe3OQB`f*w(Wc2 z*GuyBerYV@d6veO0=FVbZ-J~#=v{^aC@%{2#)Q70gC zf56xvm?fb+<#(ulCMxVjN?MV&W8}fFk(W_X^?Z^V^jq73A4Iiz5>&0 z`{nyatx*h#XVWx8`-z8#p*W0r>#es+?zrO)OY6onmN83SNBh;he*OBtOCe&DS}XSn zs)37TFtb}a)OLwwzhq{r*7t>E6*}bmu&_%nC6*}mf#IO>ThURELhqN%-i^6n#|UhX zptFrLXU-T;J@r)kjvYJpN^5jlB&AR8d3_U#55h27O=<$#=t<4ZVnfNuWIXfCGwh;kw06Hqk~t7NpSY%!$rsA#?glJL z5)O=8FS>}|y!P5_cjo8kTORGM?We)?>?^OlVzvCjR8CDCsee1V9i)g@xo!<(^npWu zZ)pU0^;R&5<<%Yy^$;Q~|9kfAF_7TKJMX-6GsIMlghI1~Cu5i4YIYSRt(S9YQ=TkI zqFg~Ak)7#6FUn9hsZ)R@D_5>O!LIj44|dRjK)}3w`Lg-;+iwRD^ILGe6~J$WWpczW zYb4Y!6j>EFNq zo#^F4&CSg!!PS)0${klsW$Gin+yJRi1B9k>CAm^G8Z@=ANQNNtL+ls8Q>hm(UUb0- zCo3aC?aZ>yxF+l#M0FU2g@vvGI8da16dG+6!-F4u@PT#1WKH@N$)IoG;K753^y}Bp zi&b+#PWlcWJgAh}X-!TBB{bA;i{*jyWGOumJG~LBg{Yo+EX!1B>FMcl*@INa9ubuI z17yH@e6(LUnca8DAxqy^ZdQurc<}MZAD@hDcPTxr_!3R#X^8eev+EV(n}t!*q)}9{ z?h)aWE`Rwz13-0U%a$$c|HKnd7_55nn1uKG@WT)NsE%v#zq^H-S+0Ysk5kjhk!#nk zom*O3>f)4#o*-DI$g5CUeNLP>5dKBT$ z)!>(NIX-BptE+2;*vU+EFB*;fNs`~WRS|J&TT{lNLx-+XH6=0_+3%5r%Fv$N_RuP4 zMB6QL6ciM=de|+;Zlzfv5f#rc20NIc@uo3MLd2gMWo4;pjC2_B=FCbbXG^Z+&YnFx zt-Fqyi4Lj@X&XCHWpG5unF-W_G8v%UvpX2u6%`c@S=zp0H1DhmkYUp%sm_D6cQdGR z*rSg=YQ!5M=p8dc@4Vt*RXCiQoMh6}@7=riN4D{HBP1^4h!G=<^78VbVnDvo8EGwv zF)K*csO~1=cJ_3)tXj3InA;7BLW0I3JR?Gzt(u(e@~~g%=<51Wup&?6c2KK|ykm&l7o`jvcv9?65&uoXpDVjpW0`0iBfRm(8+Ew$*D|kDU32Hcg$qq3FJ0mtF5wJd8D$;>NSIpycAF@a zgbbZovu1^)RC6}G5!eKLjehR*qD70s5JD!m8|Eri*R5Me0-%Oi1Wc41NLBLE z`j0kU%urheTuLNI1xob{G?6aGZfxymoKsT5aAWuG-NuCr7pxKvCNaj3A8$?38CLOT zbh>A$>d@1L!64gEz&I2?tc61dTevO(FJ4wln8}lh@`wM7*yAX30ioAIS};vh$ja#h6KBp6FpF&L}jOGnOu0S}<_n zz=X|ZPY)&+^#l^vfXJzZI{QjO%Wl9JiM!VfquttEtdBNl7>W=nTfTgG`TY6wGr06z z?i<;@<S}*mWBi6G$ z+0AzeH>(iI7Fp?)IRxQ_@i$GJII$QiE(h8<1(h)6zwGBQeLi3G(4M#+{6m!UPTE^Q(jkV@Jr zBHX9FFw3m)>r*ojc9)c9Jh!%i25>Duj4xuq#V{S}%HZR8b{b9(g7 z&yBuSrEgoQMrO?GUeTZX4x8iG_GX~d e_>~?15nuodi5A$|t>!iW0000OpYTi^Qr^^X#Z#l#=& zu!%q5p`ZNa-ygVgZe?E4U*w1K89+mXRMioZ}SqFB0l^X7k^IC0{g zqM{;^nVBhUHk%>n^73*g#cBfmZ#wk?G_fapvg$l(!92~OD50pK_>i)@{?w^cRV5`Q z!YIR23ZN(=BO^m(WihMblyc*V*^`NfZH|ByJS8r$B8n=C_m?bL(s=UZ$)tb{j(pUF zYDR`_^Wd=)U&h5&TToE&@!`XVKb!+#fonM=Jx^_>K?|;S7ey&(v3T3IZ7Z9aniQwg zY5H2s>u5A8AcHY8jm2`NK??_HQ4Ctt(fL(vZEe{umn+UyvqvNn5j{OULh6WSfLqSA zCU5zWw?+B+`OD9pJNI^BVWF_wr`Hf?F&qwS*)#+_GBTopj+kuY=0QvH=7zj2q^Li1 z=+Gw%7cPt|fgaHK__(-v^QH&}gUK!*wL%fw9Odn8R*Q`rH#WAmwmP!2vuALX0UtYd zOq@P_TKIgvS*dNsESbtQcx;Cx6{0=AS5;N@$-#pMONkycng@ftckiA!e*Cx?8ykyL zBw0;bai%s4JkEo>ahqO34dkPmni}D7IHr9#6bgxF&z^~0yLP3`Ppc+rkqdcyhvjX@ zjvYW_wEf14GsMW&8<%L8bGz!_3BovSg~sV{{1e!yfKd_Cnv?# zt5+j8ZrtdCkH{fKDS+C%kxc95t(N$^?C8;>HEe|RU5^!_ySrNu&+pUQui%Z#2=wF| z0S$T6x?Y*q^>**x-LP!gGDCS|z1_EOUmwwxC%MVp`WDdHY+UuE(@EaNt1it5>fc(y4DK2GJ;7$(Bv? z&a_V6G+KPHWy_YP_3PK$_43BFn3$LlXU?4QK6&!wOFH#`iXX91bz;4jma&JLfEMwt zS6^S>Kx$E#mzOuA>#ZcG}mop>e>9z(qGV8kPGjB6#Wt{LOQ?^mb^rMdOmYExrwm1 z_~6KqBXveyPbwH9uD1Zh4EE_&Mj4x8~z7Tdc*Tf9G0L_SNY8=;Ps)gtV;!{-uuRX4aiY&9l)zZ@G zBO_X9T&71Y$iYRSGzIY24%|Rg%3MBNVI^pnaA1Svyaj5GrpM! ze0NnVSFT*%($b>x7|U=GN00dS?b}-MR^nnV@Hs50g!ZNE4Bz8~=v)6(L6P=zr^V+c zkpRBo0gTDGUVhfAkqUEPxqbWg8n@eRICqO9rR=x0wF!^MBkte7zlc7!fX$id ze?aul=Z%ext=FzyTUuILnh>-`5nlhXNiwrnqs{9WbfT#fKS96W9}i7s_*`FKAM<$+ zz3qVsdrtJ2q;pQbQvTYtYpdCE&IVY^HWw*J$~2e0lW$7@G{`16K(QaR!KAjxj_Y(~ zWu;TUK>eK@+9Xw#_G}ysI+zZlo~lM-Fk*;jdbGE<*JEM|^u`WtoW|I&HxrU2mD4_a z>(;H#*4EZ4E`&t#WZ6HW2$^=;r2Tv0=jo!9{L#FxczYuXC5>8}rjm4p9t~ zxd>lz#!d;~)#+Xc6ReUfr?w=#%(4*bAeHb--CJ??6qwlY(TcI2lA|8-SN^nw-iTnV(#=W#*%!tE;P;$7e>M znG%#?wg68ky0K&%1jtFWaj(uMgjMwT06vf*Z6nNOYeQ?{uW_9P#M|oG8WG*h2(*}O z?zd>&+U{)#79j%PF=dzn_)ni^C&l!A?73*G0~GNjoo?FgUKE=LKEop|CXB3UOvTBR z=`?Z9A%5M&^h3?kq+JK#wd=pa<-C2`iT-@afq;ZDfQH|l)dC_b-lF)_ED zfO&xSL#GGY+uOUSxqMbqQliQkrlp(r&Ye3wkh&MT5yb4enyO#cNa~@Y!4#p(R$~BE zj&7@f4$q;_Z18`A+>H~Sz;Bi506MG!s(8e&zpxQ_60ap~9+jq3;JNRCwC#T6<^|`xTzuoqgt=XiRRh z_ond~O^gpp;_DiXTiZb(Gp zz3LXYWOE+88!KA!EDv$}J0V#lo$=Z^T9CQ)WzWKOrA~yg;fPSEl5$59> z2pg4^l~EE7CPV~tfdU{8$YJ7c3l|U;(WQ%N2_S!Nz6l~20s=rM&<=F-F@wJoBTCjA z1r(Q-md>uKs+yy!>ZpN%0iUL6P78mI61CZE2B7!!^bFM3*N5<4Bhbm|H4q4dA3l88 z#Y6`r!tyH-qVKc;uYs5BZn6PlewoTv9Dn@y@t=(yI~F`uA~GaL*L7XN4JmS}laJG@ z*l545t}gmYU$fio8m`rkn+;S~S6}@E8@qyVgNRI&ESghMQ8C@`_p3QMIqxma5a~5* z)(G-8=AicX_xr|=AOC&ue8S5wTp*T5RSu*Kuh*OJa5&y?;cR|qyt19iF}k&4$_En=FA!8!i5V&xb@bpTTS?=htr5I zTS3adQTNKs%=Au~G9?S=agQ4}E8`I`yQb{gwJV5Zeh-H}WeWtPrHV(?(b?IlUcY{w;=q05#tkoP3WKdj z>F9HSZu~7D)2uA2B1NdQJn=SKa@@57k`J+a2YAXS@8Odg3FM?*(iKig_}F+gLQ)Cs z;$*MK_0dF2Pd?e$*hr)rfO_B=6AeixwX&!jutKSc@@?baL|BaU@@vG3sEvu*k_A#I z6r#Ir1zH&NK5Uj{<_%KPCS@7BN%ty}0MR9>Hz?|rB;wdyS;SrUvGN&6Mv^wZs5TO< zh;1l@;Tp0E(i8aQh;Hgv=?qLd!Nnbd%|cYi6#P)Eo{Xq%IMo9nk(~}?1EV=04Abi9 zl2E3gh~Q>~lGm(!aHtj?N+TE5kdGff_Tg=G}`IFTUUvcxy^)wBNxu zLWC)UXvr+9mXwsJiApGHvvTCf5#_*v1GJlt@1_fZA>W{ALm*i4sO+}3wknq|U+%kq z|9)F-ZSB81Iyye%n%=|(qlK*99NCyxm=3JYhD&zO#Ar<~Gg(U7WhYf~buL50t7dTZE;Ewg6v$1TvSrJbhV$po{|Sd~agb3q2pKHvGWmob0uzbw{Q2`M%F4=$kd9K2 zvfSLt>aSnF?n9b+SyxwA&&s}8BH1IUavRUAve@sY0<$5yixw|l9PQ(+v@TS?zF@(E z)7bqx@Q}wl-QcOoKH9|>_FTPswIBGDYr2E26l6kgnOvKs#s$fK4o(}Z_cZ>Tj-??b z70#PCPqiXSD~JHIPn$Na1a9;%{;iu=M^jCyNn^hVasuw-V?id>$`DozSUA5f^OJ@J z1rQ#o`}gk;unTms8nPw1xPw=WF7EUD{Tb$bYBi*=c#aTDBbfyS1>>NEXb_+voy0&T z56eNSi88`?mUijVBsyL*CrFYZNF3zA|MIK?N!GjQ{dLoH+E65U=RuB4Nwg$+d3h9( z-EMZN|o)|vu7bGgA9n4*)A9((mHuaWx0_hrcRwY z7H>abpC*TQGAT1jC;NU5KNBdfsHiAgvSdlLtR|YoKoWLK=`JD@rJx8QU^?(6BOX~Q zt21ZLqzn6jG4ilghzE&eim8Z{?%cUEpWG1Hcta9J(}rU28oZ{J>7RaK=V@=vNvckbNLpivvS z8|aY=NR()hKluFY*|T>W8yllPvKncVMOLp~UBW%n1SV6+#PWEVzr~+V2P&2?UtYCi z#}21ejhC%S0tYS5E9}1F>K9<5v8+NCm<0R)*bhHyXl`yM7ck5le8_n6lox9sD)01Q^R1!yyw@`5-OXB48V2-e2eru^e9_OZ)I+1KQKrxN##9Zszgh6GV0k z2x^ZWJu5Oi1(pN9 zf@MEBbm&lTTU%T7#3QlMO`{|mWOW24iS9qJU(RFgN>$G(ND87Vj|6q37oj=Z*vizy zgrU0I(T^|Mx^?RZ)J{-upQv6Zdq@EVrPS2aM9|pXMk@LTxeV}}`+Heh4%Pu|QBhTd z>`UdUQpmnL0j^NCZr!?>Xu@*fm(c=1xs{}uRT3vFQ0=B_Lj);5*VfiXAfBJ2L%7S+ z>d#okH_MN0%#x8FgAXEV5)(eIKBGB5`Z>{mxOC~#QbcLq4O%Cj!>ZFTcu8+%hbY%nNvFx3~^%D;%VEsM5UR#L;CxMk4dF7gcA2jPdLP^RLnwq zP1C$?saxnB>S$g310vDPeE@`Y>G^qpm&(I%OKRL9sWD9=8#es~L;f4akZ1bCoj1uN zN;Bj^uKZ^f^WQXvJ@==;9I3&WEzS&oM7Ai=|NoP0gtz|+FaQZ$3c`e8^P>O&002ov JPDHLkV1kmK?iT<6 diff --git a/osu.Game.Tests/Resources/old-skin/score-6.png b/osu.Game.Tests/Resources/old-skin/score-6.png deleted file mode 100644 index b4cf81f26e5cab5a068ce282ee22b15b92d0df12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3337 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 TZ*tsI00000NkvXXu0mjfuoP_o diff --git a/osu.Game.Tests/Resources/old-skin/score-7.png b/osu.Game.Tests/Resources/old-skin/score-7.png deleted file mode 100644 index a23f5379b223d61079e055162fdd93f107f0ec02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1910 zcmV-+2Z{KJP)me#3{3O=paJEob5+V*U3o#8b12GSgK*5+fg)$R>*bGqz zu@xdrBK?##CxxX+6pDEdF%0n+{eGpa3S}h-QPbGi*m~i@h3~@Qu=2U#PN&nXr>AG~ z?AfzF4Gs?WVHv5MDf*dhf%q2U`!i?Gyq%ky)7Rq2WHS2v{JehU%9S~|_74#45DkQs zEDNRF5RFDRdOV)B@hP~p-|uI;ckiwsd^VB})Fa8ls4RmUQ6x!PI}-(QWo2cowY9Yo z-mW7@aPy35!VuQhdWymdZQHgjM4~+0qGS?;!*q6NXlPnh)io97a=F;{?b`zeQ_reP zk%xR38ykB&Jw3ezz+{@HF?{8L@5KX4j$evlEI+NgsIF$=g9QxqwlDP_-4Ub zMdOr-6lKrLM+b>nab!W5%ri%?cs>sOTKcX{ZEbCJSZb>y9*?ujmoHDikAKtDv_K`* z6}n)GUX#R|BD6%hAW9d6Ndl}gji?PIFj7%bp<*emd=Z@=m}U`f-AOg3LT=%vP}(<7KQM z$B#g^HduL=0DZp!dezKBWv9HWrluykW5*83%DV{qIIW64SV3QuFrkdRD+!-CaiSi3 zcq`~*u^8*_?tc9#=!?x*&h&$21XJ}{^A@mK1?XeKhK)1A%=)gQM~}8x zc^5)IFfhO#KYsi|Xm3_+1`Ev??Bzk|a6%1Z9dZWRt~Y z%dW6E@ut#6`2$w|S%Um3MmLejcV5g(7=AH$Sz5Sz{-DCmK0fu$kho|I~@SmOii z#9)VD4g&!vF(sT0Ul8oyEC&hZ@&LK-uM6G(C%(?n4mUY zKKR_s0d62nK3%43kZix8&-A@tj`_cBvQ7cXd4LEYQs4!`g|KEK*$wo_ zXSeBhujwJ~ioE0K(W5~RMvfervtYr3@sB?GXu_mPlU(89;hNLw z)IeDC`~6x^Pmcz|T17=g@40j5+VQ>i$dMyg@ZWl%F5t5PXp#qL6Vg3mX@5vJz{zeQ zIs`}tMgrNu_=19hIcwLhO^l6=)gmGyw6L%+&EdG+gv*9asB=G->=`jdv{rLbMr-<@+EK;xCYb;X}VHRAYp^7 z_-U97JuP}4@W8~06X(A7-g^&?8a2v6YX(|!up2E;;vF3w+Rd9cwW6Y;j>5vilUJ@> z`8N)q6XKQPBCU527R$PXD2M&Kz{{}c-|+PPxUfEu4J34)3v20hb#(?>T3U2iQ}6HZ zzvFzkijJVvOG`_2xY!Ncvk3SJun-s{@xpB5Bu7G3p>Rn{_i*TeM=(??o_OMk@aX7h zTZ__CZEbB@Wo4zdXU`t(+O=z4@J&9U6C)EnZQ3;Vym|99`nt#Cx#L>&Z(8}|k3a5S zvu4ewjg5^*u=@;fS$w$-M0?HAA0tD*2>8uYPd)V&EfdTlYJ)H8?d|P40!Cl7Xc2?r zIIs)Y3Ty%X0Bi>i=jP@%W0VX_@_&IF?*qd6&Ye4Zd_La}{HK5h1JWgZ8rD^bpGiMO z2v6CvWlLUGR+hsqm>4ko_wU!9dFGkQS>{(C0>Z-~vDz3A45p7h`lw9O%|*$R zDn~LPls332)v8L)xw3cf-Uhmm)#mkjwG}H?L`$wuP-N-1iY%*EB+6;h(4j+(C$!pF zl@KhNSdS!b2+1x+tr8{t1p1^R(f1ljyriV0s=K?}^7_1YWMrg<5zZ6?MhIDll7J)} z4j+sSRiQ}s{Q2`8VzFka>)oQDf^AAV_DHaFvZ~n%7H~kY#R^f6@G~rXoAM7;rRWBi zE?sI9$=4+$`a;M&b$!D)q3%I(l$@L#DQQ&+f5Wo6>FN7IJ#}COwL2(f+$}|iR*Ftf zPfrLndgM@3Q`0E|-LLNB6rvo)jzfnIRdASD!|i-3B_$OwKiU>YuoOn; zrzpXBNqG7Wxsb!u2E~dsQ{-W&peCbwO@}V_4Ie(-QWMjvAgR6b$}4Aaeab-!^%}8o zzsnS@Mj#+zd(f7oKq>h+Tu!I+jylep<>cfzjvYIeit_0c-yI?(lcoA(nTS5Jef#!g z=wi*vy%;B~j5x3Tt+(F#1iMP5kTi%Z=&m5C#f{XY!>XB+Cr|eB;(>%|@gYNoXpcYs zco@`D#*G^{lJM!1hB59s2vj~OwguGnWC{3 zVnwnWdRi1cnt#=*RR@}ynhuJ3yCl`I9UOa%>>{0l8p>oLx>EG$mkSpz)Y&(&6R^{^ zRASV()TR97Z#(72W9%YoYisq_UVBZ4fT8IAHLy$?fTX}q!74lZRiS|J&@wYKwN0Bg zSz3pFOv?wezFmibjzU|vZk>kwepQD3s&um~q$tUM--n=;_ zF)`70Xi0`ioJEBA{PWLm5o88okfAhd6!X-n=F#E9BbRH-moL|lb>b1oxft>W(?mzF zX%g?2qKlO_gl);MP+`g$(V+qgSwLrtf&{K3kFwWdr0xU$D&Y5{I9APbs;jGwfC{!@ zB#d^3jVk{0JK+1$5~YZhooX_MB{2{95uS86oZinr+ii??cB>#lP5Y^Iv%dgV13wh3 zmVLhm{sI4QJbn7KC1Dzegwd&#Nmf`QQ&*fK2vcOU#Hzo2>7|!yk>_lQG9~e*C<>L* zYX1ya4$J`_7K==l@7(u2;OD?!@Nji!&z?1|VP#UcI`zw6v6Ad`z0fi_+rMiTG#=nBrs_>aVY_cfuGW=gyt$ zqz?y^pPW=d@TH)dsZy){Z%lMdv5E<{;}9{l?k6(Y@vx-QFjcvt(ewD2PV8C^rX!FwH;c6@fwLr< z6S#cu6^Yj2 zHQ4#mm-WjzZQ>dYXytpX$STc@j>0Fw6)j|)su^=)1VT28%%P~|-BNATxJ4-dk4`h>H}#Q?{*{EsZ*!AOiNf!Mb_x&FI>3L z#9zZ zBrL7@ZIgb|Nu&}P?>42D>K!|F)Fa;xrOL9h?3giQvspDb*^;=|19des?5%vJN4Y$ zTo#Cb$$G*zpitHo{!B%N4)@#(FTBuC0mIcCt7~#US9jig^UZeo|0*C?eBEOVtVw*U z&UPZC;tVMGd2{B>aZ@-4a|!wuI|6pL<>lo@KZQ>vm>m!#VL3TD&e^kP8*TdlJKVnK zo_lU@V`JkX>2TFjYKK}6a|&~x3>oF(dJx`A3pX@0gcRax4UD5f>kwR-v04KQl$>0{ zc;k&XI&sfExc5(i1tNYD#LfP<5^7qsi;(troDg2ep)4pUsIRD~Fa&p~8xLkvi*{T{ zdG^_7t7RcYE!vG0-;cO$(Jo-^R&}_EqH`(CvokU>rfk@-;Q{E-Fe)Mj$Y9rS1N%1k zyoQnMEG#TMA5}z)1s){sI9)pHfWZtCME-NcDUY^*$fB$);rz@rb zQd~p5a*`-edNWRrN5NfU)6>(VrJ`{WxdFr7Dpjait=LHWQ)gO4X*JDq5A5X#3)1Rs zBd-vT2|$$WR|Uc?WL2l2W?R}!ucN3}PufjlCFE--gLzfTXAsgzxa9+#?F&vHuEb z{FqKQ8OQ*#fE*23@;S*T$}0H84HRLL>;T$;7NA+c_nkm5?!{Gzf&9Qdlg5uIHz3(` zU?h+S6amG2EiEk_yKv#cvE#>&&nPS`OvuX03RF~77=FLsh{a;Y_uqeS4h#&$>gwwH zE?&IYx_kHTpW52m>T&IL;2LlPxFr)H`99n?GF;+5-KT|cf-L(8pa2*Rj3-vDTJ^x1 zHERm0s;UA>Nl8Y0e7q4128}=Xe;Q&TOn zBsko`iI9AEcQ;od_U4;!*5UOa{&iX=P%l;|@vxi4?;tHctO(}hJG2;3~%3Z@=BPZQHh8`1nWQj99%%v-p4;Y30)a$pQ?n5?~4l zSMS)djauK2i;FYp_XPc3A2a}0ZvFc8jGY<~ z*K@kc<*Fz-j(AC7(}3Rt?`__^87tVv3}AW`C5}%~6PMS2S z`MKwwGjH6uVPZ0l*JFZaQ&W?y4qfDOQ0Am46nMcOX`n8`2QfB*gE4?g%{TtY&EXF&Dz^cb(d{`$a|Uw+B7 z#hiHrI1ZeWOhBvE0{`MFpvWA0_0?CSyw1@-k(F?7-_uV&Jx^ABloYlECnc&#b18C> zkH<4pu(G)g2HoY$mkneV`tAuKcUDr?O{sLZr1D(^>VSWvsMKD)desV4r>w&Ju3fv9 ziC~;qIaB08z>PF#H3FF=Z8%nb<&{^u+uPeuNnyAO+?H}2!ZNBX&?9<-CHoS-U-$02 z@AfmxI1Of-^73*6RhNNSC>1$HGMvv&I#EPV=Co=v{Sc4Ycst<)8vVK7$Lmk?Ydu8!XyyRAFAT96o3Bt=LW2H zMO>goBM%4jKWmbQKnRvuD-GZ5j=eL^;>HLJsE*@!q0_Vhs}f&~i} z_$8yINUjcw5*;MXn@8YJVUil=wu4$Bp9kT=AXnLmOAgZUUT#RgrnSe68Iz;+pF#az ze&ufm4jj0_0P$Kn{$NH%M!Kz`9n7PArD_D?rCrOE>Q*F8MF}*9MapYX1wwqO;-WXr zgQQh-cV4=5$s8(3CQh7~FA_Fa)FFR7orOu3G)w+hD9uC}8wz5^j2TmwFJEqW)dFQ4 zu80thb0Xa*)vWc|+wrNvEX3hcT&73*28e%Mt zI(n$o%Kzu*=O=5f;YgY}^fq$nb(UE-7a{3qpM93ro{qOOfm!leDd6W-qSAf?EG;Q1 zc?4o<$yZ-}m4fblNY_yAk(Za3XxqW^>01bWQn4CQW>20zeR}-dxpRF^Kh6#W0b-!5 zNqOOg7Zx5nb}UzfQHP{6MtT~oISOrBDUkixV~_a|2UZ6&+?I`j3d8wqWbYu|FE?db zzWnjWA2*=uEreEs#;@o=YUc%4RxjU_b;3k!o46%{`Cu2tDcl;0o9 z9h_P?+}pnIcgON4NMNzmT@U;f`XV}1BfwS+{%=A+nH?P+W@l%o2{FVH?b@ax$gM_z zuU>;K+*40I)hHd%BN8tOdJ~}3ShL8?Gw{J5QLe2D=(U7&A?o1t^mGHAwPjsq2B#Hb zIt3X6jf16OyaqEEgjJWV$sMp)MCBqB@#jvQIB{U>)~%Or-@a|QiJRdLihitwfmktC zu3TxXTer?|)6{z9#l-Eqtfnq%)V@cG~Rr<=EK-SWKt=FOWX0{#%bzX_<8ydO~R5ftJv6o;@Vw+6KW$R?q4=gvKV zPGt^yN6J?#u$)|gOG>M9>HGTnjI(FYnje1nVe=PXd{GCz(k`(>ojlnqi>Q5+D_5>G zOJ?gAYnylMnrdQ1xk{U)G9RjBAC~kQ8c$ZU`;ZP(Rfi*$%Ocy-(qh!s)<%yUInoJ9 za20=FkX_Oixgqxlje>%L3Airk((8JYp>~*pQhky&IcUg5OodS_b2D&K zG;UI9X=z4rad9%TMi@6ya<$80FFGW@No$pWv}YM-44pc4DkL%1qgncn{pp~HpES`C z*-B4n6Nm@_Wwilxu%Z-U{)MpkJ=dkb7|zvrr{nEzPXK#&`Gb-50cW&XqK+ zoR8jY(dyN!S0e9bz_;R%SN-Tz`cb}n5ge`i_U${006QkBu~DqitEHuw_O~iraq2_? z!F(Tcxm$&#l@Yj6#O*b$Y3|o^wC*03rRU9?S6x$6Q%ohMG6IkQ3#5Z|9$b3d(xpq4 zo7~bG*1PtTMN=n=r7ca3j${-`yA9^={rmSfN(O6@)eJk4_Ny}%1VIv0n$w02vqX7$ zc>*#=H3F|)tlXhlS|=P7!kOaFqoMXDp)~v5HiMWo(DWL6_Ut(=E7>YWiP{_IpjIMH zbzP%j< z>4J6+6LM;tmG-a#4MM5z%$YMWx6(~Ly<)|R1l*jud5SaxC1TAS*{jGEvTO(@0Tq)c zPp*QL4Z67ot;~JCojZ5_D0_mp-TYaVZxxJI+G}cRDpE}2 zRj}84?X}k)fkI1W3bY^gsIx%^5P_w&K4Uw^UXVdcvi9qAfyJXaX!mAJ?r-?$2ic6j zE?McWO-;9R#8e?ZMgpyJ?hat1-mGnh+)Ya$2QrLu;-oen7t=@jzqn%-91#>09@#eSe~xa{P!cO1}4eYfP$L>rto3I{Ze zt;qLj8ayImXxLBG-0Ra~YMUycNdW!TZ`%>l^>x&?9q_Zss;qVI4{tbBQkx-68-DQ^ jB>n#KLQK@`Y9D2Zm_ee00000NkvXXu0mjf72dv# diff --git a/osu.Game.Tests/Resources/old-skin/score-comma.png b/osu.Game.Tests/Resources/old-skin/score-comma.png deleted file mode 100644 index f68d32957ff8dc2e6e26e2b739eab85385548faf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 865 zcmV-n1D^beP) z8ION{U-N9C`MxinYv|$`M+}Q$F)W6~uoxD@Vpt6OzhDOaIXgR>o2*u=8V;)D@TF?C z+T17S{{H?ym;%w+TADXmhT?$-O-NWvN0;UR_GXqT)+8VcT7_OfX+F1jnFIZS?x8!# zh8}URz_+nygyLs1rfHeIXpZ(*xcNFpGu`( zwcG8nVHo$9mzUrA{eGSAow*&jOrP%XnP4jnsY0KQkB@6tS64o6^aq1MaO`wCezV#1 zPfkt<_8IyJm7o-H)Y71MrP!@^ySuwPg+f7WY;1^ZHX9uC`MfBVO5*6~=mXZgWiM7_ zg2J#BUQj3&i!V*nT&N{Y(}Zo?M9#738`=bqh+giU0>f>loV9|82+ zaU80#CVuw0IXW_vYCm)NDP(>JPj7d--Su=j9k?q&C<*&31~y-*}fwjTm!p*P&Jvpnyz z)m?^=o3|X6i423GymcH7s?X++lz!!LDcrGp4_y83T(4ux)LkG8*W#nj518@;Wz|pB rcm*5_XP0^3eKYf&7eE$5NN3S}(it^R$P@B}JRwiW6Y_*SAy4SP5!!0C zTE9FJKtC>>NlHB>x7U?fC3eMrUE}-6&6|p1Lzp5^Zgp4;-?Tka0Ir& z1rTBaEJ%Zoz;`eQ3$UbTh&3rgX9|7}x(Kd<2wyLO^E@RI`3er0F^e8jrdymkWwHaJ zU>EG6-At#`*8_n7WhgqG&Jb^YGairM;r=~O+gUez%_x%?%@8~chr`cux!mVouO}vx ziI~l1Vmh6Q;czJ0?Y5Q8W=ZG?xDRe~_LPX7{ta*sJS`LoBN8x8Qyg!)u8YB7AXw}% zxJ%jddP>*89q=fXO1TEF6vaC~s48xE{ zqtTROY?u4ELr$I4>-A5|~39QXWMIpY}VCkbsmq$cd@;G3M5vml{BBvrEa%t<@5R1rBdky#=iw0 zIjg12+aCmBcDQpy_7jQ3UL+DBYmj&PwOZ{J`d!cgAHWx$%}VBs{OkvExyH8F_z-XN zhAjBd4tYuwo)#^QA>$xAWkltmAR9EuCtu}5>6DRg%ppIcnq=Aa2%R;=oZ@xF169W7 zDr`CzH^QVBIE8JK`;M1drwJD)wp9Li|3LU5zyS0^RhH-3lmq|(002ovPDHLkV1mea BUy%R+ diff --git a/osu.Game.Tests/Resources/old-skin/score-percent.png b/osu.Game.Tests/Resources/old-skin/score-percent.png deleted file mode 100644 index fc750abc7e80287192efb65633e58b6b19e47125..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4904 zcmV+@6W8pCP)>HPEA?bv`x0yR5sa^IhC4IP-&%2W!qCT)6~GN6jP8z zltl$(hYNRk=iGM=@9+a<{p0;+&Rp*D`+dJ>|D1Ea=v3#IPV=h$PLh+zA^z^}r3W$! zGSIW_KsG`4u0GIsjfJ#}={1b#=re=(4i|0#YNprjY=)m@0_q3k3FJZV@v;Kd0W|n>QZAbRG^nNiO?^UR0E#l_jgTq9~z?!8NzghdU+poJL2g+hsf zr%s(3>2NrVMMXvR`T6-ZbeqY?$H!~YqD2vtCQb6+wr$(ljEsy-_`CopALtxOAAjEj zYuGe?=SEiIO=||zdXeGb;n7b$^;GQbx8L61U@)iw0|q>G>#eu0fbXu5AobnyNm!iC z4z)E9C>-cIe}DhP6)RT!WB&a4{vIA4N~hB)tJSLZ?Ahb^^2;xA!^W3iepw$D7N$%l zlbSejV#3_Hb7Ph-U;alyK|vN-05e)gekv`XA88NQ97OAmNJ~pgUb=LtH!!Mj-IkUX zjSNns|XW)x3H0^tau1n;SBX zn?MHf>esKInlWRBY5Vr=6Ysg_p1_kQPX@rpK|sYoRTMCIU1YF7xdbv8mYSNH^vENR z1SBLRC|uKMG%Balsp{(L)P@ZkPK)5?=uYJ3;9;i4!hqsIgHtwc+&E|Y^yx<2I5HzI zWE2-FuvbA>_Bc1})?TD*91RaRCO`cV;31^JRgM9}_V?m^S0O^aH#Y*}zzT%68g zvABfB&uwmQR^WS%l$4au;q4xv<76FG@VTw4nq($@$l0SFeDJ|3@X&S)+U<7L(9obR zT)3dX9M#ydW6_M1o10sk-?_WHtEi|bwPVK)pC5nxal+1>J4ff`miEGX(4U%`nq05AY1g8_2nq&;fsEnr zQ2Y3bpW%0`#RCTps3}vXd=GDT0Uf4*hJnW_qjhJRG*S3QMnje?S(4!E>)VDwyc!!D zReE|lGMG=%(|Yvi(Sbkw@IxZ_uK~=i)i+zSJD4+W8cANrELsIJhMFoVDN)~j_nrFn z*I(7<&70NW!Gm4DBQ0Vo$^*JUDGBp%OII0GBFb^v7<|huw|JS&<~BdV4OLZDsWofX zoQJoVoN_3L1ikalJI!a#oSE?ItFNli(9kx*vDs`cM(}=S3_n8+6&4oS_U+r(yldAk z=P$qff>B+qUcFkOfKk)7lu8+O)|5 zzIY7Y{t0x3g2*6X-n4b=R=?S^XGaeiGNcWYcpoHjmlc3kTPiCnFJFQSHeYt-%9SQs z8pH|t~*}3o*C3~41znaK}Kr%Bk?P!W^q8gaph!G?3&%c4t3bKg) zB0(YZEr4n)vu4e5`7Q>ZnwlEa;sv3_ik@mw1|Aps04W-@riwlVzGD+uw3;xglR`O~ zT@z^B4rI^<9(bgc@+4Y7E_lKK39#2FM#W>fwhU z_C*NCxcKLve{T8x_ur5Al@^tzbaF6UlNv!k0}NWu0R-qnZ@lrwfvgb;1OTcvLDDaw&AO2 zbN~JK>m)wK`w_8h*|H^%Hrz~t8Vs=QI!OG%oH9_0APDrulN3><7Hz5zE@l!V*v0(( z{8PJk?`}fL@`V}#O${Q(VzAx>N+gPZ;DHB*vDry54^h&(fB*iY6g-^NoC>?^uDcRP zjT)unNQ^>-#9v2REYxVx(N|11(?ikG^Im-M#RE_)I@vd|ℜ1r5i{-0$U=)+!&zI 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 diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-bg.png b/osu.Game.Tests/Resources/old-skin/scorebar-bg.png deleted file mode 100644 index 1e94f464ca497c1eb0645586af13f489c28bf6f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7087 zcmXw8XIPV26OFnmHc$`{5mf3*3r&i&l^Q@~={*qzB4CJg5|ZGGG!YOGFht0L6sZB} zEuesOLhmF>3%wg?!*@N9Ed1ps(W^tB(F z2aGMJ7`_u8M6@h9RIIKbr0077YB)E2`E-X!tE|Ek9hSDQ;kRCLl{=5(tiewIa3VNI zV+DZAQQZH6TpYQ;cZ3WtSSL1!cSJ}0BlJ=FuG16#t88p?r`Y7sYXvO`a|_~{xrM*4 z@8r0@>Z6@UJ4cGQay@*Fd?|W{^~|{-; zWB(W1(aNu|+lP?q{SQAf$O?jmdIo|y6|Z%5wT83?1>sTY{LWD8a##|YlouIR!F%F) z8I&f91-5_h?wDw!O$>#2WhQ#$#{C8Q)iNsdwpl*eW#j#}dz!H>_K}{IL*QWQ?fswU z$(x&-i}x2c`eW3Y^AU+n#%>W5*);sw4F2CH|FoX5iHz#xu5!3u)hG+}W=CTG$N>dw zHw7EaXMfi2c`rm4qG1j6Zts=EtT0yn>z5=XB@4i`K*}E3EJovS7dP}{5hY01H=OIY zoolvaQTxB;8)KflW@(~n!&P1hmW4gv1wD7WMl`X|R9&crv zysz2TxE=!hxUX9B^=lmJXpa)aRIF!o zwee%u@_PJ3QpM>^)TRI9wWrgYVdjNOUelRkTXM1Ia^yX64?7u%Yu*zs+AX2j&cqkP z{FuG9dOq}KJl?);Zi!HT7YhVc}?)7N%VfB3tejs@wQDFs1xW-s|5FcCC=z!tRIm^X}`!$PBj zs45ghAs3yvh_ai<-OZ2ULDq3<){AZVT;qSI4DuIu35&Lj!SS7NQWPv)#omd0mVmHP zSmEgKf*oo8o73%IGhWn_{a`Coo`?B91e0tOnga^GK%H0i(2~V3ya<>!I-f+`Du65R zZLiqXtxm^i9DdhgQ==P5E>5@g!_xIS8>S^MH|wb#W|hcM~pj{b%VT* zD4`W(T-sX%6dvVx#d6nt$e9Y+&9(G;YL-qqpOz%skq#9 znQB2rtUFofRMr@=zg$Cfld@%JIiH3NJ~bWv`?-ugk@>b)v7TYHw%GxM)NOYF>-T>I zv#IY+sI1lYi_%J%B!88OQaeipP5dxn9dVo}%#BJ3n#5+XlpoF$rWw*~lu66B~u=YreF^X zQ9U*4w zS|Gn+U575I8F`*@((7LFrO}-eY8|np`wOyH;+Xb^nK}X%$xgDDWM`YOxo3OA>)J-W z4HLer!{ecQ55_xlyR&6w@*1;Vze&|rkI$+x63V#2h}B^JfCLFv&L-OBi)a{=N&|L( zg&l2K)iVdz0-cCJkAAC^wa4L;3TcE4b-Fzlv$s$B)(US(V_om78THGbUp13=PLpV$ z<+cTfRMp78PdZS;WA?p17B#EI1oOAzTLQv-{1+FDl-{l=saEtk`7DM%URCzHON!}I zl%>#92u_%Sk|@~%vEf|WM3A~8HmOZ|npM%lm)rzX_gW9_wW?=YK#vX&@`g1Kn=?6j z{D_$JREl}V;;G{t`>~2(v<`yPMC$l;pM`g_ijC%5 z9kp+N))F$Aegj$E-6~ZPWAlR{85!*S{+0SpoiezGB4&d0dso;Q;o}yzemn=WXnf3S zE$wmMl(lH9cK2D z4YzxaLO(yhcQZ8j&Zd@zy#Mo)w(&&ov#?=Z`&S{uwo~X-h)smXJq5)Eld&f6TUK?1 zUr{z3jz&zaHB)`iK85?5;lXT2QWTM%2}D?d5wnY1qVf9s zBhO+ET1rUjfnSB!S0+$yu>5XCO%0$1p&*LSquhQzY5*5e{8{?OYQ(wEYK`+{ieR;M z?jT=M*{=1;rCoj?a5E4=9kTP_JfgQgBgD_C35Q7HhDguyix!^N7DQM$)bS z(VGNtmTdIcZq{1uJFPfI)`Q#5xp9aGQ0or}pBAXcZL;sMgXPsf@+i|iXpUd71DGcviJ(7EI$@FGjj*b+?*{y9F>tMJ*bWQ^M~y!(py3bc*(CL?0W-L@`y?!mN0zdpM&Yhl zu%y=TFnu|mR+!Fr?QyQ#kM!((e|WWrCHUcusb`mu0TtE!`Vr>nbeJ)r8iJ0^Bo_=z zO_hh2yN_p-Uy5|MnyaZoBqAK0$TL)g^T>N~dc>VHDp!Kgj>M)9tA9C9;$t549BJ*H z`c{r{K}UD}-fB%YX1fHh=0DRKYc|BJMX9*;CoqU)+R8<=2h2MRS7o}k8{Np~p`{NnOm6fB0DXZ z;m+!Enm_^l=|%TKe)M(;-k0^3QV|nfI_M0|$M#^SVkLB?mj7$UO+a=5uZ4zE`$ zX9_Arnp4LM#JIai@S2`sQwUJPd4}w{vGLpEWkq?>Sb_mvRc0vXnc^i0-7%Q)Xsarf zY7vhkh<>}+-EAd%J8pzcNzNGJo~}@upp}xh{sRZUMfMwqZNF5qKTvl~7O6knA_VR2 z(@6Es1%&SGq2&pb-Q3=k0&SDV{i%mHr($uZX>Nhl+WJ_t0|2E@R7%~slCm=YAdtYyNDVPe;?6KLr!QXrp?YS}g)3+v$QThFBkPJlS&wzYHPfwuVV;eN9B93ALeQDw$(>=M^V? z;@i3^J}VGvH`@(`Xf2kOmdxsr&fLxlXCk4bD}NbfH{Wr|VO~7PN*TD5myi*Nf5&~! za{o;+fb%%U2C2{ji#XoC_Xh6=4W&-!#ZC04tKs&^CLv!zdq&sheAfYdc_xc z@o%{jSM9Ni8#iy-epEJBcK3cc36xx4qZ)PU1_&+kr#xeJSz!@^8{d*UBMs(U=QTr;96W7& z8m}lS85$ZcIjI}KB<8kn68$>E~WIpt_GTVDY{PV5*;f(Dy<34$8mTZJRDZaGzYyPA74meprdp8=csNz-PrUXwUb&-~iI?zkIPV44)R~hWP z6<1l$`7av!Eq}+7yCeUO8=yH^MNh6Lme6d)$C9HJevuCX&j%s9^d``I z+cwc9SxKht^g$~K`q%Epw5Z!?DrdpM+-yx1Vla;B!u|r{O9ZL}^)|s24A5d_ROdd= z1&i75ss|&svsIuBmU9ok8=eU~MNsSzH}_Nwx1!`w}LcV?@;w9 zvzyo(^@`n=-S1|xX5x(Y1-aS4Y-1H@_ooff_y~>`vpD96vCdRK`mctkq|?B%d}nZI z?UU zna;UcUlE4~D?h_4`~KiF79qE{yFz7C9`rj+36FjVQNM0m+!{9c1UtNCdG?p|yTY2> zIEG(msE5!%1cxYV-S(J!i+ijGbh_pm&QAwZ60P8Xr4$b`woLILhq~ekXsHC_vE-wS z&GXoGEeZ{_iP%JfSw}9_oCJ@z8Sz}Pjw8woMWF0;e- ze5kGXJH0k`}B2&8Zw-(bP@Nef4Rb3aVh0QCX&7ANqUWo5hf zhnL=|>`Ma?yfvH18f2g9t?=KEgR_ns7N{&PWKDZpQkDRGTYs#}fS@~r7_=&R{-Hw_ zSXyems^7_%`9ZRbE}MX~5vhHYA$3KA6N?Xj6WyzBHCuKuz%)Nyvc*G{SpqL5u{OSR zut-gFFTAQpMo?3%+;hO%Q+ommu(aVVj0f!9Sf|}JKdk{5(F|OhHTy2y{|D42@FL16MvL1CS{->=Q27bwUl3r_-fSt z!$XX|>u}OPlMUB%I4(SNdA+OxfbA6+1{`rZL;E))~=7Z+++L31mA-nbD9&;Giw z2lc#l+Zj9T(`!qFMxgxZIxnjEU@k_lx}dGK&MU8v%Z@sW+Hn8ln4=;iSx7lwNokEH zT%^oPC$*^#X&+I;^C^;0UM`kkHx~l?LtU}4w`vo#?!b;R79$Vy2SsVCOTRXTWP87B zH5`j;-Fv(3&4r0$PPA*W2Mn{J1ve*iRVSru6|s*F!`>Wr*?ww%{mjyW$XBw^zMqew zm54N~Uc^EYnA%OJFe%3%0OdBT?nm+oySWZVg9SfiVJ$E-EH(Omre7z%dU8#^qqDYP!JSp|PXZ0(XsM0Av+qK5*aE7HVB z8tET4t&_7_locTzTdD|>3ur%GmQv78YVTT0{$fNJAUKJi*}r+xr(WQ?$6;3ro+)g* zxe(1_fgKa~IUqLkm$;BCJ&+}H7 z53`^kjap2E<(sQ|B@!y&+l!WyHJkx5dUJ!y?Wn-hF9@A$B58yS3=+wP!Tt{0de*KO zH$;ArMnxz3vm@kA4L!mLn*cj<`J6qR>SK2&3bt)#yc7KiiI_2s8xSqN>G=Tg1|+xhR_(vCy>Pg!PhIZ_d=2l%~|^q>$JEeBtyE z#hd>^3hhDL{*sQ#r`%cFu4~(K#Y{mR@J{%CZL~sSS%=6ssn_WBcHW>!i1cw_uT}>3 z+B>YtQJKl=w5(y2FrDiX>f39Q%qN$keDCWml5WM>_0MwYs87PFQf1*ya?ftUWln-VGC;n$BXHY7S$Ea%VeSPE~sS0Ts4F|PLys1AZ#y4xKZE<9=d@`r@`n~4E= zM%oVDO2&32++oROe}5=<(R(phmTg~mG}iy3Yk3Ae(| zpcQwBIO3W_$<;)v&Dstou=4@=^6D=QczqpAcSmODXf1G}LaITP*}d~zHH^Y>Lh7YZ zPsz3`UAYC8hHu8dPw=6?*R?+P&oYtgUf|f(88hkb=&(ycFGWK3(_;}uN)lUo>$1lO z0Wa=3lggLF18b5eIj2#lib(~B+p#$PAYwx~!|I>!4{Acv=i2wtJCo~w_MZr+!aiFn+P=DbXaq^@avvz;4R%~B@hfxweBE2)=&E~}ayMAAq7@)1@3zSVE z-Ph(l%4+Q%;w_qibD%(c169sLjOn4SP|z;tPeCg>9il%lOY^ zBF7%_`Qze$OG)euo8Dx1J{CCY zjluYo2@5M7nQDCNIKfdQX{uqoTw{``BeoqGe&{kq1gR50ZpiVn@~ejD?<;@~uK&kM zISYEnx+-F#LZ$7)B?E>FOoTh9DaqSI{qpg0qr;_GxJbGdF7$2NlY2fE}!K?$-2{^bW-n3mv1IZSH?_%et4|Fk+RLRa7-(G(Jm!?auK!3;_6fkZ^#|#FV!{ z`Oc{Pwdh@YGH3yCmv)j=z^Tn!fdAGhFM>zvZ&P18+}S$&n{5C<3M`%ZJFkHMb|V15 y@A3cuUj5c{fFir%4t9?0la>ys{KyD8bq)}CIm!`G%3vP5*VlQhU8wow_5T3A#}4QK diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png deleted file mode 100644 index 1119ce289e0bf1f5e7d4aba634d5b42c169c32fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 465 zcmV;?0WSWDP)p000dL0ssI2y8t*G0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzb4f%&RCwC#+rdtQKomyd|594)!lZz& z=nMK_L!yZ>F@j|%O43XjXj74#FI`;6fzpk~S=1WINrg_BnVH9Dlg!W7&UMnyiC<4n zbF=B^RNAz!L3o40cB%1c@5@AIaj?BDlQ#gm_p&r|uidw$nmYx*Zh$?YX?p%*;GG zSME6xfV;7Y8|-Y^#y2na!vVOABm>Jx)1Sr1^|)_{7IKAdb3UM zI!OdbGJu(f;~9OmDQ^@r({X?#1DF=W_$mWf$zO%Tw*UhGi7fzzR>awB00000NkvXX Hu0mjf)F0A( diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png deleted file mode 100644 index 7669474d8bcce7afd6a0bc8c247b2c0ccef46c22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 475 zcmV<10VMv3P)p000dL0ssI2y8t*G0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzeMv+?RCwC#+uKfqKoo`HwP^v37s}Mv z^v!%U1!Ihfi4kX}CPfX0X%(^kzi_c2n-*@mIwiGMK`l9bAd|@>+2kxH3;8PBW#@C; z{Cu^Jc@@viL-4zIex{JtVGGC4)cTyOZsjiG@m%+Q=2AMB3N?PF(pxDjRlkO;GHmsW zDi?wks-NXH;lwsq^CWj2U&p`iW6Y~g&f;}G#QE*L)Y`aRc23sGTTLhT%?uwWlgXpm zbdw3dY%FoWz8$vl;lR0=*8l*=7DjA@#JG(ZmZjD|Aj#jH%Dj8Vb z0@~6~q?G|y$p8**XzN)n_m!^o7qL;+0F?}&zc{Cs002ovPDHLkV1lE5)RO=J diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png deleted file mode 100644 index 70fdb4b14637abdf6237c4bf883fac33542d91f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 466 zcmV;@0WJQCP)p000dL0ssI2y8t*G0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzbV)=(RCwC#o83->Fc^m45BKBF0C$_a zGcQd`G{%^iSZSw`Wa%hu62#{T2XC9NWt{Nvv0z?bIH@L+$vN5NEM^PGL-C!9SaTCX zJA5o+j>AyD4&h@JYqruV9Br;{rFG10%`RlWm220Q(nhwE^D0YOyQH0eS6zqTH)`Hg z?PZ&|_Eq&>KJL@JgsgaK_uR=(F&}?t6ZeTU?k78^yG}mNTPLS#&d-y{}3GHw0Ev%NR~I1fq&n1`AM`PHtbK|Bh8k^vBQXJ~o4HM|P2SiJ60GQj*AXdAu| zD+ACa1Hh$*wnV#}lCGsiY{(h_B?CZOoVk+hwQ{%8GWND>UJwAFWPn9@I%QDH(4Zwu zpDAwy41kgW;6n9f343z8qHO?_4DbgPo7n{Gh`%H^(FVYC}$xlkqqJN(O*oGR(I!04(ILP<;w80AoP?p8+Di_5c6?07*qo IM6N<$f~e2NAOHXW diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png deleted file mode 100644 index 18ac6976c95a162914d6cdd371abf71aca48c72a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 464 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 JPDHLkV1iG9rlSA= diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png b/osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png deleted file mode 100644 index ac5a2c5893b635520d3f8e3804ff87101664d591..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7361 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 diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png b/osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png deleted file mode 100644 index 507be0463f0732a07aa7df87cd57c1f273c7d55b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9360 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 KMNUMnLSTX(dllStore, "Resources/metrics_skin"), this, true); legacySkin = new DefaultLegacySkin(this); specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore(dllStore, "Resources/special_skin"), this, true); - oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore(dllStore, "Resources/old_skin"), this, true); + retroSkin = new RetroSkin(this); } private readonly List createdDrawables = new List(); @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual Cell(2).Child = createProvider(metricsSkin, creationFunction, beatmap); Cell(3).Child = createProvider(legacySkin, creationFunction, beatmap); Cell(4).Child = createProvider(specialSkin, creationFunction, beatmap); - Cell(5).Child = createProvider(oldSkin, creationFunction, beatmap); + Cell(5).Child = createProvider(retroSkin, creationFunction, beatmap); } protected IEnumerable CreatedDrawables => createdDrawables; From 18b5c652a3f88c5ebd442c225c5477a3556d5acd Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 5 Sep 2025 12:25:40 -0700 Subject: [PATCH 132/267] Fix `ArgonJudgementCounterDisplay` not showing colored numbers when "Show label" is off --- osu.Game/Skinning/Components/ArgonJudgementCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs index 9d0e369682..6fe8ac7ecd 100644 --- a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs @@ -60,7 +60,7 @@ namespace osu.Game.Skinning.Components var result = Result.Types.First(); textComponent.LabelColour.Value = getJudgementColor(result); - textComponent.ShowLabel.BindValueChanged(v => textComponent.TextColour.Value = !v.NewValue ? getJudgementColor(result) : Color4.White); + textComponent.ShowLabel.BindValueChanged(v => textComponent.TextColour.Value = !v.NewValue ? getJudgementColor(result) : Color4.White, true); } private Color4 getJudgementColor(HitResult result) From 22bfab95b0ee6bc63535748277b56317a4bd370d Mon Sep 17 00:00:00 2001 From: Valerus9 Date: Sat, 6 Sep 2025 06:43:13 +0200 Subject: [PATCH 133/267] Fix testdouble failure. --- osu.Game/Extensions/NumberFormattingExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs index 33252448fc..fe2ce37a0f 100644 --- a/osu.Game/Extensions/NumberFormattingExtensions.cs +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -36,7 +36,7 @@ namespace osu.Game.Extensions string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; - return FormattableString.Invariant($"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"); + return FormattableString.Invariant($"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}", CultureInfo.InvariantCulture)}"); } /// From 111b98ef8eccb8f43b232cdd31cc57e8592c84a5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Sep 2025 15:04:22 +0900 Subject: [PATCH 134/267] Add matchmaking --- .../Matchmaking/TestSceneBeatmapPanel.cs | 28 ++ .../TestSceneBeatmapSelectionGrid.cs | 184 ++++++++ .../TestSceneBeatmapSelectionOverlay.cs | 68 +++ .../TestSceneBeatmapSelectionPanel.cs | 57 +++ .../Visual/Matchmaking/TestSceneIdleScreen.cs | 89 ++++ .../Matchmaking/TestSceneMatchmakingCloud.cs | 44 ++ .../TestSceneMatchmakingQueueScreen.cs | 55 +++ .../Matchmaking/TestSceneMatchmakingScreen.cs | 244 +++++++++++ .../TestSceneMatchmakingScreenStack.cs | 119 ++++++ .../Visual/Matchmaking/TestScenePickScreen.cs | 106 +++++ .../Matchmaking/TestScenePlayerPanel.cs | 94 +++++ .../Matchmaking/TestSceneResultsScreen.cs | 98 +++++ .../TestSceneRoomStatisticPanel.cs | 32 ++ .../TestSceneRoundResultsScreen.cs | 102 +++++ .../Matchmaking/TestSceneStageBubble.cs | 49 +++ .../Matchmaking/TestSceneStageDisplay.cs | 56 +++ .../Visual/Matchmaking/TestSceneStageText.cs | 43 ++ .../UserInterface/TestSceneMainMenuButton.cs | 25 +- .../Online/Multiplayer/IMultiplayerClient.cs | 3 +- .../Online/Multiplayer/MultiplayerClient.cs | 114 ++++- .../Multiplayer/OnlineMultiplayerClient.cs | 82 ++++ osu.Game/OsuGame.cs | 2 + osu.Game/Screens/Menu/ButtonSystem.cs | 22 +- osu.Game/Screens/Menu/MainMenu.cs | 4 + osu.Game/Screens/Menu/MatchmakingButton.cs | 19 + .../Matchmaking/MatchmakingAvatar.cs | 68 +++ .../Matchmaking/MatchmakingCloud.cs | 117 ++++++ .../Matchmaking/MatchmakingController.cs | 170 ++++++++ .../Matchmaking/MatchmakingPlayer.cs | 31 ++ .../Matchmaking/MatchmakingScreen.cs | 342 +++++++++++++++ .../Matchmaking/Screens/Idle/IdleScreen.cs | 27 ++ .../Matchmaking/Screens/Idle/PlayerPanel.cs | 197 +++++++++ .../Screens/Idle/PlayerPanelList.cs | 80 ++++ .../Screens/MatchmakingIntroScreen.cs | 263 ++++++++++++ .../Screens/MatchmakingQueueScreen.cs | 393 ++++++++++++++++++ .../Screens/MatchmakingScreenStack.cs | 121 ++++++ .../Screens/MatchmakingSubScreen.cs | 43 ++ .../Matchmaking/Screens/Pick/BeatmapPanel.cs | 192 +++++++++ .../Screens/Pick/BeatmapSelectionGrid.cs | 340 +++++++++++++++ .../Screens/Pick/BeatmapSelectionOverlay.cs | 139 +++++++ .../Screens/Pick/BeatmapSelectionPanel.cs | 213 ++++++++++ .../Matchmaking/Screens/Pick/PickScreen.cs | 83 ++++ .../Screens/Results/ResultsScreen.cs | 345 +++++++++++++++ .../Screens/Results/RoomStatisticPanel.cs | 52 +++ .../Screens/Results/UserStatisticPanel.cs | 49 +++ .../RoundResults/RoundResultsScorePanel.cs | 36 ++ .../RoundResults/RoundResultsScreen.cs | 181 ++++++++ .../OnlinePlay/Matchmaking/StageBubble.cs | 157 +++++++ .../OnlinePlay/Matchmaking/StageDisplay.cs | 91 ++++ .../OnlinePlay/Matchmaking/StageText.cs | 84 ++++ .../Multiplayer/TestMultiplayerClient.cs | 102 ++++- 51 files changed, 5637 insertions(+), 18 deletions(-) create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs create mode 100644 osu.Game/Screens/Menu/MatchmakingButton.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs new file mode 100644 index 0000000000..c46beba037 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs @@ -0,0 +1,28 @@ +// 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.Graphics; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Tests.Visual.Multiplayer; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneBeatmapPanel : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("add beatmap panel", () => + { + Child = new BeatmapPanel(CreateAPIBeatmap()) + { + Size = new Vector2(300, 70), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs new file mode 100644 index 0000000000..79ed79e388 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs @@ -0,0 +1,184 @@ +// 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; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Tests.Visual.OnlinePlay; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneBeatmapSelectionGrid : OnlinePlayTestScene + { + private MultiplayerPlaylistItem[] items = null!; + + private BeatmapSelectionGrid grid = null!; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + var beatmaps = beatmapManager.GetAllUsableBeatmapSets() + .SelectMany(it => it.Beatmaps) + .Take(50) + .ToArray(); + + if (beatmaps.Length > 0) + { + items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = beatmaps[i % beatmaps.Length].OnlineID, + StarRating = i / 10.0, + }).ToArray(); + } + else + { + items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + }).ToArray(); + } + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("add grid", () => Child = grid = new BeatmapSelectionGrid + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }); + + AddStep("add items", () => + { + foreach (var item in items) + grid.AddItem(item); + }); + + AddWaitStep("wait for panels", 3); + } + + [Test] + public void TestCompleteRollAnimation() + { + AddStep("play animation", () => + { + var (candidateItems, finalItem) = pickRandomItems(5); + + grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); + }); + } + + [Test] + public void TestRollAnimation() + { + AddStep("play animation", () => + { + var (candidateItems, finalItem) = pickRandomItems(5); + + grid.TransferCandidatePanelsToRollContainer(candidateItems, duration: 0); + grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); + + Scheduler.AddDelayed(() => grid.PlayRollAnimation(finalItem), 500); + }); + } + + [Test] + public void TestPresentRolledBeatmap() + { + AddStep("present beatmap", () => + { + var (candidateItems, finalItem) = pickRandomItems(5); + + grid.TransferCandidatePanelsToRollContainer(candidateItems, duration: 0); + grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); + grid.PlayRollAnimation(finalItem, duration: 0); + + Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem), 500); + }); + } + + [Test] + public void TestPresentUnanimouslyChosenBeatmap() + { + AddStep("present beatmap", () => + { + var (candidateItems, finalItem) = pickRandomItems(5); + + grid.TransferCandidatePanelsToRollContainer(candidateItems, duration: 0); + grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); + grid.PlayRollAnimation(finalItem, duration: 0); + + Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem), 500); + }); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + [TestCase(7)] + [TestCase(8)] + public void TestPanelArrangement(int count) + { + AddStep("arrange panels", () => + { + var (candidateItems, _) = pickRandomItems(count); + + grid.TransferCandidatePanelsToRollContainer(candidateItems); + grid.Delay(BeatmapSelectionGrid.ARRANGE_DELAY) + .Schedule(() => grid.ArrangeItemsForRollAnimation()); + }); + + AddWaitStep("wait for movement", 5); + + AddStep("display roll order", () => + { + var panels = grid.ChildrenOfType().ToArray(); + + for (int i = 0; i < panels.Length; i++) + { + var panel = panels[i]; + + panel.Add(new OsuSpriteText + { + Text = (i + 1).ToString(), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Default.With(size: 50, weight: FontWeight.SemiBold), + }); + } + }); + } + + private (long[] candidateItems, long finalItem) pickRandomItems(int count) + { + long[] candidateItems = items.Select(it => it.ID).ToArray(); + Random.Shared.Shuffle(candidateItems); + candidateItems = candidateItems.Take(count).ToArray(); + + long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)]; + + return (candidateItems, finalItem); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs new file mode 100644 index 0000000000..4e596d65cc --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs @@ -0,0 +1,68 @@ +// 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.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneBeatmapSelectionOverlay : OsuTestScene + { + private BeatmapSelectionOverlay selectionOverlay = null!; + + [SetUpSteps] + public void SetupSteps() + { + AddStep("add drawable", () => Child = new Container + { + Width = 100, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, + }, + selectionOverlay = new BeatmapSelectionOverlay + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + }); + } + + [Test] + public void TestSelectionOverlay() + { + AddStep("add maarvin", () => selectionOverlay.AddUser(new APIUser + { + Id = 6411631, + Username = "Maarvin", + }, isOwnUser: true)); + AddStep("add peppy", () => selectionOverlay.AddUser(new APIUser + { + Id = 2, + Username = "peppy", + }, false)); + AddStep("add smogipoo", () => selectionOverlay.AddUser(new APIUser + { + Id = 1040328, + Username = "smoogipoo", + }, false)); + AddStep("remove smogipoo", () => selectionOverlay.RemoveUser(1040328)); + AddStep("remove peppy", () => selectionOverlay.RemoveUser(2)); + AddStep("remove maarvin", () => selectionOverlay.RemoveUser(6411631)); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs new file mode 100644 index 0000000000..addb0ed3a0 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs @@ -0,0 +1,57 @@ +// 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.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneBeatmapSelectionPanel : MultiplayerTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + [Test] + public void TestBeatmapPanel() + { + BeatmapSelectionPanel? panel = null; + + AddStep("add panel", () => Child = panel = new BeatmapSelectionPanel(new MultiplayerPlaylistItem()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddStep("add maarvin", () => panel!.AddUser(new APIUser + { + Id = 6411631, + Username = "Maarvin", + }, isOwnUser: true)); + AddStep("add peppy", () => panel!.AddUser(new APIUser + { + Id = 2, + Username = "peppy", + })); + AddStep("add smogipoo", () => panel!.AddUser(new APIUser + { + Id = 1040328, + Username = "smoogipoo", + })); + AddStep("remove smogipoo", () => panel!.RemoveUser(new APIUser { Id = 1040328 })); + AddStep("remove peppy", () => panel!.RemoveUser(new APIUser { Id = 2 })); + AddStep("remove maarvin", () => panel!.RemoveUser(new APIUser { Id = 6411631 })); + + AddToggleStep("allow selection", value => + { + if (panel != null) + panel.AllowSelection = value; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs new file mode 100644 index 0000000000..49daedb6a3 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs @@ -0,0 +1,89 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Tests.Visual.Multiplayer; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneIdleScreen : MultiplayerTestScene + { + private const int user_count = 8; + + private (MultiplayerRoomUser user, int score)[] userScores = null!; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add list", () => + { + userScores = Enumerable.Range(1, user_count).Select(i => + { + var user = new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"Player {i}" + } + }; + + return (user, 0); + }).ToArray(); + + Child = new ScreenStack(new IdleScreen()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.8f) + }; + }); + + AddStep("join users", () => + { + foreach (var (user, _) in userScores) + MultiplayerClient.AddUser(user); + }); + } + + [Test] + public void TestRandomChanges() + { + AddStep("apply random changes", () => + { + int[] deltas = Enumerable.Range(1, userScores.Length).ToArray(); + new Random().Shuffle(deltas); + + for (int i = 0; i < userScores.Length; i++) + userScores[i] = (userScores[i].user, userScores[i].score + deltas[i]); + userScores = userScores.OrderByDescending(u => u.score).ToArray(); + + MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Users = + { + UserDictionary = userScores.Select((tuple, i) => new MatchmakingUser + { + UserId = tuple.user.UserID, + Points = tuple.score, + Placement = i + 1 + }).ToDictionary(s => s.UserId) + } + }).WaitSafely(); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs new file mode 100644 index 0000000000..c25057c84b --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs @@ -0,0 +1,44 @@ +// 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.Utils; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneMatchmakingCloud : OsuTestScene + { + private MatchmakingCloud cloud = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Child = cloud = new MatchmakingCloud + { + RelativeSizeAxes = Axes.Both, + }; + } + + [Test] + public void TestBasic() + { + AddStep("refresh users", () => + { + var testUsers = Enumerable.Range(0, 50).Select(_ => new APIUser + { + Username = "peppy", + Statistics = new UserStatistics { GlobalRank = 1234 }, + Id = RNG.Next(2, 30000000), + }).ToArray(); + + cloud.Users = testUsers; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs new file mode 100644 index 0000000000..ea2a2d15eb --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs @@ -0,0 +1,55 @@ +// 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.Testing; +using osu.Framework.Utils; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneMatchmakingQueueScreen : ScreenTestScene + { + [Cached] + private readonly MatchmakingController controller = new MatchmakingController(); + + private MatchmakingQueueScreen? queueScreen => Stack.CurrentScreen as MatchmakingQueueScreen; + + [SetUpSteps] + public override void SetUpSteps() + { + AddStep("load screen", () => LoadScreen(new MatchmakingIntroScreen())); + } + + [Test] + public void TestBasic() + { + AddUntilStep("wait for queue screen", () => queueScreen != null); + + AddStep("set users", () => + { + queueScreen!.Users = Enumerable.Range(0, 10).Select(_ => new APIUser + { + Username = "peppy", + Statistics = new UserStatistics { GlobalRank = 1234 }, + Id = RNG.Next(2, 30000000), + }).ToArray(); + }); + + AddStep("change state to idle", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.Idle)); + + AddStep("change state to queueing", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.Queueing)); + + AddStep("change state to found match", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.PendingAccept)); + + AddStep("change state to waiting for room", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.AcceptedWaitingForRoom)); + + AddStep("change state to in room", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.InRoom)); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs new file mode 100644 index 0000000000..416811d345 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -0,0 +1,244 @@ +// 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 NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Tests.Visual.Multiplayer; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneMatchmakingScreen : MultiplayerTestScene + { + private const int user_count = 8; + private const int beatmap_count = 50; + + private MultiplayerRoomUser[] users = null!; + private MatchmakingScreen screen = null!; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => + { + var room = CreateDefaultRoom(); + room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + })).ToArray(); + + JoinRoom(room); + }); + + WaitForJoined(); + + setupRequestHandler(); + + AddStep("load match", () => + { + users = Enumerable.Range(1, user_count).Select(i => new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"Player {i}" + } + }).ToArray(); + + var beatmaps = Enumerable.Range(1, beatmap_count).Select(i => new MultiplayerPlaylistItem + { + BeatmapID = i, + StarRating = i / 10.0 + }).ToArray(); + + LoadScreen(screen = new MatchmakingScreen(new MultiplayerRoom(0) + { + Users = users, + Playlist = beatmaps + })); + }); + AddUntilStep("wait for load", () => screen.IsCurrentScreen()); + } + + [Test] + public void TestGameplayFlow() + { + // Initial "ready" status of the room". + AddWaitStep("wait", 5); + + AddStep("round start", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = MatchmakingStage.RoundWarmupTime + }).WaitSafely()); + + // Next round starts with picks. + AddWaitStep("wait", 5); + + AddStep("pick", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = MatchmakingStage.UserBeatmapSelect + }).WaitSafely()); + + // Make some selections + AddWaitStep("wait", 5); + + for (int i = 0; i < 3; i++) + { + int j = i * 2; + AddStep("click a beatmap", () => + { + Quad panelQuad = this.ChildrenOfType().ElementAt(j).ScreenSpaceDrawQuad; + + InputManager.MoveMouseTo(new Vector2(panelQuad.Centre.X, panelQuad.TopLeft.Y + 5)); + InputManager.Click(MouseButton.Left); + }); + + AddWaitStep("wait", 2); + } + + // Lock in the gameplay beatmap + + AddStep("selection", () => + { + MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + }).ToArray(); + + MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = MatchmakingStage.ServerBeatmapFinalised, + CandidateItems = beatmaps.Select(b => b.ID).ToArray(), + CandidateItem = beatmaps[0].ID + }).WaitSafely(); + }); + + // Prepare gameplay. + AddWaitStep("wait", 25); + + AddStep("prepare gameplay", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = MatchmakingStage.GameplayWarmupTime + }).WaitSafely()); + + // Start gameplay. + AddWaitStep("wait", 5); + + AddStep("gameplay", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = MatchmakingStage.Gameplay + }).WaitSafely()); + + AddStep("start gameplay", () => MultiplayerClient.StartMatch().WaitSafely()); + // AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); + + // Finish gameplay. + AddWaitStep("wait", 5); + + AddStep("round end", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = MatchmakingStage.ResultsDisplaying + }).WaitSafely()); + + AddWaitStep("wait", 10); + + AddStep("room end", () => + { + MatchmakingRoomState state = new MatchmakingRoomState + { + CurrentRound = 1, + Stage = MatchmakingStage.Ended + }; + + int localUserId = API.LocalUser.Value.OnlineID; + + state.Users[localUserId].Placement = 1; + state.Users[localUserId].Rounds[1].Placement = 1; + state.Users[localUserId].Rounds[1].TotalScore = 1; + state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1; + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + } + + private void setupRequestHandler() + { + AddStep("setup request handler", () => + { + Func? defaultRequestHandler = null; + + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapsRequest getBeatmaps: + getBeatmaps.TriggerSuccess(new GetBeatmapsResponse + { + Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap + { + OnlineID = id, + StarRating = id, + DifficultyName = $"Beatmap {id}", + BeatmapSet = new APIBeatmapSet + { + Title = $"Title {id}", + Artist = $"Artist {id}", + AuthorString = $"Author {id}" + } + }).ToList() + }); + return true; + + case IndexPlaylistScoresRequest index: + var result = new IndexedMultiplayerScores(); + + for (int i = 0; i < 8; ++i) + { + result.Scores.Add(new MultiplayerScore + { + ID = i, + Accuracy = 1 - (float)i / 16, + Position = i + 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH), + MaxCombo = 1000 - i, + TotalScore = (long)(1_000_000 * (1 - (float)i / 16)), + User = new APIUser { Username = $"user {i}" }, + Statistics = new Dictionary() + }); + } + + index.TriggerSuccess(result); + return true; + + default: + return defaultRequestHandler?.Invoke(request) ?? false; + } + }; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs new file mode 100644 index 0000000000..be3d7463d6 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs @@ -0,0 +1,119 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneMatchmakingScreenStack : MultiplayerTestScene + { + private const int user_count = 8; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => + { + var room = CreateDefaultRoom(); + room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + })).ToArray(); + + JoinRoom(room); + }); + + WaitForJoined(); + + AddStep("add carousel", () => + { + Child = new MatchmakingScreenStack + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }; + }); + + AddStep("join users", () => + { + var users = Enumerable.Range(1, user_count).Select(i => new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"Player {i}" + } + }).ToArray(); + + foreach (var user in users) + MultiplayerClient.AddUser(user); + }); + } + + [Test] + public void TestStatus() + { + AddWaitStep("wait for scroll", 5); + AddStep("pick", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = MatchmakingStage.UserBeatmapSelect + }).WaitSafely()); + + AddWaitStep("wait for scroll", 5); + AddStep("selection", () => + { + MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + }).ToArray(); + + beatmaps = Random.Shared.GetItems(beatmaps, 8); + + MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = MatchmakingStage.ServerBeatmapFinalised, + CandidateItems = beatmaps.Select(b => b.ID).ToArray(), + CandidateItem = beatmaps[0].ID + }).WaitSafely(); + }); + + AddWaitStep("wait for scroll", 35); + AddStep("room end", () => + { + var state = new MatchmakingRoomState + { + CurrentRound = 1, + Stage = MatchmakingStage.Ended + }; + + int localUserId = API.LocalUser.Value.OnlineID; + + state.Users[localUserId].Placement = 1; + state.Users[localUserId].Rounds[1].Placement = 1; + state.Users[localUserId].Rounds[1].TotalScore = 1; + state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1; + + state.Users[1].Placement = 2; + state.Users[1].Rounds[1].Placement = 2; + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs new file mode 100644 index 0000000000..fdb5aed789 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -0,0 +1,106 @@ +// 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 NUnit.Framework; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestScenePickScreen : MultiplayerTestScene + { + private readonly IReadOnlyList users = new[] + { + new APIUser + { + Id = 2, + Username = "peppy", + }, + new APIUser + { + Id = 1040328, + Username = "smoogipoo", + }, + new APIUser + { + Id = 6573093, + Username = "OliBomby", + }, + new APIUser + { + Id = 7782553, + Username = "aesth", + }, + new APIUser + { + Id = 6411631, + Username = "Maarvin", + } + }; + + private readonly PlaylistItem[] items = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + })).ToArray(); + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => + { + var room = CreateDefaultRoom(); + room.Playlist = items; + + JoinRoom(room); + }); + + WaitForJoined(); + + AddStep("add users", () => + { + foreach (var user in users) + MultiplayerClient.AddUser(user); + }); + } + + [Test] + public void TestScreen() + { + var selectedItems = new List(); + + PickScreen screen = null!; + + AddStep("add screen", () => LoadScreen(screen = new PickScreen())); + + AddStep("select maps", () => + { + selectedItems.Clear(); + + foreach (var user in users) + { + var item = items[Random.Shared.Next(items.Length)]; + selectedItems.Add(item.ID); + + MultiplayerClient.MatchmakingToggleUserSelection(user.Id, item.ID).FireAndForget(); + } + }); + + AddStep("show final map", () => + { + long[] candidateItems = selectedItems.ToArray(); + long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)]; + + screen.RollFinalBeatmap(candidateItems, finalItem); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs new file mode 100644 index 0000000000..dafb2d9f03 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -0,0 +1,94 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestScenePlayerPanel : MultiplayerTestScene + { + private PlayerPanel panel = null!; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1) + { + User = new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } + } + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + [Test] + public void TestIncreasePlacement() + { + int rank = 0; + + AddStep("increase placement", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Users = + { + UserDictionary = + { + { + 2, new MatchmakingUser + { + UserId = 2, + Placement = ++rank + } + } + } + } + }).WaitSafely()); + + AddToggleStep("toggle horizontal", h => panel.Horizontal = h); + } + + [Test] + public void TestIncreasePoints() + { + int points = 0; + + AddStep("increase points", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Users = + { + UserDictionary = + { + { + 1, new MatchmakingUser + { + UserId = 1, + Placement = 1, + Points = ++points + } + } + } + } + }).WaitSafely()); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs new file mode 100644 index 0000000000..5fd5b1c906 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -0,0 +1,98 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; +using osu.Game.Tests.Visual.Multiplayer; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneResultsScreen : MultiplayerTestScene + { + private const int invalid_user_id = 1; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add results screen", () => + { + Child = new ScreenStack(new ResultsScreen()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.8f) + }; + }); + + AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(invalid_user_id) + { + User = new APIUser + { + Id = invalid_user_id, + Username = "Invalid user" + } + })); + } + + [Test] + public void TestResults() + { + AddStep("set results stage", () => + { + var state = new MatchmakingRoomState + { + CurrentRound = 6, + Stage = MatchmakingStage.Ended + }; + + int localUserId = API.LocalUser.Value.OnlineID; + + // Overall state. + state.Users[localUserId].Placement = 1; + state.Users[localUserId].Points = 8; + state.Users[invalid_user_id].Placement = 2; + state.Users[invalid_user_id].Points = 7; + for (int round = 1; round <= state.CurrentRound; round++) + state.Users[localUserId].Rounds[round].Placement = round; + + // Highest score. + state.Users[localUserId].Rounds[1].TotalScore = 1000; + state.Users[invalid_user_id].Rounds[1].TotalScore = 990; + + // Highest accuracy. + state.Users[localUserId].Rounds[2].Accuracy = 0.9995; + state.Users[invalid_user_id].Rounds[2].Accuracy = 0.5; + + // Highest combo. + state.Users[localUserId].Rounds[3].MaxCombo = 100; + state.Users[invalid_user_id].Rounds[3].MaxCombo = 10; + + // Most bonus score. + state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50; + state.Users[invalid_user_id].Rounds[4].Statistics[HitResult.LargeBonus] = 25; + + // Smallest score difference. + state.Users[localUserId].Rounds[5].TotalScore = 1000; + state.Users[invalid_user_id].Rounds[5].TotalScore = 999; + + // Largest score difference. + state.Users[localUserId].Rounds[6].TotalScore = 1000; + state.Users[invalid_user_id].Rounds[6].TotalScore = 0; + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs new file mode 100644 index 0000000000..b5d69485cf --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs @@ -0,0 +1,32 @@ +// 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.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneRoomStatisticPanel : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("add statistic", () => Child = new RoomStatisticPanel("Statistic description", new MultiplayerRoomUser(1) + { + User = new APIUser + { + Id = 1, + Username = "peppy" + } + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs new file mode 100644 index 0000000000..e19d228c85 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs @@ -0,0 +1,102 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Framework.Utils; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults; +using osu.Game.Tests.Visual.Multiplayer; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneRoundResultsScreen : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + setupRequestHandler(); + + AddStep("load screen", () => + { + Child = new ScreenStack(new RoundResultsScreen()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.8f) + }; + }); + } + + private void setupRequestHandler() + { + AddStep("setup request handler", () => + { + Func? defaultRequestHandler = null; + + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapsRequest getBeatmaps: + getBeatmaps.TriggerSuccess(new GetBeatmapsResponse + { + Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap + { + OnlineID = id, + StarRating = id, + DifficultyName = $"Beatmap {id}", + BeatmapSet = new APIBeatmapSet + { + Title = $"Title {id}", + Artist = $"Artist {id}", + AuthorString = $"Author {id}" + } + }).ToList() + }); + return true; + + case IndexPlaylistScoresRequest index: + var result = new IndexedMultiplayerScores(); + + for (int i = 0; i < 8; ++i) + { + result.Scores.Add(new MultiplayerScore + { + ID = i, + Accuracy = 1 - (float)i / 16, + Position = i + 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH), + MaxCombo = 1000 - i, + TotalScore = (long)(1_000_000 * (1 - (float)i / 16)), + User = new APIUser { Username = $"user {i}" }, + Statistics = new Dictionary() + }); + } + + index.TriggerSuccess(result); + return true; + + default: + return defaultRequestHandler?.Invoke(request) ?? false; + } + }; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs new file mode 100644 index 0000000000..6349f01f28 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.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 System; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Game.Online.Matchmaking; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneStageBubble : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add bubble", () => Child = new StageBubble(MatchmakingStage.RoundWarmupTime, "Next Round") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 100 + }); + } + + [Test] + public void TestStartStopCountdown() + { + MultiplayerCountdown countdown = null!; + + AddStep("start countdown", () => MultiplayerClient.StartCountdown(countdown = new MatchmakingStageCountdown + { + Stage = MatchmakingStage.RoundWarmupTime, + TimeRemaining = TimeSpan.FromSeconds(5) + }).WaitSafely()); + + AddWaitStep("wait a bit", 10); + + AddStep("stop countdown", () => MultiplayerClient.StopCountdown(countdown).WaitSafely()); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs new file mode 100644 index 0000000000..49680acd64 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs @@ -0,0 +1,56 @@ +// 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 NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Game.Online.Matchmaking; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneStageDisplay : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add bubble", () => Child = new StageDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Width = 0.5f, + }); + } + + [Test] + public void TestStartCountdown() + { + foreach (var status in Enum.GetValues()) + { + AddStep($"{status}", () => + { + MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + { + Stage = status + }).WaitSafely(); + + MultiplayerClient.StartCountdown(new MatchmakingStageCountdown + { + Stage = status, + TimeRemaining = TimeSpan.FromSeconds(5) + }).WaitSafely(); + }); + + AddWaitStep("wait a bit", 10); + } + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs new file mode 100644 index 0000000000..0094c7645a --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs @@ -0,0 +1,43 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneStageText : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("create display", () => Child = new StageText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + [TestCase(MatchmakingStage.WaitingForClientsJoin)] + [TestCase(MatchmakingStage.RoundWarmupTime)] + [TestCase(MatchmakingStage.UserBeatmapSelect)] + [TestCase(MatchmakingStage.ServerBeatmapFinalised)] + [TestCase(MatchmakingStage.WaitingForClientsBeatmapDownload)] + [TestCase(MatchmakingStage.GameplayWarmupTime)] + [TestCase(MatchmakingStage.Gameplay)] + [TestCase(MatchmakingStage.ResultsDisplaying)] + [TestCase(MatchmakingStage.Ended)] + public void TestStatus(MatchmakingStage status) + { + AddStep("set status", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState { Stage = status }).WaitSafely()); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index c091c089cf..793bc3cd66 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -13,8 +13,8 @@ using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.Menu; +using osuTK.Graphics; using osuTK.Input; -using Color4 = osuTK.Graphics.Color4; namespace osu.Game.Tests.Visual.UserInterface { @@ -177,5 +177,28 @@ namespace osu.Game.Tests.Visual.UserInterface })); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); } + + [Test] + public void TestMatchmaking() + { + AddStep("add content", () => + { + Children = new Drawable[] + { + new DependencyProvidingContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Child = new MatchmakingButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ButtonSystemState = ButtonSystemState.TopLevel, + }, + }, + }; + }); + } } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index adb9b92614..aaf9f6e863 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using osu.Game.Online.API; +using osu.Game.Online.Matchmaking; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -13,7 +14,7 @@ namespace osu.Game.Online.Multiplayer /// /// An interface defining a multiplayer client instance. /// - public interface IMultiplayerClient : IStatefulUserHubClient + public interface IMultiplayerClient : IStatefulUserHubClient, IMatchmakingClient { /// /// Signals that the room has changed state. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 745e773512..1946863988 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -17,6 +17,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Rooms; using osu.Game.Overlays.Notifications; @@ -26,7 +27,7 @@ using osu.Game.Utils; namespace osu.Game.Online.Multiplayer { - public abstract partial class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer + public abstract partial class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer, IMatchmakingServer { public Action? PostNotification { protected get; set; } @@ -112,6 +113,22 @@ namespace osu.Game.Online.Multiplayer /// public event Action? Disconnecting; + public event Action? CountdownStarted; + + public event Action? CountdownStopped; + + public event Action? UserStateChanged; + + public event Action? MatchmakingQueueJoined; + public event Action? MatchmakingQueueLeft; + public event Action? MatchmakingRoomInvited; + public event Action? MatchmakingRoomReady; + public event Action? MatchmakingLobbyStatusChanged; + public event Action? MatchmakingQueueStatusChanged; + public event Action? MatchmakingItemSelected; + public event Action? MatchmakingItemDeselected; + public event Action? MatchRoomStateChanged; + /// /// Whether the is currently connected. /// This is NOT thread safe and usage should be scheduled. @@ -179,9 +196,13 @@ namespace osu.Game.Online.Multiplayer { IsConnected.BindValueChanged(connected => Scheduler.Add(() => { - // clean up local room state on server disconnect. - if (!connected.NewValue && Room != null) - LeaveRoom(); + if (!connected.NewValue) + { + if (Room != null) + LeaveRoom(); + + MatchmakingQueueLeft?.Invoke(); + } })); } @@ -254,6 +275,9 @@ namespace osu.Game.Online.Multiplayer Room = joinedRoom; APIRoom = apiRoom; + while (pendingRequests.TryDequeue(out Action? action)) + action(); + APIRoom.RoomID = joinedRoom.RoomID; APIRoom.ChannelId = joinedRoom.ChannelID; APIRoom.Host = joinedRoom.Host?.User; @@ -640,6 +664,7 @@ namespace osu.Game.Online.Multiplayer user.State = state; updateUserPlayingState(userId, state); + UserStateChanged?.Invoke(user, state); RoomUpdated?.Invoke(); }); @@ -672,6 +697,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(Room != null); Room.MatchState = state; + MatchRoomStateChanged?.Invoke(state); RoomUpdated?.Invoke(); }); @@ -688,6 +714,7 @@ namespace osu.Game.Online.Multiplayer { case CountdownStartedEvent countdownStartedEvent: Room.ActiveCountdowns.Add(countdownStartedEvent.Countdown); + CountdownStarted?.Invoke(countdownStartedEvent.Countdown); switch (countdownStartedEvent.Countdown) { @@ -700,8 +727,13 @@ namespace osu.Game.Online.Multiplayer case CountdownStoppedEvent countdownStoppedEvent: MultiplayerCountdown? countdown = Room.ActiveCountdowns.FirstOrDefault(countdown => countdown.ID == countdownStoppedEvent.ID); + if (countdown != null) + { Room.ActiveCountdowns.Remove(countdown); + CountdownStopped?.Invoke(countdown); + } + break; } @@ -1001,6 +1033,80 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + Task IMatchmakingClient.MatchmakingQueueJoined() + { + Scheduler.Add(() => MatchmakingQueueJoined?.Invoke()); + return Task.CompletedTask; + } + + Task IMatchmakingClient.MatchmakingQueueLeft() + { + Scheduler.Add(() => MatchmakingQueueLeft?.Invoke()); + return Task.CompletedTask; + } + + Task IMatchmakingClient.MatchmakingRoomInvited() + { + Scheduler.Add(() => MatchmakingRoomInvited?.Invoke()); + return Task.CompletedTask; + } + + Task IMatchmakingClient.MatchmakingRoomReady(long roomId) + { + Scheduler.Add(() => MatchmakingRoomReady?.Invoke(roomId)); + return Task.CompletedTask; + } + + Task IMatchmakingClient.MatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status) + { + Scheduler.Add(() => MatchmakingLobbyStatusChanged?.Invoke(status)); + return Task.CompletedTask; + } + + Task IMatchmakingClient.MatchmakingQueueStatusChanged(MatchmakingQueueStatus status) + { + Scheduler.Add(() => MatchmakingQueueStatusChanged?.Invoke(status)); + return Task.CompletedTask; + } + + Task IMatchmakingClient.MatchmakingItemSelected(int userId, long playlistItemId) + { + Scheduler.Add(() => + { + MatchmakingItemSelected?.Invoke(userId, playlistItemId); + RoomUpdated?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMatchmakingClient.MatchmakingItemDeselected(int userId, long playlistItemId) + { + Scheduler.Add(() => + { + MatchmakingItemDeselected?.Invoke(userId, playlistItemId); + RoomUpdated?.Invoke(); + }); + + return Task.CompletedTask; + } + + public abstract Task MatchmakingJoinLobby(); + + public abstract Task MatchmakingLeaveLobby(); + + public abstract Task MatchmakingJoinQueue(MatchmakingSettings settings); + + public abstract Task MatchmakingLeaveQueue(); + + public abstract Task MatchmakingAcceptInvitation(); + + public abstract Task MatchmakingDeclineInvitation(); + + public abstract Task MatchmakingToggleSelection(long playlistItemId); + + public abstract Task MatchmakingSkipToNextStage(); + private partial class MultiplayerInvitationNotification : UserAvatarNotification { protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 02e9cd4ee8..83ff06d095 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -14,6 +14,7 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays.Notifications; using osu.Game.Localisation; +using osu.Game.Online.Matchmaking; namespace osu.Game.Online.Multiplayer { @@ -70,6 +71,15 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved); connection.On(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested); + + connection.On(nameof(IMultiplayerClient.MatchmakingQueueJoined), ((IMultiplayerClient)this).MatchmakingQueueJoined); + connection.On(nameof(IMultiplayerClient.MatchmakingQueueLeft), ((IMultiplayerClient)this).MatchmakingQueueLeft); + connection.On(nameof(IMultiplayerClient.MatchmakingRoomInvited), ((IMultiplayerClient)this).MatchmakingRoomInvited); + connection.On(nameof(IMultiplayerClient.MatchmakingRoomReady), ((IMultiplayerClient)this).MatchmakingRoomReady); + connection.On(nameof(IMultiplayerClient.MatchmakingLobbyStatusChanged), ((IMultiplayerClient)this).MatchmakingLobbyStatusChanged); + connection.On(nameof(IMultiplayerClient.MatchmakingQueueStatusChanged), ((IMultiplayerClient)this).MatchmakingQueueStatusChanged); + connection.On(nameof(IMultiplayerClient.MatchmakingItemSelected), ((IMultiplayerClient)this).MatchmakingItemSelected); + connection.On(nameof(IMultiplayerClient.MatchmakingItemDeselected), ((IMultiplayerClient)this).MatchmakingItemDeselected); }; IsConnected.BindTo(connector.IsConnected); @@ -310,6 +320,78 @@ namespace osu.Game.Online.Multiplayer return connector.Disconnect(); } + public override Task MatchmakingJoinLobby() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinLobby)); + } + + public override Task MatchmakingLeaveLobby() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveLobby)); + } + + public override Task MatchmakingJoinQueue(MatchmakingSettings settings) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueue), settings); + } + + public override Task MatchmakingLeaveQueue() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveQueue)); + } + + public override Task MatchmakingAcceptInvitation() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingAcceptInvitation)); + } + + public override Task MatchmakingDeclineInvitation() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingDeclineInvitation)); + } + + public override Task MatchmakingToggleSelection(long playlistItemId) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingToggleSelection), playlistItemId); + } + + public override Task MatchmakingSkipToNextStage() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingSkipToNextStage)); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index bf08023242..d610bd64d5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -65,6 +65,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; @@ -1270,6 +1271,7 @@ namespace osu.Game loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(detachedBeatmapStore = new RealmDetachedBeatmapStore(), Add, true); + loadComponentSingleFile(new MatchmakingController(), Add, true); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 073a0d4021..48d745562c 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -46,6 +46,7 @@ namespace osu.Game.Screens.Menu public Action? OnSolo; public Action? OnSettings; public Action? OnMultiplayer; + public Action? OnMatchmaking; public Action? OnPlaylists; public Action? OnDailyChallenge; @@ -138,23 +139,27 @@ namespace osu.Game.Screens.Menu Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M)); + buttonsPlay.Add(new MatchmakingButton(@"button-default-select", new Color4(94, 63, 186, 255), onMatchmaking, Key.N)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L)); buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, Key.E) + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, + Key.E) { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), (_, _) => OnEditSkin?.Invoke(), Key.S)); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M, Key.L) + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M, + Key.L) { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), (_, _) => State = ButtonSystemState.Edit, Key.E)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B, Key.D)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B, + Key.D)); if (host.CanExit) buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q)); @@ -191,6 +196,17 @@ namespace osu.Game.Screens.Menu OnMultiplayer?.Invoke(); } + private void onMatchmaking(MainMenuButton mainMenuButton, UIEvent uiEvent) + { + if (api.State.Value != APIState.Online) + { + loginOverlay?.Show(); + return; + } + + OnMatchmaking?.Invoke(); + } + private void onPlaylists(MainMenuButton mainMenuButton, UIEvent uiEvent) { if (api.State.Value != APIState.Online) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index bc3bcbd800..c74b60c5d7 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -37,6 +37,7 @@ using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.SelectV2; @@ -159,6 +160,7 @@ namespace osu.Game.Screens.Menu }, OnSolo = loadSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), + OnMatchmaking = joinOrLeaveMatchmakingQueue, OnPlaylists = () => this.Push(new Playlists()), OnDailyChallenge = room => { @@ -481,6 +483,8 @@ namespace osu.Game.Screens.Menu private void loadSongSelect() => this.Push(new SoloSongSelect()); + private void joinOrLeaveMatchmakingQueue() => this.Push(new MatchmakingIntroScreen()); + private partial class MobileDisclaimerDialog : PopupDialog { public MobileDisclaimerDialog(Action confirmed) diff --git a/osu.Game/Screens/Menu/MatchmakingButton.cs b/osu.Game/Screens/Menu/MatchmakingButton.cs new file mode 100644 index 0000000000..b65f08fe03 --- /dev/null +++ b/osu.Game/Screens/Menu/MatchmakingButton.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. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Screens.Menu +{ + public partial class MatchmakingButton : MainMenuButton + { + public MatchmakingButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) + : base("matchmaking", sampleName, FontAwesome.Solid.Newspaper, colour, clickAction, triggerKeys) + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs new file mode 100644 index 0000000000..e3d314844f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs @@ -0,0 +1,68 @@ +// 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.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class MatchmakingAvatar : CompositeDrawable + { + public static readonly Vector2 SIZE = new Vector2(30); + + private readonly APIUser user; + private readonly bool isOwnUser; + + public MatchmakingAvatar(APIUser user, bool isOwnUser = false) + { + this.user = user; + this.isOwnUser = isOwnUser; + + Size = SIZE; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + if (isOwnUser) + { + AddInternal(new Container + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Padding = new MarginPadding(-2), + Child = new FastCircle + { + RelativeSizeAxes = Axes.Both, + Colour = colour.Yellow, + } + }); + } + + AddInternal(new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.LightSlateGray, + }, + new ClickableAvatar(user, true) + { + RelativeSizeAxes = Axes.Both, + } + } + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs new file mode 100644 index 0000000000..5a738f05d4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs @@ -0,0 +1,117 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Ranking; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class MatchmakingCloud : CompositeDrawable + { + private APIUser[] users = []; + private Container usersContainer = null!; + + public APIUser[] Users + { + get => users; + set + { + users = value; + + foreach (var u in usersContainer) + u.Delay(RNG.Next(0, 1000)).FadeOut(500).Expire(); + + LoadComponentsAsync(users.Select(u => new MovingAvatar(u)), avatars => + { + if (usersContainer.Count == 0) + { + usersContainer.ScaleTo(0) + .ScaleTo(1, 5000, Easing.OutPow10); + } + + usersContainer.AddRange(avatars); + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + InternalChildren = new Drawable[] + { + usersContainer = new AspectContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + }, + }; + } + + public partial class MovingAvatar : MatchmakingAvatar + { + private float angle; + private float angularSpeed; + + private float targetSpeed; + private float targetScale; + private float targetAlpha; + + public MovingAvatar(APIUser apiUser) + : base(apiUser) + { + RelativePositionAxes = Axes.Both; + Scale = new Vector2(2); + + Origin = Anchor.Centre; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateParams(); + + angle = RNG.NextSingle(0f, MathF.Tau); + + angularSpeed = targetSpeed; + Scale = new Vector2(targetScale); + + Hide(); + this.Delay(RNG.Next(0, 1000)).FadeTo(targetAlpha, 2000, Easing.OutQuint); + } + + private void updateParams() + { + targetSpeed = RNG.NextSingle(0.05f, 0.5f); + targetScale = RNG.NextSingle(0.2f, 3f); + targetAlpha = RNG.NextSingle(0.5f, 1f); + + Scheduler.AddDelayed(updateParams, RNG.Next(500, 5000)); + } + + protected override void Update() + { + base.Update(); + + float elapsed = (float)Math.Min(20, Time.Elapsed) / 1000; + + Scale = new Vector2((float)Interpolation.Lerp(Scale.X, targetScale, elapsed / 100)); + Alpha = (float)Interpolation.Lerp(Alpha, targetAlpha, elapsed / 100); + angularSpeed = (float)Interpolation.Lerp(angularSpeed, targetSpeed, elapsed / 100); + + angle += angularSpeed * elapsed * 0.5f; + + Position = new Vector2(0.5f) + + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * angularSpeed; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs new file mode 100644 index 0000000000..dde7adfc13 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs @@ -0,0 +1,170 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class MatchmakingController : Component + { + public readonly Bindable CurrentState = new Bindable(); + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private INotificationOverlay? notifications { get; set; } + + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + private ProgressNotification? backgroundNotification; + private Notification? readyNotification; + private bool isBackgrounded; + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.RoomUpdated += onRoomUpdated; + client.MatchmakingQueueJoined += onMatchmakingQueueJoined; + client.MatchmakingQueueLeft += onMatchmakingQueueLeft; + client.MatchmakingRoomInvited += onMatchmakingRoomInvited; + client.MatchmakingRoomReady += onMatchmakingRoomReady; + + ruleset.BindValueChanged(_ => client.MatchmakingLeaveQueue().FireAndForget()); + } + + public void SearchInBackground() + { + if (isBackgrounded) + return; + + isBackgrounded = true; + postNotification(); + } + + public void SearchInForeground() + { + if (!isBackgrounded) + return; + + isBackgrounded = false; + closeNotifications(); + } + + private void onRoomUpdated() => Scheduler.Add(() => + { + if (client.Room == null) + CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Idle; + }); + + private void onMatchmakingQueueJoined() => Scheduler.Add(() => + { + CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Queueing; + + if (isBackgrounded) + { + closeNotifications(); + postNotification(); + } + }); + + private void onMatchmakingQueueLeft() => Scheduler.Add(() => + { + if (CurrentState.Value != MatchmakingQueueScreen.MatchmakingScreenState.InRoom) + CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Idle; + + closeNotifications(); + }); + + private void onMatchmakingRoomInvited() => Scheduler.Add(() => + { + CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.PendingAccept; + + if (backgroundNotification != null) + { + backgroundNotification.State = ProgressNotificationState.Completed; + backgroundNotification = null; + } + }); + + private void onMatchmakingRoomReady(long roomId) => Scheduler.Add(() => + { + client.JoinRoom(new Room { RoomID = roomId }) + .FireAndForget(() => Scheduler.Add(() => + { + CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.InRoom; + })); + }); + + private void postNotification() + { + if (backgroundNotification != null) + return; + + notifications?.Post(backgroundNotification = new ProgressNotification + { + Text = "Searching for opponents...", + CompletionTarget = n => notifications.Post(readyNotification = n), + CompletionText = "Your match is ready! Click to join.", + CompletionClickAction = () => + { + client.MatchmakingAcceptInvitation().FireAndForget(); + performer?.PerformFromScreen(s => s.Push(new MatchmakingIntroScreen())); + + closeNotifications(); + return true; + }, + CancelRequested = () => + { + client.MatchmakingLeaveQueue().FireAndForget(); + + closeNotifications(); + return true; + } + }); + } + + private void closeNotifications() + { + if (backgroundNotification != null) + { + backgroundNotification.State = ProgressNotificationState.Cancelled; + backgroundNotification.Close(false); + } + + readyNotification?.Close(false); + + backgroundNotification = null; + readyNotification = null; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.RoomUpdated -= onRoomUpdated; + client.MatchmakingQueueJoined -= onMatchmakingQueueJoined; + client.MatchmakingQueueLeft -= onMatchmakingQueueLeft; + client.MatchmakingRoomInvited -= onMatchmakingRoomInvited; + client.MatchmakingRoomReady -= onMatchmakingRoomReady; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs new file mode 100644 index 0000000000..af19aa1252 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs @@ -0,0 +1,31 @@ +// 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.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class MatchmakingPlayer : MultiplayerPlayer + { + public MatchmakingPlayer(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users) + : base(room, playlistItem, users) + { + } + + protected override async Task PrepareScoreForResultsAsync(Score score) + { + await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); + + Scheduler.Add(() => + { + if (this.IsCurrentScreen()) + this.Exit(); + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs new file mode 100644 index 0000000000..af77306113 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs @@ -0,0 +1,342 @@ +// 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 System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osu.Game.Rulesets; +using osu.Game.Screens.Footer; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class MatchmakingScreen : OsuScreen + { + /// + /// Padding between rows of the content. + /// + private const float row_padding = 10; + + public override bool? ApplyModTrackAdjustments => true; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + public override bool ShowFooter => true; + + [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))] + private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker(); + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private IDialogOverlay dialogOverlay { get; set; } = null!; + + private readonly MultiplayerRoom room; + + private CancellationTokenSource? downloadCheckCancellation; + private int? lastDownloadCheckedBeatmapId; + + public MatchmakingScreen(MultiplayerRoom room) + { + this.room = room; + + Activity.Value = new UserActivity.InLobby(room); + Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + beatmapAvailabilityTracker, + new MultiplayerRoomSounds(), + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = ScreenFooter.HEIGHT + 20 + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, row_padding), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new Drawable[]?[] + { + [ + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + }, + new MatchmakingScreenStack(), + } + } + ], + null, + [ + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = 100, + Padding = new MarginPadding + { + Horizontal = 200, + }, + Child = new MatchChatDisplay(new Room(room)) + { + RelativeSizeAxes = Axes.Both, + } + }, + new RoundedButton + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Text = "Don't click me", + Size = new Vector2(100, 30), + Action = () => client.MatchmakingSkipToNextStage() + } + } + } + ] + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.RoomUpdated += onRoomUpdated; + client.UserStateChanged += onUserStateChanged; + client.SettingsChanged += onSettingsChanged; + client.LoadRequested += onLoadRequested; + + beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); + } + + private void onRoomUpdated() + { + if (this.IsCurrentScreen() && client.Room == null) + { + Logger.Log($"{this} exiting due to loss of room or connection"); + this.Exit(); + } + } + + private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) + { + if (user.Equals(client.LocalUser) && state == MultiplayerUserState.Idle) + this.MakeCurrent(); + } + + private void onSettingsChanged(MultiplayerRoomSettings _) => Scheduler.Add(() => + { + checkForAutomaticDownload(); + updateGameplayState(); + }); + + private void onBeatmapAvailabilityChanged(ValueChangedEvent e) => Scheduler.Add(() => + { + if (client.Room == null || client.LocalUser == null) + return; + + client.ChangeBeatmapAvailability(e.NewValue).FireAndForget(); + + switch (e.NewValue.State) + { + case DownloadState.NotDownloaded: + case DownloadState.LocallyAvailable: + updateGameplayState(); + break; + } + }); + + private void updateGameplayState() + { + if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState) + return; + + if (matchmakingState.Stage != MatchmakingStage.WaitingForClientsBeatmapDownload) + return; + + MultiplayerPlaylistItem item = client.Room!.CurrentPlaylistItem; + RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!; + Ruleset rulesetInstance = ruleset.CreateInstance(); + + // Update global gameplay state to correspond to the new selection. + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + Ruleset.Value = ruleset; + Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + if (Beatmap.Value is DummyWorkingBeatmap) + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); + else + client.ChangeState(MultiplayerUserState.Ready).FireAndForget(); + } + + private void onLoadRequested() => Scheduler.Add(() => + { + updateGameplayState(); + this.Push(new MultiplayerPlayerLoader(() => new MatchmakingPlayer(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray()))); + }); + + private void checkForAutomaticDownload() + { + if (client.Room == null) + return; + + MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; + + // This method is called every time anything changes in the room. + // This could result in download requests firing far too often, when we only expect them to fire once per beatmap. + // + // Without this check, we would see especially egregious behaviour when a user has hit the download rate limit. + if (lastDownloadCheckedBeatmapId == item.BeatmapID) + return; + + lastDownloadCheckedBeatmapId = item.BeatmapID; + + downloadCheckCancellation?.Cancel(); + + // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. + // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. + beatmapLookupCache + .GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token) + .ContinueWith(resolved => Schedule(() => + { + var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; + + if (beatmapSet == null) + return; + + if (beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID })) + return; + + beatmapDownloader.Download(beatmapSet); + })); + } + + private bool exitConfirmed; + + public override bool OnExiting(ScreenExitEvent e) + { + if (base.OnExiting(e)) + return true; + + if (exitConfirmed) + { + client.LeaveRoom().FireAndForget(); + return false; + } + + if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog) + confirmDialog.PerformOkAction(); + else + { + dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => + { + exitConfirmed = true; + if (this.IsCurrentScreen()) + this.Exit(); + })); + } + + return true; + } + + public override void OnResuming(ScreenTransitionEvent e) + { + base.OnResuming(e); + + if (e.Last is not MultiplayerPlayerLoader playerLoader) + return; + + if (!playerLoader.GameplayPassed) + { + client.AbortGameplay().FireAndForget(); + return; + } + + client.ChangeState(MultiplayerUserState.Idle); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.RoomUpdated -= onRoomUpdated; + client.UserStateChanged -= onUserStateChanged; + client.SettingsChanged -= onSettingsChanged; + client.LoadRequested -= onLoadRequested; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs new file mode 100644 index 0000000000..e67e2a520a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Screens; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle +{ + public partial class IdleScreen : MatchmakingSubScreen + { + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new PlayerPanelList + { + RelativeSizeAxes = Axes.Both + }; + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + this.MoveToX(0); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs new file mode 100644 index 0000000000..eaddb0f2e4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs @@ -0,0 +1,197 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle +{ + public partial class PlayerPanel : UserPanel + { + public readonly MultiplayerRoomUser RoomUser; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private OsuSpriteText rankText = null!; + private OsuSpriteText scoreText = null!; + + private MatchmakingAvatar avatar = null!; + private OsuSpriteText username = null!; + + private Container mainContent = null!; + + public bool Horizontal + { + get => horizontal; + set + { + horizontal = value; + if (IsLoaded) + updateLayout(false); + } + } + + private bool horizontal; + + public PlayerPanel(MultiplayerRoomUser user) + : base(user.User!) + { + RoomUser = user; + } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + CornerRadius = 10; + + Add(mainContent = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.Centre, + Size = new Vector2(80), + }, + rankText = new OsuSpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomCentre, + Blending = BlendingParameters.Additive, + Margin = new MarginPadding(4), + Font = OsuFont.Style.Title.With(size: 70), + }, + username = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Text = User.Username, + Font = OsuFont.Style.Heading1, + }, + scoreText = new OsuSpriteText + { + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Style.Heading2, + Text = "0 pts" + } + } + }); + } + + protected override Drawable CreateLayout() => Empty(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateLayout(true); + + client.MatchRoomStateChanged += onRoomStateChanged; + onRoomStateChanged(client.Room!.MatchState); + + avatar.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + + rankText.Hide(); + scoreText.Hide(); + username.Hide(); + + using (BeginDelayedSequence(100)) + { + username.FadeInFromZero(600); + + using (BeginDelayedSequence(100)) + { + scoreText.FadeInFromZero(600); + + using (BeginDelayedSequence(100)) + { + rankText.FadeTo(0.6f, 600); + } + } + } + } + + private Vector2 avatarPosition => horizontal ? new Vector2(50) : new Vector2(75, 50); + + private void updateLayout(bool instant) + { + double duration = instant ? 0 : 1000; + + avatar.MoveTo(avatarPosition, duration, Easing.OutPow10); + this.ResizeTo(horizontal ? new Vector2(250, 100) : new Vector2(150, 200), duration, Easing.OutPow10); + + rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10); + username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10); + scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10); + } + + protected override bool OnHover(HoverEvent e) + { + this.ScaleTo(1.02f, 1000, Easing.OutQuint); + mainContent.ScaleTo(1.03f, 1000, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + this.ScaleTo(1f, 500, Easing.OutQuint); + mainContent.ScaleTo(1, 500, Easing.OutQuint); + + mainContent.MoveTo(Vector2.Zero, 500, Easing.OutElasticHalf); + avatar.MoveTo(avatarPosition, 1500, Easing.OutElastic); + base.OnHoverLost(e); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + var offset = (avatar.ToLocalSpace(e.ScreenSpaceMousePosition) - avatar.DrawSize / 2) * 0.02f; + + mainContent.MoveTo(offset * 0.5f, 1000, Easing.OutQuint); + avatar.MoveTo(avatarPosition + offset, 400, Easing.OutQuint); + return base.OnMouseMove(e); + } + + private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore)) + return; + + rankText.Text = $"#{userScore.Placement}"; + scoreText.Text = $"{userScore.Points} pts"; + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onRoomStateChanged; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs new file mode 100644 index 0000000000..003c35d8c4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs @@ -0,0 +1,80 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle +{ + public partial class PlayerPanelList : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + public bool Horizontal { get; init; } + + private FillFlowContainer panels = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = panels = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(20, 5), + LayoutEasing = Easing.InOutQuint, + LayoutDuration = 500 + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onRoomStateChanged; + client.UserJoined += onUserJoined; + client.UserLeft += onUserLeft; + + if (client.Room != null) + { + onRoomStateChanged(client.Room.MatchState); + foreach (var user in client.Room.Users) + onUserJoined(user); + } + } + + private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => + { + panels.Add(new PlayerPanel(user) + { + Horizontal = Horizontal, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + }); + + private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => + { + panels.Single(p => p.RoomUser.Equals(user)).Expire(); + }); + + private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + foreach (var panel in panels) + { + if (matchmakingState.Users.UserDictionary.TryGetValue(panel.User.Id, out MatchmakingUser? user)) + panels.SetLayoutPosition(panel, user.Placement); + else + panels.SetLayoutPosition(panel, float.MaxValue); + } + }); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs new file mode 100644 index 0000000000..9a23c963a9 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs @@ -0,0 +1,263 @@ +// 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; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.Match; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +{ + public partial class MatchmakingIntroScreen : OsuScreen + { + public override bool DisallowExternalBeatmapRulesetChanges => false; + + public override bool? ApplyModTrackAdjustments => true; + + public override bool ShowFooter => true; + + private Container introContent = null!; + + private Container titleContainer = null!; + + private bool animationBegan; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Resolved] + private MusicController musicController { get; set; } = null!; + + [Resolved] + private MatchmakingController controller { get; set; } = null!; + + public override bool AllowUserExit => !ValidForResume; + + private Sample? dateWindupSample; + private Sample? dateImpactSample; + private Sample? beatmapWindupSample; + private Sample? beatmapImpactSample; + + private SampleChannel? dateWindupChannel; + private SampleChannel? dateImpactChannel; + private SampleChannel? beatmapWindupChannel; + private SampleChannel? beatmapImpactChannel; + + private IDisposable? duckOperation; + + protected override BackgroundScreen CreateBackground() => new MatchmakingIntroBackgroundScreen(colourProvider); + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + InternalChildren = new Drawable[] + { + introContent = new Container + { + Alpha = 0f, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = OsuGame.SHEAR, + Children = new Drawable[] + { + titleContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + X = 10, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Matchmaking", + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + Shear = -OsuGame.SHEAR, + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + } + }, + } + }, + } + } + }; + + dateWindupSample = audio.Samples.Get(@"DailyChallenge/date-windup"); + dateImpactSample = audio.Samples.Get(@"DailyChallenge/date-impact"); + beatmapWindupSample = audio.Samples.Get(@"DailyChallenge/beatmap-windup"); + beatmapImpactSample = audio.Samples.Get(@"DailyChallenge/beatmap-impact"); + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + this.FadeInFromZero(400, Easing.OutQuint); + + updateAnimationState(); + playDateWindupSample(); + + controller.SearchInForeground(); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + ValidForResume = false; + + this.FadeOut(800, Easing.OutQuint); + base.OnSuspending(e); + } + + private void updateAnimationState() + { + if (animationBegan) + return; + + beginAnimation(); + animationBegan = true; + } + + private void beginAnimation() + { + using (BeginDelayedSequence(200)) + { + introContent.Show(); + + titleContainer + .ScaleTo(2) + .Then() + .ScaleTo(1, 400, Easing.In); + + using (BeginDelayedSequence(150)) + { + Schedule(() => + { + playDateImpactSample(); + playBeatmapWindupSample(); + + duckOperation?.Dispose(); + duckOperation = musicController.Duck(new DuckParameters + { + RestoreDuration = 1500f, + }); + }); + + using (BeginDelayedSequence(2750)) + { + Schedule(() => + { + duckOperation?.Dispose(); + }); + } + } + + using (BeginDelayedSequence(1000)) + { + using (BeginDelayedSequence(100)) + { + titleContainer + .ScaleTo(0.4f, 400, Easing.In) + .FadeOut(500, Easing.OutQuint); + } + + using (BeginDelayedSequence(240)) + { + Schedule(() => + { + if (this.IsCurrentScreen()) + this.Push(new MatchmakingQueueScreen()); + }); + } + } + } + } + + private void playDateWindupSample() + { + dateWindupChannel = dateWindupSample?.GetChannel(); + dateWindupChannel?.Play(); + } + + private void playDateImpactSample() + { + dateImpactChannel = dateImpactSample?.GetChannel(); + dateImpactChannel?.Play(); + } + + private void playBeatmapWindupSample() + { + beatmapWindupChannel = beatmapWindupSample?.GetChannel(); + beatmapWindupChannel?.Play(); + } + + private void playBeatmapImpactSample() + { + beatmapImpactChannel = beatmapImpactSample?.GetChannel(); + beatmapImpactChannel?.Play(); + } + + protected override void Dispose(bool isDisposing) + { + resetAudio(); + base.Dispose(isDisposing); + } + + private void resetAudio() + { + dateWindupChannel?.Stop(); + dateImpactChannel?.Stop(); + beatmapWindupChannel?.Stop(); + beatmapImpactChannel?.Stop(); + duckOperation?.Dispose(); + } + + private partial class MatchmakingIntroBackgroundScreen : RoomBackgroundScreen + { + private readonly OverlayColourProvider colourProvider; + + public MatchmakingIntroBackgroundScreen(OverlayColourProvider colourProvider) + : base(null) + { + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new Box + { + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5.Opacity(0.6f), + }); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs new file mode 100644 index 0000000000..e434ed240a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs @@ -0,0 +1,393 @@ +// 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 System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Matchmaking; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osu.Game.Rulesets; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +{ + public partial class MatchmakingQueueScreen : OsuScreen + { + public override bool ShowFooter => true; + + private Container mainContent = null!; + + private MatchmakingScreenState state; + private MatchmakingCloud cloud = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private IDialogOverlay dialogOverlay { get; set; } = null!; + + [Resolved] + private MatchmakingController controller { get; set; } = null!; + + [Resolved] + private UserLookupCache userLookupCache { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + private readonly IBindable currentState = new Bindable(); + private CancellationTokenSource userLookupCancellation = new CancellationTokenSource(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + InternalChildren = new Drawable[] + { + cloud = new MatchmakingCloud + { + Y = -100, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.6f) + }, + new MatchmakingAvatar(api.LocalUser.Value, true) + { + Y = -100, + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new Container + { + RelativePositionAxes = Axes.Y, + Y = 0.25f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + mainContent = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 300, + AutoSizeEasing = Easing.OutQuint, + Padding = new MarginPadding(20), + }, + } + }, + }; + + currentState.BindTo(controller.CurrentState); + currentState.BindValueChanged(s => SetState(s.NewValue)); + + client.MatchmakingLobbyStatusChanged += onMatchmakingLobbyStatusChanged; + } + + private void onMatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status) => Scheduler.Add(() => + { + userLookupCancellation.Cancel(); + var cancellation = userLookupCancellation = new CancellationTokenSource(); + + userLookupCache.GetUsersAsync(status.UsersInQueue, cancellation.Token) + .ContinueWith(result => Schedule(() => + { + APIUser?[] users = result.GetResultSafely(); + if (!cancellation.IsCancellationRequested) + Users = users.OfType().ToArray(); + }), cancellation.Token); + }); + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + client.MatchmakingJoinLobby().FireAndForget(); + + using (BeginDelayedSequence(800)) + Schedule(() => SetState(currentState.Value)); + } + + public override void OnResuming(ScreenTransitionEvent e) + { + base.OnResuming(e); + + client.MatchmakingJoinLobby().FireAndForget(); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + base.OnSuspending(e); + + client.MatchmakingLeaveLobby().FireAndForget(); + } + + private bool exitConfirmed; + private bool isBackgrounded; + + public override bool OnExiting(ScreenExitEvent e) + { + if (base.OnExiting(e)) + return true; + + client.MatchmakingLeaveLobby().FireAndForget(); + + if (isBackgrounded) + return false; + + if (exitConfirmed) + { + client.MatchmakingLeaveQueue().FireAndForget(); + return false; + } + + if (currentState.Value == MatchmakingScreenState.Idle) + return false; + + if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog) + confirmDialog.PerformOkAction(); + else + { + dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave the matchmaking queue?", () => + { + exitConfirmed = true; + if (this.IsCurrentScreen()) + this.Exit(); + })); + } + + return true; + } + + public APIUser[] Users + { + set => cloud.Users = value; + } + + public void SetState(MatchmakingScreenState newState) + { + state = newState; + + mainContent.FadeInFromZero(500, Easing.OutQuint); + mainContent.Clear(); + + switch (newState) + { + case MatchmakingScreenState.Idle: + mainContent.Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new ShearedButton(200) + { + DarkerColour = colours.Blue2, + LighterColour = colours.Blue1, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => client.MatchmakingJoinQueue(new MatchmakingSettings { RulesetId = ruleset.Value.OnlineID }).FireAndForget(), + Text = "Begin queueing", + } + } + }; + break; + + case MatchmakingScreenState.Queueing: + ShearedButton sendToBackgroundButton; + + mainContent.Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Waiting for a game...", + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + new LoadingSpinner + { + State = { Value = Visibility.Visible }, + }, + sendToBackgroundButton = new ShearedButton(200) + { + DarkerColour = colours.Orange3, + LighterColour = colours.Orange4, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Queue in background", + Action = () => + { + controller.SearchInBackground(); + isBackgrounded = true; + this.Exit(); + }, + Enabled = { Value = false }, + TooltipText = "Wait 5 seconds for this option to become available." + } + } + }; + + Scheduler.AddDelayed(() => + { + if (state != newState) + return; + + sendToBackgroundButton.Enabled.Value = true; + sendToBackgroundButton.TooltipText = "You will receive a notification when your game is ready. Make sure to watch out for it!"; + }, 5000); + break; + + case MatchmakingScreenState.PendingAccept: + mainContent.Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Found a match!", + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Regular, typeface: Typeface.TorusAlternate), + }, + new ShearedButton(200) + { + DarkerColour = colours.YellowDark, + LighterColour = colours.YellowLight, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => + { + client.MatchmakingAcceptInvitation().FireAndForget(); + SetState(MatchmakingScreenState.AcceptedWaitingForRoom); + }, + Text = "Join match!", + } + } + }; + break; + + case MatchmakingScreenState.AcceptedWaitingForRoom: + mainContent.Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Waiting for all players...", + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + new LoadingSpinner + { + State = { Value = Visibility.Visible }, + }, + } + }; + break; + + case MatchmakingScreenState.InRoom: + // room received, show users and transition to next screen. + mainContent.Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Good luck!", + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + } + }; + + using (BeginDelayedSequence(2000)) + Schedule(() => this.Push(new MatchmakingScreen(client.Room!))); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(newState), newState, null); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchmakingLobbyStatusChanged -= onMatchmakingLobbyStatusChanged; + } + + public enum MatchmakingScreenState + { + Idle, + Queueing, + PendingAccept, + AcceptedWaitingForRoom, + InRoom + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs new file mode 100644 index 0000000000..cba5c89385 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs @@ -0,0 +1,121 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +{ + public partial class MatchmakingScreenStack : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private ScreenStack screenStack = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + Padding = new MarginPadding(10); + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize) }, + Content = new Drawable[][] + { + [ + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.AutoSize) }, + Padding = new MarginPadding { Bottom = 20 }, + Content = new Drawable?[][] + { + [ + screenStack = new ScreenStack(), + null, + new PlayerPanelList + { + Horizontal = true, + RelativeSizeAxes = Axes.Y, + Width = 250, + Scale = new Vector2(0.8f), + } + ] + } + } + ], + [ + new StageDisplay + { + RelativeSizeAxes = Axes.X + } + ] + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + screenStack.Push(new IdleScreen()); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + onMatchRoomStateChanged(client.Room!.MatchState); + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + switch (matchmakingState.Stage) + { + case MatchmakingStage.WaitingForClientsJoin: + case MatchmakingStage.RoundWarmupTime: + while (screenStack.CurrentScreen is not IdleScreen) + screenStack.Exit(); + break; + + case MatchmakingStage.UserBeatmapSelect: + screenStack.Push(new PickScreen()); + break; + + case MatchmakingStage.ServerBeatmapFinalised: + Debug.Assert(screenStack.CurrentScreen is PickScreen); + ((PickScreen)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem); + break; + + case MatchmakingStage.ResultsDisplaying: + screenStack.Push(new RoundResultsScreen()); + break; + + case MatchmakingStage.Ended: + screenStack.Push(new ResultsScreen()); + break; + } + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs new file mode 100644 index 0000000000..86a46546ca --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs @@ -0,0 +1,43 @@ +// 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.Graphics; +using osu.Framework.Screens; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +{ + public partial class MatchmakingSubScreen : Screen + { + public MatchmakingSubScreen() + { + RelativePositionAxes = Axes.X; + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + this.MoveToX(1).MoveToX(0, 200); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + base.OnSuspending(e); + this.MoveToX(-1, 200); + } + + public override void OnResuming(ScreenTransitionEvent e) + { + base.OnResuming(e); + this.MoveToX(0, 200); + } + + public override bool OnExiting(ScreenExitEvent e) + { + if (base.OnExiting(e)) + return true; + + this.MoveToX(1, 200); + return false; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs new file mode 100644 index 0000000000..d3e5249c73 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs @@ -0,0 +1,192 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +{ + public partial class BeatmapPanel : CompositeDrawable + { + public static readonly Vector2 SIZE = new Vector2(300, 70); + + public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both }; + + public APIBeatmap? Beatmap + { + get => beatmap; + set + { + if (beatmap?.OnlineID == value?.OnlineID) + return; + + beatmap = value; + + if (IsLoaded) + updateContent(); + } + } + + private APIBeatmap? beatmap; + + private Container content = null!; + private UpdateableOnlineBeatmapSetCover cover = null!; + + public BeatmapPanel(APIBeatmap? beatmap = null) + { + this.beatmap = beatmap; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Masking = true; + CornerRadius = 6; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3 + }, + cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.1f), Color4.White.Opacity(0.3f)) + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 6, + BorderThickness = 2, + BorderColour = ColourInfo.GradientVertical(colourProvider.Background1, colourProvider.Background1.Opacity(0)), + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + }, + OverlayLayer, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateContent(); + FinishTransforms(true); + } + + private void updateContent() + { + foreach (var child in content.Children) + child.FadeOut(300).Expire(); + + cover.OnlineInfo = beatmap?.BeatmapSet; + + if (beatmap != null) + { + var panelContent = new BeatmapPanelContent(beatmap) + { + RelativeSizeAxes = Axes.Both, + }; + + content.Add(panelContent); + + panelContent.FadeInFromZero(300); + } + } + + private partial class BeatmapPanelContent : CompositeDrawable + { + private readonly APIBeatmap beatmap; + + public BeatmapPanelContent(APIBeatmap beatmap) + { + this.beatmap = beatmap; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Horizontal = 12 }, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode), + Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), + Shadow = false, + RelativeSizeAxes = Axes.X, + }, + new TextFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); + }).With(d => + { + d.RelativeSizeAxes = Axes.X; + d.AutoSizeAxes = Axes.Y; + d.AddText("by "); + d.AddText(new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); + }), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Top = 6 }, + Spacing = new Vector2(4), + Children = new Drawable[] + { + new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), + Shadow = false, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + }, + }; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs new file mode 100644 index 0000000000..8e93139e98 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs @@ -0,0 +1,340 @@ +// 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.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.Toolkit.HighPerformance; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +{ + public partial class BeatmapSelectionGrid : CompositeDrawable + { + public const double ARRANGE_DELAY = 200; + + private const double hide_duration = 800; + private const double arrange_duration = 1000; + private const double roll_duration = 4000; + private const double present_beatmap_delay = 1200; + private const float panel_spacing = 20; + + public event Action? ItemSelected; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private readonly Dictionary panelLookup = new Dictionary(); + + private readonly PanelGridContainer panelGridContainer; + private readonly Container rollContainer; + private readonly OsuScrollContainer scroll; + + private bool allowSelection = true; + + public BeatmapSelectionGrid() + { + InternalChildren = new Drawable[] + { + scroll = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = panelGridContainer = new PanelGridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(20), + Spacing = new Vector2(panel_spacing) + }, + }, + rollContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const double enter_duration = 500; + + // the scroll container has a 1 frame delay until it receives the correct height for the scrollable area which leads to the scrollbar resizing awkwardly + // if we wait until the panels have entered we get to avoid having to see that and the scrollbar it will appear synchronized with the rest of the content as a bonus + Scheduler.AddDelayed(() => scroll.ScrollbarVisible = true, enter_duration); + + SchedulerAfterChildren.Add(() => + { + foreach (var panel in panelGridContainer) + { + double delay = panel.Y / 3; + + panel.FadeInAndEnterFromBelow(duration: enter_duration, delay: delay); + } + }); + } + + public void AddItem(MultiplayerPlaylistItem item) + { + var panel = panelLookup[item.ID] = new BeatmapSelectionPanel(item) + { + Size = new Vector2(300, 70), + AllowSelection = allowSelection, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = ItemSelected, + }; + + panelGridContainer.Add(panel); + panelGridContainer.SetLayoutPosition(panel, (float)item.StarRating); + } + + public void RemoveItem(long id) + { + if (!panelLookup.Remove(id, out var panel)) + return; + + panel.Expire(); + } + + public void SetUserSelection(APIUser user, long itemId, bool selected) + { + if (!panelLookup.TryGetValue(itemId, out var panel)) + return; + + if (selected) + panel.AddUser(user, user.Equals(api.LocalUser.Value)); + else + panel.RemoveUser(user); + } + + public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) + { + Debug.Assert(candidateItemIds.Length >= 1); + Debug.Assert(candidateItemIds.Contains(finalItemId)); + Debug.Assert(panelLookup.ContainsKey(finalItemId)); + Debug.Assert(candidateItemIds.All(id => panelLookup.ContainsKey(id))); + + allowSelection = false; + + TransferCandidatePanelsToRollContainer(candidateItemIds); + + if (candidateItemIds.Length == 1) + { + this.Delay(ARRANGE_DELAY) + .Schedule(() => ArrangeItemsForRollAnimation()) + .Delay(arrange_duration + present_beatmap_delay) + .Schedule(() => PresentUnanimouslyChosenBeatmap(finalItemId)); + } + else + { + this.Delay(ARRANGE_DELAY) + .Schedule(() => ArrangeItemsForRollAnimation()) + .Delay(arrange_duration) + .Schedule(() => PlayRollAnimation(finalItemId, roll_duration)) + .Delay(roll_duration + present_beatmap_delay) + .Schedule(() => PresentRolledBeatmap(finalItemId)); + } + } + + internal void TransferCandidatePanelsToRollContainer(long[] candidateItemIds, double duration = hide_duration) + { + scroll.ScrollbarVisible = false; + panelGridContainer.LayoutDisabled = true; + + var rng = new Random(); + + var remainingPanels = new List(); + + foreach (var panel in panelGridContainer.Children.ToArray()) + { + panel.AllowSelection = false; + + if (!candidateItemIds.Contains(panel.Item.ID)) + { + panel.PopOutAndExpire(duration: duration / 2, delay: rng.NextDouble() * duration / 2); + continue; + } + + remainingPanels.Add(panel); + } + + rng.Shuffle(remainingPanels.AsSpan()); + + foreach (var panel in remainingPanels) + { + var position = panel.ScreenSpaceDrawQuad.Centre; + + panelGridContainer.Remove(panel, false); + + panel.Anchor = panel.Origin = Anchor.Centre; + panel.Position = rollContainer.ToLocalSpace(position) - rollContainer.ChildSize / 2; + + rollContainer.Add(panel); + } + } + + internal void ArrangeItemsForRollAnimation(double duration = arrange_duration, double stagger = 30) + { + var positions = calculateLayoutPositionsForRollAnimation(rollContainer.Children.Count); + + Debug.Assert(positions.Length == rollContainer.Children.Count); + + for (int i = 0; i < positions.Length; i++) + { + var panel = rollContainer.Children[i]; + + var position = positions[i] * (BeatmapPanel.SIZE + new Vector2(panel_spacing)); + + panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); + } + } + + private static Vector2[] calculateLayoutPositionsForRollAnimation(int panelCount) + { + if (panelCount == 1) + return new[] { Vector2.Zero }; + + // goal is to get the positions arranged in clockwise order, with the top-left position being the first one + // to keep things simple the positions are first inserted in the order: right row, optional bottom center panel, left row backwards + // then the positions get shifted by 1 to move the top-left position into the first spot + + bool hasCenterPanel = panelCount % 2 == 1; + int rowCount = (panelCount + 1) / 2; + int outerRowCount = hasCenterPanel ? rowCount - 1 : rowCount; + + float yOffset = -(rowCount - 1f) / 2; + + var positions = new Vector2[panelCount]; + + for (int row = 0; row < outerRowCount; row++) + { + positions[row] = new Vector2(0.5f, row + yOffset); + } + + if (hasCenterPanel) + { + int centerIndex = panelCount / 2; + + positions[centerIndex] = new Vector2(0, outerRowCount + yOffset); + } + + for (int row = 0; row < outerRowCount; row++) + { + int index = positions.Length - 1 - row; + + positions[index] = new Vector2(-0.5f, row + yOffset); + } + + return positions.TakeLast(1).Concat(positions.SkipLast(1)).ToArray(); + } + + internal void PlayRollAnimation(long finalItem, double duration = roll_duration) + { + const int minimum_steps = 20; + + int finalItemIndex = rollContainer.Children + .Select(it => it.Item.ID) + .ToImmutableList() + .IndexOf(finalItem); + + Debug.Assert(finalItemIndex >= 0); + + int numSteps = minimum_steps; + while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex) + numSteps++; + + BeatmapSelectionPanel? lastPanel = null; + + for (int i = 0; i < numSteps; i++) + { + float progress = ((float)i) / (numSteps - 1); + + double delay = Math.Pow(progress, 2.5) * duration; + var panel = rollContainer.Children[i % rollContainer.Children.Count]; + + Scheduler.AddDelayed(() => + { + lastPanel?.HideBorder(); + panel.ShowBorder(); + + lastPanel = panel; + }, delay); + } + } + + internal void PresentRolledBeatmap(long finalItem) + { + Debug.Assert(rollContainer.Children.Any(it => it.Item.ID == finalItem)); + + foreach (var panel in rollContainer.Children) + { + if (panel.Item.ID != finalItem) + { + panel.FadeOut(200); + panel.PopOutAndExpire(easing: Easing.InQuad); + continue; + } + + // if we changed child depth without scheduling we'd change the order of the panels while iterating + Schedule(() => + { + rollContainer.ChangeChildDepth(panel, float.MinValue); + + panel.ShowBorder(); + panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) + .ScaleTo(1.5f, 1000, Easing.OutExpo); + }); + } + } + + internal void PresentUnanimouslyChosenBeatmap(long finalItem) + { + // TODO: display special animation in this case + + PresentRolledBeatmap(finalItem); + } + + private partial class PanelGridContainer : FillFlowContainer + { + public bool LayoutDisabled; + + protected override IEnumerable ComputeLayoutPositions() + { + if (LayoutDisabled) + return FlowingChildren.Select(c => c.Position); + + return base.ComputeLayoutPositions(); + } + } + + private readonly struct SplitEasingFunction(DefaultEasingFunction easeIn, DefaultEasingFunction easeOut, float ratio) : IEasingFunction + { + public SplitEasingFunction(Easing easeIn, Easing easeOut, float ratio = 0.5f) + : this(new DefaultEasingFunction(easeIn), new DefaultEasingFunction(easeOut), ratio) + { + } + + public double ApplyEasing(double time) + { + if (time < ratio) + return easeIn.ApplyEasing(time / ratio) * ratio; + + return double.Lerp(ratio, 1, easeOut.ApplyEasing((time - ratio) / (1 - ratio))); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs new file mode 100644 index 0000000000..3f3fda32d8 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs @@ -0,0 +1,139 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +{ + public partial class BeatmapSelectionOverlay : CompositeDrawable + { + private readonly Dictionary avatars = new Dictionary(); + + private readonly Container avatarContainer; + + public new Axes AutoSizeAxes + { + get => base.AutoSizeAxes; + set => base.AutoSizeAxes = value; + } + + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + public BeatmapSelectionOverlay() + { + InternalChild = avatarContainer = new Container(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + avatarContainer.AutoSizeAxes = AutoSizeAxes; + avatarContainer.RelativeSizeAxes = RelativeSizeAxes; + } + + public bool AddUser(APIUser user, bool isOwnUser) + { + if (avatars.ContainsKey(user.Id)) + return false; + + var avatar = new SelectionAvatar(user, isOwnUser) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }; + + avatarContainer.Add(avatars[user.Id] = avatar); + + updateLayout(); + + avatar.FinishTransforms(); + + return true; + } + + public bool RemoveUser(int id) + { + if (!avatars.Remove(id, out var avatar)) + return false; + + avatar.PopOutAndExpire(); + avatarContainer.ChangeChildDepth(avatar, float.MaxValue); + + updateLayout(); + + return true; + } + + private void updateLayout() + { + const double stagger = 30; + const float spacing = 4; + + double delay = 0; + float x = 0; + + for (int i = avatarContainer.Count - 1; i >= 0; i--) + { + var avatar = avatarContainer[i]; + + if (avatar.Expired) + continue; + + avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); + + x -= avatar.LayoutSize.X + spacing; + + delay += stagger; + } + } + + public partial class SelectionAvatar : CompositeDrawable + { + public bool Expired { get; private set; } + + private readonly Container content; + + public SelectionAvatar(APIUser user, bool isOwnUser) + { + Size = new Vector2(30); + + InternalChildren = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = new MatchmakingAvatar(user, isOwnUser) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + content.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + } + + public void PopOutAndExpire() + { + content.ScaleTo(0, 400, Easing.OutExpo); + + this.FadeOut(100).Expire(); + Expired = true; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs new file mode 100644 index 0000000000..029bf48e30 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs @@ -0,0 +1,213 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +{ + public partial class BeatmapSelectionPanel : Container + { + private const float corner_radius = 6; + private const float border_width = 3; + + public readonly MultiplayerPlaylistItem Item; + + private readonly Container scaleContainer; + private readonly BeatmapPanel beatmapPanel; + private readonly BeatmapSelectionOverlay selectionOverlay; + private readonly Container border; + private readonly Box flash; + private readonly Container shadow; + + public bool AllowSelection; + + public Action? Action; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + public override bool PropagatePositionalInputSubTree => AllowSelection; + + public BeatmapSelectionPanel(MultiplayerPlaylistItem item) + { + Item = item; + Size = BeatmapPanel.SIZE; + + InternalChildren = new Drawable[] + { + scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + shadow = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(4), + Y = 8, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 7, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.15f, + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-border_width), + Child = border = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius + border_width, + Alpha = 0, + Child = new Box { RelativeSizeAxes = Axes.Both }, + } + }, + beatmapPanel = new BeatmapPanel + { + RelativeSizeAxes = Axes.Both, + OverlayLayer = + { + Children = new[] + { + flash = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + } + } + }, + selectionOverlay = new BeatmapSelectionOverlay + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10 }, + Origin = Anchor.CentreLeft, + }, + } + }, + new HoverClickSounds(), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => + { + var beatmap = b.GetResultSafely()!; + + beatmap.StarRating = Item.StarRating; + + beatmapPanel.Beatmap = beatmap; + })); + } + + public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser); + + public bool RemoveUser(int userId) => selectionOverlay.RemoveUser(userId); + + public bool RemoveUser(APIUser user) => RemoveUser(user.Id); + + protected override bool OnHover(HoverEvent e) + { + flash.FadeTo(0.2f, 50) + .Then() + .FadeTo(0.1f, 300); + + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + + flash.FadeOut(200); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + { + scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); + shadow.MoveToY(4, 400, Easing.OutExpo) + .TransformTo(nameof(Padding), new MarginPadding(2), 400, Easing.OutExpo); + return true; + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + base.OnMouseUp(e); + + if (e.Button == MouseButton.Left) + { + scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); + shadow.MoveToY(8, 500, Easing.OutElasticHalf) + .TransformTo(nameof(Padding), new MarginPadding(4), 400, Easing.OutExpo); + } + } + + protected override bool OnClick(ClickEvent e) + { + Action?.Invoke(Item); + + flash.FadeTo(0.5f, 50) + .Then() + .FadeTo(0.1f, 400); + + return true; + } + + public void ShowBorder() => border.Show(); + + public void HideBorder() => border.Hide(); + + public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) + { + scaleContainer + .FadeOut() + .MoveToY(distance) + .Delay(delay) + .FadeIn(duration / 2) + .MoveToY(0, duration, Easing.OutExpo); + } + + public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic) + { + AllowSelection = false; + + scaleContainer.Delay(delay) + .ScaleTo(0, duration, easing) + .FadeOut(duration); + + this.Delay(delay + duration).FadeOut().Expire(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs new file mode 100644 index 0000000000..73e2188273 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs @@ -0,0 +1,83 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +{ + public partial class PickScreen : OsuScreen + { + private BeatmapSelectionGrid selectionGrid = null!; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + Child = selectionGrid = new BeatmapSelectionGrid + { + RelativeSizeAxes = Axes.Both, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.ItemAdded += onItemAdded; + + foreach (var item in client.Room!.Playlist) + onItemAdded(item); + + selectionGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); + + client.MatchmakingItemSelected += onItemSelected; + client.MatchmakingItemDeselected += onItemDeselected; + } + + private void onItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => + { + if (item.Expired) + return; + + selectionGrid.AddItem(item); + }); + + private void onItemSelected(int userId, long itemId) + { + var user = client.Room!.Users.First(it => it.UserID == userId).User!; + selectionGrid.SetUserSelection(user, itemId, true); + } + + private void onItemDeselected(int userId, long itemId) + { + var user = client.Room!.Users.First(it => it.UserID == userId).User!; + selectionGrid.SetUserSelection(user, itemId, false); + } + + public void RollFinalBeatmap(long[] candidateItems, long finalItem) => selectionGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.ItemAdded -= onItemAdded; + client.MatchmakingItemSelected -= onItemSelected; + client.MatchmakingItemDeselected -= onItemDeselected; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs new file mode 100644 index 0000000000..50b34f7555 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs @@ -0,0 +1,345 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results +{ + public partial class ResultsScreen : MatchmakingSubScreen + { + private const float grid_spacing = 5; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private OsuSpriteText placementText = null!; + private FillFlowContainer userStatistics = null!; + private FillFlowContainer roomStatistics = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, grid_spacing), + new Dimension(), + new Dimension(GridSizeMode.Absolute, grid_spacing), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 75) + ], + Content = new Drawable[]?[] + { + [ + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(grid_spacing), + Children = new[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Placement", + Font = OsuFont.Default.With(size: 12) + }, + placementText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Default.With(size: 72), + UseFullGlyphHeight = false + } + } + } + ], + null, + [ + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, grid_spacing), + new Dimension() + ], + Content = new Drawable?[][] + { + [ + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(grid_spacing), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Breakdown", + Font = OsuFont.Default.With(size: 12) + }, + userStatistics = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(grid_spacing) + } + } + }, + null, + new PlayerPanelList + { + RelativeSizeAxes = Axes.Both + } + ] + } + } + ], + null, + [ + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(grid_spacing), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Statistics", + Font = OsuFont.Default.With(size: 12) + }, + roomStatistics = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(grid_spacing) + } + } + }, + ], + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onRoomStateChanged; + + onRoomStateChanged(client.Room?.MatchState); + } + + private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState || matchmakingState.Stage != MatchmakingStage.Ended) + return; + + populateUserStatistics(matchmakingState); + populateRoomStatistics(matchmakingState); + }); + + private void populateUserStatistics(MatchmakingRoomState state) + { + userStatistics.Clear(); + + if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0) + { + placementText.Text = "-"; + addStatistic("No rounds played"); + return; + } + + int overallPlacement = state.Users[client.LocalUser!.UserID].Placement; + int overallPoints = state.Users[client.LocalUser!.UserID].Points; + int bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.Min(r => r.Placement); + var accuracyPlacement = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average())) + .OrderByDescending(t => t.avgAcc) + .Select((t, i) => (info: t, index: i)) + .Single(t => t.info.user.UserId == client.LocalUser!.UserID); + + placementText.Text = $"#{state.Users[client.LocalUser!.UserID].Placement}"; + addStatistic($"#{overallPlacement} overall ({overallPoints}pts)"); + addStatistic($"#{bestPlacement} best placement"); + addStatistic($"#{accuracyPlacement.index + 1} accuracy ({accuracyPlacement.info.avgAcc.FormatAccuracy()})"); + + void addStatistic(string text) + { + userStatistics.Add(new UserStatisticPanel(text) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }); + } + } + + private void populateRoomStatistics(MatchmakingRoomState state) + { + roomStatistics.Clear(); + + long maxScore = long.MinValue; + int maxScoreUserId = 0; + + double maxAccuracy = double.MinValue; + int maxAccuracyUserId = 0; + + int maxCombo = int.MinValue; + int maxComboUserId = 0; + + long maxBonusScore = 0; + int maxBonusScoreUserId = 0; + + long largestScoreDifference = long.MinValue; + int largestScoreDifferenceUserId = 0; + + long smallestScoreDifference = long.MaxValue; + int smallestScoreDifferenceUserId = 0; + + for (int round = 1; round <= state.CurrentRound; round++) + { + long roundHighestScore = long.MinValue; + int roundHighestScoreUserId = 0; + + long roundLowestScore = long.MaxValue; + + foreach (MatchmakingUser user in state.Users) + { + if (!user.Rounds.RoundsDictionary.TryGetValue(round, out MatchmakingRound? mmRound)) + continue; + + if (mmRound.TotalScore > maxScore) + { + maxScore = mmRound.TotalScore; + maxScoreUserId = user.UserId; + } + + if (mmRound.Accuracy > maxAccuracy) + { + maxAccuracy = mmRound.Accuracy; + maxAccuracyUserId = user.UserId; + } + + if (mmRound.MaxCombo > maxCombo) + { + maxCombo = mmRound.MaxCombo; + maxComboUserId = user.UserId; + } + + if (mmRound.TotalScore > roundHighestScore) + { + roundHighestScore = mmRound.TotalScore; + roundHighestScoreUserId = user.UserId; + } + + if (mmRound.TotalScore < roundLowestScore) + roundLowestScore = mmRound.TotalScore; + } + + long roundScoreDifference = roundHighestScore - roundLowestScore; + + if (roundScoreDifference > 0 && roundScoreDifference > largestScoreDifference) + { + largestScoreDifference = roundScoreDifference; + largestScoreDifferenceUserId = roundHighestScoreUserId; + } + + if (roundScoreDifference > 0 && roundScoreDifference < smallestScoreDifference) + { + smallestScoreDifference = roundScoreDifference; + smallestScoreDifferenceUserId = roundHighestScoreUserId; + } + } + + foreach (MatchmakingUser user in state.Users) + { + int userBonusScore = 0; + + foreach (MatchmakingRound round in user.Rounds) + { + userBonusScore += round.Statistics.TryGetValue(HitResult.LargeBonus, out int bonus) ? bonus * 5 : 0; + userBonusScore += round.Statistics.TryGetValue(HitResult.SmallBonus, out bonus) ? bonus : 0; + } + + if (userBonusScore > maxBonusScore) + { + maxBonusScore = userBonusScore; + maxBonusScoreUserId = user.UserId; + } + } + + // Highest score - highest score across all rounds. + addStatistic(maxScoreUserId, "Highest score"); + + // Most accurate - highest accuracy across all rounds. + addStatistic(maxAccuracyUserId, "Most accurate"); + + // Most combo - highest combo across all rounds. + addStatistic(maxComboUserId, "Most combo"); + + // Most bonus - most bonus score across all rounds. + if (maxBonusScoreUserId > 0) + addStatistic(maxBonusScoreUserId, "Most bonus"); + + // Most clutch - smallest victory in any round. + if (smallestScoreDifferenceUserId > 0) + addStatistic(smallestScoreDifferenceUserId, "Most clutch"); + + // Best finish - largest victory in any round. + if (largestScoreDifferenceUserId > 0) + addStatistic(largestScoreDifferenceUserId, "Best finish"); + + void addStatistic(int userId, string text) + { + MultiplayerRoomUser? user = client.Room?.Users.FirstOrDefault(u => u.UserID == userId); + + if (user == null) + throw new InvalidOperationException($"User not found in room: {userId}"); + + roomStatistics.Add(new RoomStatisticPanel(text, user) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onRoomStateChanged; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs new file mode 100644 index 0000000000..00c61113ab --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs @@ -0,0 +1,52 @@ +// 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.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results +{ + public partial class RoomStatisticPanel : CompositeDrawable + { + private readonly Color4 backgroundColour = Color4.SaddleBrown; + + private readonly string text; + private readonly MultiplayerRoomUser user; + + public RoomStatisticPanel(string text, MultiplayerRoomUser user) + { + this.text = text; + this.user = user; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new CircularContainer + { + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new OsuSpriteText + { + Margin = new MarginPadding(10), + Text = $"{text}: {user.User?.Username}" + } + } + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs new file mode 100644 index 0000000000..3a39fc714d --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results +{ + public partial class UserStatisticPanel : CompositeDrawable + { + private readonly Color4 backgroundColour = Color4.SaddleBrown; + + private readonly string text; + + public UserStatisticPanel(string text) + { + this.text = text; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new CircularContainer + { + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new OsuSpriteText + { + Margin = new MarginPadding(10), + Text = text + } + } + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs new file mode 100644 index 0000000000..ad30c19c02 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs @@ -0,0 +1,36 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults +{ + internal partial class RoundResultsScorePanel : CompositeDrawable + { + public RoundResultsScorePanel(ScoreInfo score) + { + AutoSizeAxes = Axes.Both; + InternalChild = new InstantSizingScorePanel(score); + } + + public override bool PropagateNonPositionalInputSubTree => false; + public override bool PropagatePositionalInputSubTree => false; + + private partial class InstantSizingScorePanel : ScorePanel + { + public InstantSizingScorePanel(ScoreInfo score, bool isNewLocalScore = false) + : base(score, isNewLocalScore) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + FinishTransforms(true); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs new file mode 100644 index 0000000000..d7837e96c6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs @@ -0,0 +1,181 @@ +// 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.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Models; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults +{ + public partial class RoundResultsScreen : MatchmakingSubScreen + { + private const int panel_spacing = 5; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private AutoScrollContainer scrollContainer = null!; + private LoadingSpinner loadingSpinner = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + scrollContainer = new AutoScrollContainer + { + RelativeSizeAxes = Axes.Both + }, + loadingSpinner = new LoadingSpinner + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + loadingSpinner.Show(); + + queryScores().FireAndForget(); + } + + private async Task queryScores() + { + try + { + if (client.Room == null) + return; + + Task beatmapTask = beatmapLookupCache.GetBeatmapAsync(client.Room.CurrentPlaylistItem.BeatmapID); + TaskCompletionSource> scoreTask = new TaskCompletionSource>(); + + var request = new IndexPlaylistScoresRequest(client.Room.RoomID, client.Room.Settings.PlaylistItemId); + request.Success += req => scoreTask.SetResult(req.Scores); + request.Failure += e => scoreTask.SetException(e); + api.Queue(request); + + await Task.WhenAll(beatmapTask, scoreTask.Task).ConfigureAwait(false); + + APIBeatmap? apiBeatmap = beatmapTask.GetResultSafely(); + List apiScores = scoreTask.Task.GetResultSafely(); + + if (apiBeatmap == null) + return; + + // Reference: PlaylistItemResultsScreen + setScores(apiScores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(apiBeatmap.Difficulty), + Metadata = + { + Artist = apiBeatmap.Metadata.Artist, + Title = apiBeatmap.Metadata.Title, + Author = new RealmUser + { + Username = apiBeatmap.Metadata.Author.Username, + OnlineID = apiBeatmap.Metadata.Author.OnlineID, + } + }, + DifficultyName = apiBeatmap.DifficultyName, + StarRating = apiBeatmap.StarRating, + Length = apiBeatmap.Length, + BPM = apiBeatmap.BPM + })).ToArray()); + } + catch (Exception e) + { + Logger.Error(e, "Failed to load scores for playlist item."); + throw; + } + finally + { + Scheduler.Add(() => loadingSpinner.Hide()); + } + } + + private void setScores(ScoreInfo[] scores) => Scheduler.Add(() => + { + Container panels; + + scrollContainer.Child = panels = new Container + { + RelativeSizeAxes = Axes.Y, + Width = scores.Length * (ScorePanel.CONTRACTED_WIDTH + panel_spacing), + ChildrenEnumerable = scores.Select(s => new RoundResultsScorePanel(s) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }) + }; + + for (int i = 0; i < panels.Count; i++) + { + panels[i].MoveToX(panels.DrawWidth * 2) + .Delay(i * 100) + .MoveToX((ScorePanel.CONTRACTED_WIDTH + panel_spacing) * i, 500, Easing.OutQuint); + } + }); + + private partial class AutoScrollContainer : UserTrackingScrollContainer + { + private const float initial_offset = -0.5f; + private const double scroll_duration = 20000; + + private double? scrollStartTime; + + public AutoScrollContainer() + : base(Direction.Horizontal) + { + } + + protected override void Update() + { + base.Update(); + + if (!UserScrolling && Children.Count > 0) + { + scrollStartTime ??= Time.Current; + + double scrollOffset = (Time.Current - scrollStartTime.Value) / scroll_duration; + + if (scrollOffset < 1) + ScrollTo(DrawWidth * (initial_offset + scrollOffset), false); + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs new file mode 100644 index 0000000000..281374ba71 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs @@ -0,0 +1,157 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Matchmaking; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + internal partial class StageBubble : CompositeDrawable + { + private readonly Color4 backgroundColour = Color4.Salmon; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private readonly MatchmakingStage stage; + private readonly LocalisableString displayText; + private Drawable progressBar = null!; + + private DateTimeOffset countdownStartTime; + private DateTimeOffset countdownEndTime; + + public StageBubble(MatchmakingStage stage, LocalisableString displayText) + { + this.stage = stage; + this.displayText = displayText; + + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour.Darken(0.2f) + }, + progressBar = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = displayText, + Padding = new MarginPadding(10) + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + client.CountdownStarted += onCountdownStarted; + client.CountdownStopped += onCountdownStopped; + + if (client.Room != null) + { + onMatchRoomStateChanged(client.Room.MatchState); + foreach (var countdown in client.Room.ActiveCountdowns) + onCountdownStarted(countdown); + } + } + + protected override void Update() + { + base.Update(); + + TimeSpan duration = countdownEndTime - countdownStartTime; + + if (duration.TotalMilliseconds == 0) + progressBar.Width = 0; + else + { + TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; + progressBar.Width = (float)(elapsed.TotalMilliseconds / duration.TotalMilliseconds); + } + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + if (matchmakingState.Stage == MatchmakingStage.RoundWarmupTime) + { + countdownStartTime = countdownEndTime = DateTimeOffset.Now; + activate(); + } + }); + + private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (countdown is not MatchmakingStageCountdown matchmakingStatusCountdown || matchmakingStatusCountdown.Stage != stage) + return; + + countdownStartTime = DateTimeOffset.Now; + countdownEndTime = countdownStartTime + countdown.TimeRemaining; + activate(); + }); + + private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (countdown is not MatchmakingStageCountdown matchmakingStatusCountdown || matchmakingStatusCountdown.Stage != stage) + return; + + countdownEndTime = DateTimeOffset.Now; + deactivate(); + }); + + private void activate() + { + this.FadeTo(1, 200); + } + + private void deactivate() + { + this.FadeTo(0.5f, 200); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + client.CountdownStarted -= onCountdownStarted; + client.CountdownStopped -= onCountdownStopped; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs new file mode 100644 index 0000000000..1f426ec8e6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs @@ -0,0 +1,91 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class StageDisplay : CompositeDrawable + { + public static readonly (MatchmakingStage status, LocalisableString text)[] DISPLAYED_STAGES = + [ + (MatchmakingStage.RoundWarmupTime, "Next Round"), + (MatchmakingStage.UserBeatmapSelect, "Beatmap Selection"), + (MatchmakingStage.GameplayWarmupTime, "Get Ready"), + (MatchmakingStage.ResultsDisplaying, "Results"), + (MatchmakingStage.Ended, "Match End") + ]; + + public StageDisplay() + { + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + List columnDimensions = new List(); + List columnContent = new List(); + + for (int i = 0; i < DISPLAYED_STAGES.Length; i++) + { + if (i > 0) + { + columnDimensions.Add(new Dimension(GridSizeMode.AutoSize)); + columnContent.Add(new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(16), + Icon = FontAwesome.Solid.ArrowRight, + Margin = new MarginPadding { Horizontal = 10 } + }); + } + + columnDimensions.Add(new Dimension()); + columnContent.Add(new StageBubble(DISPLAYED_STAGES[i].status, DISPLAYED_STAGES[i].text) + { + RelativeSizeAxes = Axes.X + }); + } + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + ], + Content = new Drawable[][] + { + [ + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = columnDimensions.ToArray(), + RowDimensions = [new Dimension(GridSizeMode.AutoSize)], + Content = new[] { columnContent.ToArray() } + } + ], + [ + new StageText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + } + ] + } + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs new file mode 100644 index 0000000000..ab2627474e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs @@ -0,0 +1,84 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class StageText : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private OsuSpriteText text = null!; + + public StageText() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = text = new OsuSpriteText + { + Height = 16, + Font = OsuFont.Default, + AlwaysPresent = true, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + onMatchRoomStateChanged(client.Room!.MatchState); + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + text.Text = getTextForStatus(matchmakingState.Stage); + }); + + private LocalisableString getTextForStatus(MatchmakingStage status) + { + switch (status) + { + case MatchmakingStage.WaitingForClientsJoin: + return "Players are joining the match..."; + + case MatchmakingStage.WaitingForClientsBeatmapDownload: + return "Players are downloading the beatmap..."; + + case MatchmakingStage.Gameplay: + return "Game is in progress..."; + + case MatchmakingStage.Ended: + return "Thanks for playing! The match will close shortly."; + + default: + return string.Empty; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + } + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 806dc63aed..0944626edf 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; @@ -73,6 +74,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private long lastPlaylistItemId; private int lastCountdownId; + private readonly Dictionary matchmakingUserPicks = new Dictionary(); + private readonly TestRoomRequestsHandler apiRequestHandler; public TestMultiplayerClient(TestRoomRequestsHandler? apiRequestHandler = null) @@ -409,22 +412,43 @@ namespace osu.Game.Tests.Visual.Multiplayer break; case StartMatchCountdownRequest startCountdown: - ServerRoom.ActiveCountdowns.Add(new MatchStartCountdown - { - ID = ++lastCountdownId, - TimeRemaining = startCountdown.Duration - }); - - await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStartedEvent(ServerRoom.ActiveCountdowns[^1]))).ConfigureAwait(false); + await StartCountdown(new MatchStartCountdown { TimeRemaining = startCountdown.Duration }).ConfigureAwait(false); break; case StopCountdownRequest stopCountdown: - ServerRoom.ActiveCountdowns.Remove(ServerRoom.ActiveCountdowns.First(c => c.ID == stopCountdown.ID)); - await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStoppedEvent(stopCountdown.ID))).ConfigureAwait(false); + await StopCountdown(ServerRoom.ActiveCountdowns.First(c => c.ID == stopCountdown.ID)).ConfigureAwait(false); break; } } + public async Task StartCountdown(MultiplayerCountdown countdown) + { + countdown.ID = ++lastCountdownId; + countdown = clone(countdown); + + Debug.Assert(ServerRoom != null); + Debug.Assert(LocalUser != null); + + if (countdown.IsExclusive) + { + MultiplayerCountdown? existingCountdown = ServerRoom.ActiveCountdowns.FirstOrDefault(c => c.GetType() == countdown.GetType()); + if (existingCountdown != null) + await StopCountdown(existingCountdown).ConfigureAwait(false); + } + + ServerRoom.ActiveCountdowns.Add(countdown); + await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStartedEvent(ServerRoom.ActiveCountdowns[^1]))).ConfigureAwait(false); + } + + public async Task StopCountdown(MultiplayerCountdown countdown) + { + Debug.Assert(ServerRoom != null); + Debug.Assert(LocalUser != null); + + ServerRoom.ActiveCountdowns.Remove(ServerRoom.ActiveCountdowns.First(c => c.ID == countdown.ID)); + await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStoppedEvent(countdown.ID))).ConfigureAwait(false); + } + public override Task StartMatch() { Debug.Assert(ServerRoom != null); @@ -718,6 +742,66 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public async Task ChangeMatchRoomState(MatchRoomState state) + { + Debug.Assert(ServerRoom != null); + + ServerRoom.MatchState = state; + await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).ConfigureAwait(false); + } + + public override Task MatchmakingJoinLobby() + { + return Task.CompletedTask; + } + + public override Task MatchmakingLeaveLobby() + { + return Task.CompletedTask; + } + + public override async Task MatchmakingJoinQueue(MatchmakingSettings settings) + { + await ((IMultiplayerClient)this).MatchmakingQueueJoined().ConfigureAwait(false); + await ((IMultiplayerClient)this).MatchmakingQueueStatusChanged(new MatchmakingQueueStatus.Searching()).ConfigureAwait(false); + } + + public override async Task MatchmakingLeaveQueue() + { + await ((IMultiplayerClient)this).MatchmakingQueueLeft().ConfigureAwait(false); + } + + public override Task MatchmakingAcceptInvitation() + { + return Task.CompletedTask; + } + + public override Task MatchmakingDeclineInvitation() + { + return Task.CompletedTask; + } + + public override Task MatchmakingToggleSelection(long playlistItemId) + => MatchmakingToggleUserSelection(api.LocalUser.Value.OnlineID, playlistItemId); + + public override Task MatchmakingSkipToNextStage() + => Task.CompletedTask; + + public async Task MatchmakingToggleUserSelection(int userId, long playlistItemId) + { + if (matchmakingUserPicks.TryGetValue(userId, out long existingId)) + { + if (existingId == playlistItemId) + return; + + await ((IMultiplayerClient)this).MatchmakingItemDeselected(clone(userId), clone(existingId)).ConfigureAwait(false); + } + + matchmakingUserPicks[userId] = playlistItemId; + + await ((IMultiplayerClient)this).MatchmakingItemSelected(clone(userId), clone(playlistItemId)).ConfigureAwait(false); + } + #region API Room Handling public IReadOnlyList ServerSideRooms From 0225c1a8677663750db8e561f87e1a3e1f5d9a79 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Sep 2025 19:45:14 +0900 Subject: [PATCH 135/267] Fix event handler leak --- .../Matchmaking/Screens/Idle/PlayerPanelList.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs index 003c35d8c4..aa294f5bd3 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; @@ -76,5 +77,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle panels.SetLayoutPosition(panel, float.MaxValue); } }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.MatchRoomStateChanged -= onRoomStateChanged; + client.UserJoined -= onUserJoined; + client.UserLeft -= onUserLeft; + } + } } } From 3786efaa5ec9db440e3c81883f9ccee343f7766f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Sep 2025 11:36:47 +0900 Subject: [PATCH 136/267] Separate IMatchmakingClient and IMultiplayerClient Co-authored-by: Dean Herbert --- .../Online/Multiplayer/IMultiplayerClient.cs | 3 +-- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- .../Multiplayer/OnlineMultiplayerClient.cs | 16 ++++++++-------- .../Visual/Multiplayer/TestMultiplayerClient.cs | 10 +++++----- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index aaf9f6e863..adb9b92614 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using osu.Game.Online.API; -using osu.Game.Online.Matchmaking; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -14,7 +13,7 @@ namespace osu.Game.Online.Multiplayer /// /// An interface defining a multiplayer client instance. /// - public interface IMultiplayerClient : IStatefulUserHubClient, IMatchmakingClient + public interface IMultiplayerClient : IStatefulUserHubClient { /// /// Signals that the room has changed state. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 1946863988..5118b46475 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -27,7 +27,7 @@ using osu.Game.Utils; namespace osu.Game.Online.Multiplayer { - public abstract partial class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer, IMatchmakingServer + public abstract partial class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer, IMatchmakingServer, IMatchmakingClient { public Action? PostNotification { protected get; set; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 83ff06d095..7963d32469 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -72,14 +72,14 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested); - connection.On(nameof(IMultiplayerClient.MatchmakingQueueJoined), ((IMultiplayerClient)this).MatchmakingQueueJoined); - connection.On(nameof(IMultiplayerClient.MatchmakingQueueLeft), ((IMultiplayerClient)this).MatchmakingQueueLeft); - connection.On(nameof(IMultiplayerClient.MatchmakingRoomInvited), ((IMultiplayerClient)this).MatchmakingRoomInvited); - connection.On(nameof(IMultiplayerClient.MatchmakingRoomReady), ((IMultiplayerClient)this).MatchmakingRoomReady); - connection.On(nameof(IMultiplayerClient.MatchmakingLobbyStatusChanged), ((IMultiplayerClient)this).MatchmakingLobbyStatusChanged); - connection.On(nameof(IMultiplayerClient.MatchmakingQueueStatusChanged), ((IMultiplayerClient)this).MatchmakingQueueStatusChanged); - connection.On(nameof(IMultiplayerClient.MatchmakingItemSelected), ((IMultiplayerClient)this).MatchmakingItemSelected); - connection.On(nameof(IMultiplayerClient.MatchmakingItemDeselected), ((IMultiplayerClient)this).MatchmakingItemDeselected); + connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined); + connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft); + connection.On(nameof(IMatchmakingClient.MatchmakingRoomInvited), ((IMatchmakingClient)this).MatchmakingRoomInvited); + connection.On(nameof(IMatchmakingClient.MatchmakingRoomReady), ((IMatchmakingClient)this).MatchmakingRoomReady); + connection.On(nameof(IMatchmakingClient.MatchmakingLobbyStatusChanged), ((IMatchmakingClient)this).MatchmakingLobbyStatusChanged); + connection.On(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged); + connection.On(nameof(IMatchmakingClient.MatchmakingItemSelected), ((IMatchmakingClient)this).MatchmakingItemSelected); + connection.On(nameof(IMatchmakingClient.MatchmakingItemDeselected), ((IMatchmakingClient)this).MatchmakingItemDeselected); }; IsConnected.BindTo(connector.IsConnected); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 0944626edf..96a7aae983 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -762,13 +762,13 @@ namespace osu.Game.Tests.Visual.Multiplayer public override async Task MatchmakingJoinQueue(MatchmakingSettings settings) { - await ((IMultiplayerClient)this).MatchmakingQueueJoined().ConfigureAwait(false); - await ((IMultiplayerClient)this).MatchmakingQueueStatusChanged(new MatchmakingQueueStatus.Searching()).ConfigureAwait(false); + await ((IMatchmakingClient)this).MatchmakingQueueJoined().ConfigureAwait(false); + await ((IMatchmakingClient)this).MatchmakingQueueStatusChanged(new MatchmakingQueueStatus.Searching()).ConfigureAwait(false); } public override async Task MatchmakingLeaveQueue() { - await ((IMultiplayerClient)this).MatchmakingQueueLeft().ConfigureAwait(false); + await ((IMatchmakingClient)this).MatchmakingQueueLeft().ConfigureAwait(false); } public override Task MatchmakingAcceptInvitation() @@ -794,12 +794,12 @@ namespace osu.Game.Tests.Visual.Multiplayer if (existingId == playlistItemId) return; - await ((IMultiplayerClient)this).MatchmakingItemDeselected(clone(userId), clone(existingId)).ConfigureAwait(false); + await ((IMatchmakingClient)this).MatchmakingItemDeselected(clone(userId), clone(existingId)).ConfigureAwait(false); } matchmakingUserPicks[userId] = playlistItemId; - await ((IMultiplayerClient)this).MatchmakingItemSelected(clone(userId), clone(playlistItemId)).ConfigureAwait(false); + await ((IMatchmakingClient)this).MatchmakingItemSelected(clone(userId), clone(playlistItemId)).ConfigureAwait(false); } #region API Room Handling From 3985596602087c5a4a821a16f56072a082b5245b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Sep 2025 12:36:50 +0900 Subject: [PATCH 137/267] Join matchmaking rooms with password --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 6 +++--- osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 2 +- .../Screens/OnlinePlay/Matchmaking/MatchmakingController.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 5118b46475..6be50fcf1a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -122,7 +122,7 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchmakingQueueJoined; public event Action? MatchmakingQueueLeft; public event Action? MatchmakingRoomInvited; - public event Action? MatchmakingRoomReady; + public event Action? MatchmakingRoomReady; public event Action? MatchmakingLobbyStatusChanged; public event Action? MatchmakingQueueStatusChanged; public event Action? MatchmakingItemSelected; @@ -1051,9 +1051,9 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMatchmakingClient.MatchmakingRoomReady(long roomId) + Task IMatchmakingClient.MatchmakingRoomReady(long roomId, string password) { - Scheduler.Add(() => MatchmakingRoomReady?.Invoke(roomId)); + Scheduler.Add(() => MatchmakingRoomReady?.Invoke(roomId, password)); return Task.CompletedTask; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 7963d32469..137e21add4 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -75,7 +75,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined); connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft); connection.On(nameof(IMatchmakingClient.MatchmakingRoomInvited), ((IMatchmakingClient)this).MatchmakingRoomInvited); - connection.On(nameof(IMatchmakingClient.MatchmakingRoomReady), ((IMatchmakingClient)this).MatchmakingRoomReady); + connection.On(nameof(IMatchmakingClient.MatchmakingRoomReady), ((IMatchmakingClient)this).MatchmakingRoomReady); connection.On(nameof(IMatchmakingClient.MatchmakingLobbyStatusChanged), ((IMatchmakingClient)this).MatchmakingLobbyStatusChanged); connection.On(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged); connection.On(nameof(IMatchmakingClient.MatchmakingItemSelected), ((IMatchmakingClient)this).MatchmakingItemSelected); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs index dde7adfc13..46fd887b75 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs @@ -102,9 +102,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } }); - private void onMatchmakingRoomReady(long roomId) => Scheduler.Add(() => + private void onMatchmakingRoomReady(long roomId, string password) => Scheduler.Add(() => { - client.JoinRoom(new Room { RoomID = roomId }) + client.JoinRoom(new Room { RoomID = roomId }, password) .FireAndForget(() => Scheduler.Add(() => { CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.InRoom; From 330b61f4c0ecf0e36815e6b84075970496e6a5d8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Sep 2025 21:06:24 +0900 Subject: [PATCH 138/267] Add ruleset selector (#130) --- .../TestSceneMatchmakingRulesetSelector.cs | 20 ++ .../Matchmaking/MatchmakingController.cs | 6 - .../Matchmaking/MatchmakingRulesetSelector.cs | 183 ++++++++++++++++++ .../Screens/MatchmakingQueueScreen.cs | 28 ++- 4 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingRulesetSelector.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingRulesetSelector.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingRulesetSelector.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingRulesetSelector.cs new file mode 100644 index 0000000000..802e3b0f9d --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingRulesetSelector.cs @@ -0,0 +1,20 @@ +// 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.Graphics; +using osu.Game.Screens.OnlinePlay.Matchmaking; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneMatchmakingRulesetSelector : OsuTestScene + { + public TestSceneMatchmakingRulesetSelector() + { + Child = new MatchmakingRulesetSelector + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs index 46fd887b75..1a426501d7 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs @@ -10,7 +10,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; namespace osu.Game.Screens.OnlinePlay.Matchmaking @@ -28,9 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking [Resolved] private IPerformFromScreenRunner? performer { get; set; } - [Resolved] - private IBindable ruleset { get; set; } = null!; - private ProgressNotification? backgroundNotification; private Notification? readyNotification; private bool isBackgrounded; @@ -44,8 +40,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking client.MatchmakingQueueLeft += onMatchmakingQueueLeft; client.MatchmakingRoomInvited += onMatchmakingRoomInvited; client.MatchmakingRoomReady += onMatchmakingRoomReady; - - ruleset.BindValueChanged(_ => client.MatchmakingLeaveQueue().FireAndForget()); } public void SearchInBackground() diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingRulesetSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingRulesetSelector.cs new file mode 100644 index 0000000000..e281c210b5 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingRulesetSelector.cs @@ -0,0 +1,183 @@ +// 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.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Matchmaking; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class MatchmakingRulesetSelector : CompositeDrawable + { + private const float icon_size = 36; + + public readonly Bindable SelectedSettings = new Bindable(new MatchmakingSettings()); + + public MatchmakingRulesetSelector() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3), + Children = + [ + new SelectorButton(OsuIcon.RulesetOsu) + { + Settings = new MatchmakingSettings { RulesetId = 0 }, + SelectedSettings = { BindTarget = SelectedSettings } + }, + new SelectorButton(OsuIcon.RulesetTaiko) + { + Settings = new MatchmakingSettings { RulesetId = 1 }, + SelectedSettings = { BindTarget = SelectedSettings } + }, + new SelectorButton(OsuIcon.RulesetCatch) + { + Settings = new MatchmakingSettings { RulesetId = 2 }, + SelectedSettings = { BindTarget = SelectedSettings } + }, + new ManiaSelectorButton(4) + { + Settings = new MatchmakingSettings + { + RulesetId = 3, + Variant = 4 + }, + SelectedSettings = { BindTarget = SelectedSettings } + }, + new ManiaSelectorButton(7) + { + Settings = new MatchmakingSettings + { + RulesetId = 3, + Variant = 7 + }, + SelectedSettings = { BindTarget = SelectedSettings } + } + ] + }; + } + + private partial class SelectorButton : CompositeDrawable + { + public required MatchmakingSettings Settings { get; init; } + + public readonly Bindable SelectedSettings = new Bindable(new MatchmakingSettings()); + + private readonly IconUsage icon; + private Drawable iconSprite = null!; + + public SelectorButton(IconUsage icon) + { + this.icon = icon; + + Size = new Vector2(icon_size); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new OsuAnimatedButton + { + RelativeSizeAxes = Axes.Both, + Child = iconSprite = CreateIcon(), + Action = () => SelectedSettings.Value = Settings + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedSettings.BindValueChanged(onSelectionChanged, true); + FinishTransforms(true); + } + + private void onSelectionChanged(ValueChangedEvent selection) + { + if (selection.NewValue.Equals(Settings)) + iconSprite.FadeColour(Color4.Gold, 100, Easing.OutQuint); + else + iconSprite.FadeColour(OsuColour.Gray(0.5f), 100); + } + + protected override bool OnClick(ClickEvent e) + { + SelectedSettings.Value = Settings; + return true; + } + + protected virtual Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = icon + }; + } + + private partial class ManiaSelectorButton : SelectorButton + { + private readonly int variant; + + public ManiaSelectorButton(int variant) + : base(OsuIcon.RulesetMania) + { + this.variant = variant; + } + + protected override Drawable CreateIcon() => new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = OsuIcon.RulesetMania + }, + new Container + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(14, 10), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = $"{variant}K", + Font = OsuFont.Default.With(size: 8, fixedWidth: true, weight: FontWeight.Bold), + UseFullGlyphHeight = false, + Blending = new BlendingParameters + { + AlphaEquation = BlendingEquation.ReverseSubtract + } + } + } + } + } + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs index e434ed240a..5a6f13d2eb 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs @@ -61,6 +61,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private IBindable ruleset { get; set; } = null!; private readonly IBindable currentState = new Bindable(); + private readonly Bindable currentSettings = new Bindable(new MatchmakingSettings()); + private CancellationTokenSource userLookupCancellation = new CancellationTokenSource(); protected override void LoadComplete() @@ -117,6 +119,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens currentState.BindTo(controller.CurrentState); currentState.BindValueChanged(s => SetState(s.NewValue)); + // Pull current ruleset from the global select ruleset, if able. + currentSettings.Value = new MatchmakingSettings + { + RulesetId = ruleset.Value.CreateInstance() is ILegacyRuleset legacy ? legacy.LegacyID : 0 + }; + + // Default mania to 4K. + if (currentSettings.Value.RulesetId == 3) + currentSettings.Value.Variant = 4; + + currentSettings.BindValueChanged(_ => client.MatchmakingLeaveQueue().FireAndForget()); + client.MatchmakingLobbyStatusChanged += onMatchmakingLobbyStatusChanged; } @@ -216,16 +230,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(20), + Spacing = new Vector2(10), Children = new Drawable[] { + new MatchmakingRulesetSelector + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + SelectedSettings = { BindTarget = currentSettings } + }, new ShearedButton(200) { DarkerColour = colours.Blue2, LighterColour = colours.Blue1, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = () => client.MatchmakingJoinQueue(new MatchmakingSettings { RulesetId = ruleset.Value.OnlineID }).FireAndForget(), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = () => client.MatchmakingJoinQueue(currentSettings.Value).FireAndForget(), Text = "Begin queueing", } } From 35e1fa666015d896f45e7d8ff11bccba3c6c7472 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Sep 2025 21:09:25 +0900 Subject: [PATCH 139/267] Fix test failure --- .../Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs index ea2a2d15eb..8aa66ffc09 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestBasic() { - AddUntilStep("wait for queue screen", () => queueScreen != null); + AddUntilStep("wait for queue screen", () => queueScreen?.IsLoaded == true); AddStep("set users", () => { From e0c11504a2dca2656df00fd5e8426e72c12b1f70 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Sep 2025 15:34:34 +0900 Subject: [PATCH 140/267] Query for available pools for selection (#131) --- .../TestSceneMatchmakingPoolSelector.cs | 35 ++++ .../TestSceneMatchmakingQueueScreen.cs | 5 +- .../TestSceneMatchmakingRulesetSelector.cs | 20 -- .../Online/Multiplayer/MultiplayerClient.cs | 4 +- .../Multiplayer/OnlineMultiplayerClient.cs | 13 +- .../Matchmaking/MatchmakingPoolSelector.cs | 147 ++++++++++++++ .../Matchmaking/MatchmakingRulesetSelector.cs | 183 ------------------ .../Screens/MatchmakingQueueScreen.cs | 64 ++++-- .../Multiplayer/TestMultiplayerClient.cs | 14 +- 9 files changed, 260 insertions(+), 225 deletions(-) create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingRulesetSelector.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingRulesetSelector.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs new file mode 100644 index 0000000000..442a06606b --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs @@ -0,0 +1,35 @@ +// 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.Graphics; +using osu.Game.Online.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneMatchmakingPoolSelector : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("add selector", () => Child = new MatchmakingPoolSelector + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AvailablePools = + { + Value = + [ + new MatchmakingPool { Id = 0, RulesetId = 0 }, + new MatchmakingPool { Id = 1, RulesetId = 1 }, + new MatchmakingPool { Id = 2, RulesetId = 2 }, + new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4 }, + new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7 }, + ] + } + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs index 8aa66ffc09..72eba6e1c8 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs @@ -9,11 +9,12 @@ using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneMatchmakingQueueScreen : ScreenTestScene + public partial class TestSceneMatchmakingQueueScreen : MultiplayerTestScene { [Cached] private readonly MatchmakingController controller = new MatchmakingController(); @@ -23,6 +24,8 @@ namespace osu.Game.Tests.Visual.Matchmaking [SetUpSteps] public override void SetUpSteps() { + base.SetUpSteps(); + AddStep("load screen", () => LoadScreen(new MatchmakingIntroScreen())); } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingRulesetSelector.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingRulesetSelector.cs deleted file mode 100644 index 802e3b0f9d..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingRulesetSelector.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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.Graphics; -using osu.Game.Screens.OnlinePlay.Matchmaking; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneMatchmakingRulesetSelector : OsuTestScene - { - public TestSceneMatchmakingRulesetSelector() - { - Child = new MatchmakingRulesetSelector - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - } - } -} diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 6be50fcf1a..09dd3a00ae 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -1091,11 +1091,13 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + public abstract Task GetMatchmakingPools(); + public abstract Task MatchmakingJoinLobby(); public abstract Task MatchmakingLeaveLobby(); - public abstract Task MatchmakingJoinQueue(MatchmakingSettings settings); + public abstract Task MatchmakingJoinQueue(int poolId); public abstract Task MatchmakingLeaveQueue(); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 137e21add4..0decff7ab3 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -320,6 +320,15 @@ namespace osu.Game.Online.Multiplayer return connector.Disconnect(); } + public override Task GetMatchmakingPools() + { + if (!IsConnected.Value) + return Task.FromResult(Array.Empty()); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMatchmakingServer.GetMatchmakingPools)); + } + public override Task MatchmakingJoinLobby() { if (!IsConnected.Value) @@ -338,13 +347,13 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveLobby)); } - public override Task MatchmakingJoinQueue(MatchmakingSettings settings) + public override Task MatchmakingJoinQueue(int poolId) { if (!IsConnected.Value) return Task.CompletedTask; Debug.Assert(connection != null); - return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueue), settings); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueue), poolId); } public override Task MatchmakingLeaveQueue() diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs new file mode 100644 index 0000000000..43e6acfaf7 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs @@ -0,0 +1,147 @@ +// 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.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Matchmaking; +using osu.Game.Rulesets; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + public partial class MatchmakingPoolSelector : CompositeDrawable + { + private const float icon_size = 36; + + public readonly Bindable AvailablePools = new Bindable(); + public readonly Bindable SelectedPool = new Bindable(); + + private FillFlowContainer poolFlow = null!; + + public MatchmakingPoolSelector() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = poolFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3) + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AvailablePools.BindValueChanged(pools => + { + poolFlow.Clear(); + foreach (var p in pools.NewValue) + poolFlow.Add(new SelectorButton(p) { SelectedPool = { BindTarget = SelectedPool } }); + }, true); + } + + private partial class SelectorButton : CompositeDrawable + { + public readonly Bindable SelectedPool = new Bindable(); + + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + + private readonly MatchmakingPool pool; + private Drawable iconSprite = null!; + + public SelectorButton(MatchmakingPool pool) + { + this.pool = pool; + + Size = new Vector2(icon_size); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new OsuAnimatedButton + { + RelativeSizeAxes = Axes.Both, + Child = iconSprite = createIcon(), + Action = () => SelectedPool.Value = pool + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedPool.BindValueChanged(onSelectionChanged, true); + FinishTransforms(true); + } + + private void onSelectionChanged(ValueChangedEvent selection) + { + if (selection.NewValue?.Equals(pool) == true) + iconSprite.FadeColour(Color4.Gold, 100, Easing.OutQuint); + else + iconSprite.FadeColour(OsuColour.Gray(0.5f), 100); + } + + private Drawable createIcon() + { + Ruleset? rulesetInstance = rulesetStore.GetRuleset(pool.RulesetId)?.CreateInstance(); + if (rulesetInstance == null) + return Empty(); + + Drawable icon = rulesetInstance.CreateIcon().With(d => d.RelativeSizeAxes = Axes.Both); + + if (pool.Variant == 0) + return icon; + + return new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + icon, + new Container + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(14, 10), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = $"{pool.Variant}K", + Font = OsuFont.Default.With(size: 8, fixedWidth: true, weight: FontWeight.Bold), + UseFullGlyphHeight = false, + Blending = new BlendingParameters + { + AlphaEquation = BlendingEquation.ReverseSubtract + } + } + } + } + } + }; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingRulesetSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingRulesetSelector.cs deleted file mode 100644 index e281c210b5..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingRulesetSelector.cs +++ /dev/null @@ -1,183 +0,0 @@ -// 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.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Matchmaking; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking -{ - public partial class MatchmakingRulesetSelector : CompositeDrawable - { - private const float icon_size = 36; - - public readonly Bindable SelectedSettings = new Bindable(new MatchmakingSettings()); - - public MatchmakingRulesetSelector() - { - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(3), - Children = - [ - new SelectorButton(OsuIcon.RulesetOsu) - { - Settings = new MatchmakingSettings { RulesetId = 0 }, - SelectedSettings = { BindTarget = SelectedSettings } - }, - new SelectorButton(OsuIcon.RulesetTaiko) - { - Settings = new MatchmakingSettings { RulesetId = 1 }, - SelectedSettings = { BindTarget = SelectedSettings } - }, - new SelectorButton(OsuIcon.RulesetCatch) - { - Settings = new MatchmakingSettings { RulesetId = 2 }, - SelectedSettings = { BindTarget = SelectedSettings } - }, - new ManiaSelectorButton(4) - { - Settings = new MatchmakingSettings - { - RulesetId = 3, - Variant = 4 - }, - SelectedSettings = { BindTarget = SelectedSettings } - }, - new ManiaSelectorButton(7) - { - Settings = new MatchmakingSettings - { - RulesetId = 3, - Variant = 7 - }, - SelectedSettings = { BindTarget = SelectedSettings } - } - ] - }; - } - - private partial class SelectorButton : CompositeDrawable - { - public required MatchmakingSettings Settings { get; init; } - - public readonly Bindable SelectedSettings = new Bindable(new MatchmakingSettings()); - - private readonly IconUsage icon; - private Drawable iconSprite = null!; - - public SelectorButton(IconUsage icon) - { - this.icon = icon; - - Size = new Vector2(icon_size); - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new OsuAnimatedButton - { - RelativeSizeAxes = Axes.Both, - Child = iconSprite = CreateIcon(), - Action = () => SelectedSettings.Value = Settings - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - SelectedSettings.BindValueChanged(onSelectionChanged, true); - FinishTransforms(true); - } - - private void onSelectionChanged(ValueChangedEvent selection) - { - if (selection.NewValue.Equals(Settings)) - iconSprite.FadeColour(Color4.Gold, 100, Easing.OutQuint); - else - iconSprite.FadeColour(OsuColour.Gray(0.5f), 100); - } - - protected override bool OnClick(ClickEvent e) - { - SelectedSettings.Value = Settings; - return true; - } - - protected virtual Drawable CreateIcon() => new SpriteIcon - { - RelativeSizeAxes = Axes.Both, - Icon = icon - }; - } - - private partial class ManiaSelectorButton : SelectorButton - { - private readonly int variant; - - public ManiaSelectorButton(int variant) - : base(OsuIcon.RulesetMania) - { - this.variant = variant; - } - - protected override Drawable CreateIcon() => new BufferedContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new SpriteIcon - { - RelativeSizeAxes = Axes.Both, - Icon = OsuIcon.RulesetMania - }, - new Container - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Size = new Vector2(14, 10), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = $"{variant}K", - Font = OsuFont.Default.With(size: 8, fixedWidth: true, weight: FontWeight.Bold), - UseFullGlyphHeight = false, - Blending = new BlendingParameters - { - AlphaEquation = BlendingEquation.ReverseSubtract - } - } - } - } - } - }; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs index 5a6f13d2eb..266dd8782c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -61,7 +63,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private IBindable ruleset { get; set; } = null!; private readonly IBindable currentState = new Bindable(); - private readonly Bindable currentSettings = new Bindable(new MatchmakingSettings()); + + private readonly Bindable availablePools = new Bindable(); + private readonly Bindable selectedPool = new Bindable(); private CancellationTokenSource userLookupCancellation = new CancellationTokenSource(); @@ -119,19 +123,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens currentState.BindTo(controller.CurrentState); currentState.BindValueChanged(s => SetState(s.NewValue)); - // Pull current ruleset from the global select ruleset, if able. - currentSettings.Value = new MatchmakingSettings - { - RulesetId = ruleset.Value.CreateInstance() is ILegacyRuleset legacy ? legacy.LegacyID : 0 - }; - - // Default mania to 4K. - if (currentSettings.Value.RulesetId == 3) - currentSettings.Value.Variant = 4; - - currentSettings.BindValueChanged(_ => client.MatchmakingLeaveQueue().FireAndForget()); - client.MatchmakingLobbyStatusChanged += onMatchmakingLobbyStatusChanged; + + populateAvailablePools().FireAndForget(); + } + + private async Task populateAvailablePools() + { + MatchmakingPool[] pools = await client.GetMatchmakingPools().ConfigureAwait(false); + + Schedule(() => + { + availablePools.Value = pools; + + // Default to the user's ruleset for the initial pool selection. + selectedPool.Value = pools.FirstOrDefault(p => p.RulesetId == ruleset.Value.OnlineID) ?? pools.FirstOrDefault(); + }); } private void onMatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status) => Scheduler.Add(() => @@ -233,19 +240,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens Spacing = new Vector2(10), Children = new Drawable[] { - new MatchmakingRulesetSelector + new MatchmakingPoolSelector { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - SelectedSettings = { BindTarget = currentSettings } + AvailablePools = { BindTarget = availablePools }, + SelectedPool = { BindTarget = selectedPool } }, - new ShearedButton(200) + new BeginQueueingButton(200) { DarkerColour = colours.Blue2, LighterColour = colours.Blue1, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = () => client.MatchmakingJoinQueue(currentSettings.Value).FireAndForget(), + SelectedPool = { BindTarget = selectedPool }, + Action = () => + { + Debug.Assert(selectedPool.Value != null); + client.MatchmakingJoinQueue(selectedPool.Value.Id).FireAndForget(); + }, Text = "Begin queueing", } } @@ -409,5 +422,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens AcceptedWaitingForRoom, InRoom } + + private partial class BeginQueueingButton : ShearedButton + { + public readonly IBindable SelectedPool = new Bindable(); + + public BeginQueueingButton(float? width = null) + : base(width) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedPool.BindValueChanged(p => Enabled.Value = p.NewValue != null, true); + } + } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 96a7aae983..1cea38667e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -750,6 +750,18 @@ namespace osu.Game.Tests.Visual.Multiplayer await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).ConfigureAwait(false); } + public override Task GetMatchmakingPools() + { + return Task.FromResult( + [ + new MatchmakingPool { Id = 0, RulesetId = 0 }, + new MatchmakingPool { Id = 1, RulesetId = 1 }, + new MatchmakingPool { Id = 2, RulesetId = 2 }, + new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4 }, + new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7 }, + ]); + } + public override Task MatchmakingJoinLobby() { return Task.CompletedTask; @@ -760,7 +772,7 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - public override async Task MatchmakingJoinQueue(MatchmakingSettings settings) + public override async Task MatchmakingJoinQueue(int poolId) { await ((IMatchmakingClient)this).MatchmakingQueueJoined().ConfigureAwait(false); await ((IMatchmakingClient)this).MatchmakingQueueStatusChanged(new MatchmakingQueueStatus.Searching()).ConfigureAwait(false); From e6dbb1020c364ef136427b9ade13f8300548c8b6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 5 Sep 2025 19:23:22 +0900 Subject: [PATCH 141/267] Add some audio feedback to the matchmaking flow (#132) --- .../Visual/Matchmaking/TestScenePickScreen.cs | 6 ++- osu.Game/Configuration/SessionStatics.cs | 8 ++++ .../Matchmaking/MatchmakingCloud.cs | 33 ++++++++++++++- .../Screens/MatchmakingQueueScreen.cs | 11 +++++ .../Screens/Pick/BeatmapSelectionGrid.cs | 42 +++++++++++++++++++ .../Screens/Pick/BeatmapSelectionOverlay.cs | 18 ++++++++ 6 files changed, 116 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index fdb5aed789..16f687d772 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -90,7 +91,10 @@ namespace osu.Game.Tests.Visual.Matchmaking var item = items[Random.Shared.Next(items.Length)]; selectedItems.Add(item.ID); - MultiplayerClient.MatchmakingToggleUserSelection(user.Id, item.ID).FireAndForget(); + Scheduler.AddDelayed(() => + { + MultiplayerClient.MatchmakingToggleUserSelection(user.Id, item.ID).FireAndForget(); + }, RNG.NextDouble(10, 1000)); } }); diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 59e107a23e..0c0b2a989d 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -8,6 +8,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Users; @@ -28,6 +29,7 @@ namespace osu.Game.Configuration SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.LastRankChangeSamplePlaybackTime, (double?)null); + SetDefault(Static.LastMatchmakingCloudSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); SetDefault(Static.LastLocalUserScore, null); @@ -81,6 +83,12 @@ namespace osu.Game.Configuration /// LastRankChangeSamplePlaybackTime, + /// + /// The last playback time in milliseconds for the 'user appear' sample in . + /// Used to debounce sample playback to avoid volume saturation from multiple simultaneous playback. + /// + LastMatchmakingCloudSamplePlaybackTime, + /// /// Whether the last positional input received was a touch input. /// Used in touchscreen detection scenarios (). diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs index 5a738f05d4..d2b2b72f02 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs @@ -3,9 +3,14 @@ using System; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Ranking; using osuTK; @@ -64,6 +69,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private float targetScale; private float targetAlpha; + private Bindable lastSamplePlaybackTime = null!; + + private Sample? playerAppearSample; + public MovingAvatar(APIUser apiUser) : base(apiUser) { @@ -73,6 +82,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Origin = Anchor.Centre; } + [BackgroundDependencyLoader] + private void load(AudioManager audio, SessionStatics statics) + { + playerAppearSample = audio.Samples.Get(@"UI/toolbar-hover"); + lastSamplePlaybackTime = statics.GetBindable(Static.LastMatchmakingCloudSamplePlaybackTime); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -85,7 +101,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Scale = new Vector2(targetScale); Hide(); - this.Delay(RNG.Next(0, 1000)).FadeTo(targetAlpha, 2000, Easing.OutQuint); + int appearDelay = RNG.Next(0, 1000); + this.Delay(appearDelay).FadeTo(targetAlpha, 2000, Easing.OutQuint); + Scheduler.AddDelayed(() => + { + bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + if (!enoughTimeElapsed) return; + + var chan = playerAppearSample?.GetChannel(); + + if (chan == null) return; + + chan.Frequency.Value = 1f + RNG.NextDouble(0.25f); + chan.Play(); + + lastSamplePlaybackTime.Value = Time.Current; + }, appearDelay); } private void updateParams() diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs index 266dd8782c..f906a0e06f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; @@ -69,6 +71,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private CancellationTokenSource userLookupCancellation = new CancellationTokenSource(); + private Sample? matchFoundSample; + protected override void LoadComplete() { base.LoadComplete(); @@ -141,6 +145,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens }); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + matchFoundSample = audio.Samples.Get(@"Multiplayer/matchmaking/match-found"); + } + private void onMatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status) => Scheduler.Add(() => { userLookupCancellation.Cancel(); @@ -349,6 +359,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens } } }; + matchFoundSample?.Play(); break; case MatchmakingScreenState.AcceptedWaitingForRoom: diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs index 8e93139e98..66cae72616 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs @@ -8,9 +8,12 @@ using System.Diagnostics; using System.Linq; using Microsoft.Toolkit.HighPerformance; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -42,6 +45,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick private bool allowSelection = true; + private readonly Sample[] rouletteSamples = new Sample[8]; + private Sample? rouletteResultSample; + private Sample? swooshSample; + private double? lastSamplePlayback; + public BeatmapSelectionGrid() { InternalChildren = new Drawable[] @@ -66,6 +74,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick }; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + rouletteSamples[0] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-0"); + rouletteSamples[1] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-1"); + rouletteSamples[2] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-2"); + rouletteSamples[3] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-3"); + rouletteSamples[4] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-4"); + rouletteSamples[5] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-2"); + rouletteSamples[6] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-3"); + rouletteSamples[7] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-4"); + rouletteResultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-result"); + swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -200,6 +223,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick var position = positions[i] * (BeatmapPanel.SIZE + new Vector2(panel_spacing)); panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); + + Scheduler.AddDelayed(() => + { + var chan = swooshSample?.GetChannel(); + if (chan == null) return; + + chan.Frequency.Value = 1.25f - RNG.NextDouble(0.5f); + chan.Play(); + }, stagger * i); } } @@ -266,11 +298,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick double delay = Math.Pow(progress, 2.5) * duration; var panel = rollContainer.Children[i % rollContainer.Children.Count]; + int ii = i; Scheduler.AddDelayed(() => { lastPanel?.HideBorder(); panel.ShowBorder(); + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) + { + int sampleIdx = ii % (rouletteSamples.Length); + rouletteSamples[sampleIdx].Play(); + lastSamplePlayback = Time.Current; + } + lastPanel = panel; }, delay); } @@ -297,6 +337,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick panel.ShowBorder(); panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) .ScaleTo(1.5f, 1000, Easing.OutExpo); + + rouletteResultSample?.Play(); }); } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs index 3f3fda32d8..2a15201d11 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; @@ -15,6 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick private readonly Container avatarContainer; + private Sample? userAddedSample; + private double? lastSamplePlayback; + public new Axes AutoSizeAxes { get => base.AutoSizeAxes; @@ -40,6 +46,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick avatarContainer.RelativeSizeAxes = RelativeSizeAxes; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); + } + public bool AddUser(APIUser user, bool isOwnUser) { if (avatars.ContainsKey(user.Id)) @@ -53,6 +65,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick avatarContainer.Add(avatars[user.Id] = avatar); + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) + { + userAddedSample?.Play(); + lastSamplePlayback = Time.Current; + } + updateLayout(); avatar.FinishTransforms(); From 27db49bad379fd7479b050a7da92256a53e9997e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Sep 2025 19:23:31 +0900 Subject: [PATCH 142/267] Fix results screen crash from missing users (#133) --- .../Matchmaking/TestSceneRoomStatisticPanel.cs | 11 +---------- .../Screens/Results/ResultsScreen.cs | 8 +------- .../Screens/Results/RoomStatisticPanel.cs | 17 +++++++++++------ 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs index b5d69485cf..494f9b6517 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; using osu.Game.Tests.Visual.Multiplayer; @@ -15,14 +13,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("add statistic", () => Child = new RoomStatisticPanel("Statistic description", new MultiplayerRoomUser(1) - { - User = new APIUser - { - Id = 1, - Username = "peppy" - } - }) + AddStep("add statistic", () => Child = new RoomStatisticPanel("Statistic description", 1) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs index 50b34f7555..3fe4cc6d7a 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs @@ -1,7 +1,6 @@ // 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 osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; @@ -321,12 +320,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results void addStatistic(int userId, string text) { - MultiplayerRoomUser? user = client.Room?.Users.FirstOrDefault(u => u.UserID == userId); - - if (user == null) - throw new InvalidOperationException($"User not found in room: {userId}"); - - roomStatistics.Add(new RoomStatisticPanel(text, user) + roomStatistics.Add(new RoomStatisticPanel(text, userId) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs index 00c61113ab..5988a73ef8 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Database; using osu.Game.Graphics.Sprites; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API.Requests.Responses; using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results @@ -16,19 +18,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results private readonly Color4 backgroundColour = Color4.SaddleBrown; private readonly string text; - private readonly MultiplayerRoomUser user; + private readonly int userId; - public RoomStatisticPanel(string text, MultiplayerRoomUser user) + public RoomStatisticPanel(string text, int userId) { this.text = text; - this.user = user; + this.userId = userId; AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load() + private void load(UserLookupCache userLookupCache) { + // Should be cached by this point. + APIUser? user = userLookupCache.GetUserAsync(userId).GetResultSafely(); + InternalChild = new CircularContainer { AutoSizeAxes = Axes.Both, @@ -43,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results new OsuSpriteText { Margin = new MarginPadding(10), - Text = $"{text}: {user.User?.Username}" + Text = $"{text}: {user?.Username}" } } }; From 1a49c81bab4cd71ae9f7e586612d93bd3b9afc58 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Sep 2025 19:54:12 +0900 Subject: [PATCH 143/267] Fix matchmaking being permanently sound-ducked (#134) --- .../Screens/MatchmakingIntroScreen.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs index 9a23c963a9..3fabd95e6c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs @@ -131,10 +131,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens { ValidForResume = false; + duckOperation?.Dispose(); + this.FadeOut(800, Easing.OutQuint); base.OnSuspending(e); } + public override bool OnExiting(ScreenExitEvent e) + { + if (base.OnExiting(e)) + return true; + + duckOperation?.Dispose(); + return false; + } + private void updateAnimationState() { if (animationBegan) @@ -168,14 +179,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens RestoreDuration = 1500f, }); }); - - using (BeginDelayedSequence(2750)) - { - Schedule(() => - { - duckOperation?.Dispose(); - }); - } } using (BeginDelayedSequence(1000)) From 48bad312552cb3d51d6505f02c5ce544d4060674 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Sep 2025 20:36:58 +0900 Subject: [PATCH 144/267] Pessimistically set the beatmap in all stages (#135) --- .../Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs index af77306113..832cfa118a 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; @@ -217,12 +216,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void updateGameplayState() { - if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState) - return; - - if (matchmakingState.Stage != MatchmakingStage.WaitingForClientsBeatmapDownload) - return; - MultiplayerPlaylistItem item = client.Room!.CurrentPlaylistItem; RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = ruleset.CreateInstance(); From 31188127efcf8f3b388cd2ed14936763ef38ec30 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 7 Sep 2025 16:02:56 +0900 Subject: [PATCH 145/267] Transmit availability with ready state Regressed with https://github.com/ppy/osu-server-spectator/pull/311. As it turns out, the method not just resets ready states but also the beatmap availabilities. So we have to send it again here. I have a feeling this is also broken in standard multiplayer in some way or another. --- osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs index 832cfa118a..ba2e5593bf 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs @@ -231,6 +231,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); else client.ChangeState(MultiplayerUserState.Ready).FireAndForget(); + + client.ChangeBeatmapAvailability(beatmapAvailabilityTracker.Availability.Value).FireAndForget(); } private void onLoadRequested() => Scheduler.Add(() => From 645518f5bdfa3dec69c995ded2b7816d1ac6cfe6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 7 Sep 2025 21:47:20 +0900 Subject: [PATCH 146/267] Fix external edit filename sanitising unintentionally dropping folder separators (#34945) * Add failing test coverage showing external edits not working with folders * Fix external edit filename sanitising unintentionally dropping folder separators Closes https://github.com/ppy/osu/issues/34929. --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 46 +++++++++++++++++++ .../Database/RealmArchiveModelImporter.cs | 4 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index f909638333..2535d5b2e2 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -294,6 +294,52 @@ namespace osu.Game.Tests.Skins.IO #endregion + [Test] + public async Task TestExternallyMountingWithSubDirectory() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host); + + var zipStream = new MemoryStream(); + using var zip = ZipArchive.Create(); + zip.AddEntry("folder/test.png", new MemoryStream(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF })); + zip.SaveTo(zipStream); + + var import = await loadSkinIntoOsu(osu, new ImportTask(zipStream, "test skin.osk")); + + var skinManager = osu.Dependencies.Get(); + var externalEdit = await skinManager.BeginExternalEditing(import.PerformRead(s => s.Detach())); // should not fail + + Assert.That(Directory.Exists(externalEdit.MountedPath)); + + var directoryInfo = new DirectoryInfo(externalEdit.MountedPath); + + Assert.That(directoryInfo.GetFiles().Select(f => f.Name), Is.EquivalentTo(new[] + { + "skin.ini", + })); + + var subDirectory = directoryInfo.GetDirectories().Single(); + Assert.That(subDirectory.Name, Is.EqualTo("folder")); + Assert.That(subDirectory.GetFiles().Select(f => f.Name), Is.EquivalentTo(new[] + { + "test.png", + })); + + Task finishTask = Task.CompletedTask; + host.UpdateThread.Scheduler.Add(() => finishTask = externalEdit.Finish()); + await finishTask; + } + finally + { + host.Exit(); + } + } + } + /// /// Note that this test passing / failing is platform / OS-specific (if it is to fail, it'll fail on windows). /// diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 0f9832578b..aefb628422 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -217,7 +217,9 @@ namespace osu.Game.Database // to prevent this bricking external edit, strip invalid characters on external edit. // the presumption here is that whatever produced the mangled archive is primarily at fault here, and we're just trying to trudge on locally as best as possible. // if there are further troubles related to similar issues, reevaluate moving this sort of check to the import side instead (sanitising filenames on import from archive). - string destinationPath = Path.Join(mountedPath, realmFile.Filename.GetValidFilename()); + string destinationPath = mountedPath; + foreach (string piece in realmFile.Filename.Split('/').Select(f => f.GetValidFilename())) + destinationPath = Path.Combine(destinationPath, piece); Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); From c2bc67d083be206041e50b2b39dcb9fc9fe2ff36 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 7 Sep 2025 10:23:00 -0700 Subject: [PATCH 147/267] Fix sheared dropdown click sound area --- osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index e365e20ad5..af69aefaaf 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -34,8 +34,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 osuHeader.Dropdown = this; osuHeader.LeftSideLabel = label; } - - AddInternal(new HoverClickSounds()); } public bool OnPressed(KeyBindingPressEvent e) @@ -192,6 +190,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 } }, }; + + AddInternal(new HoverClickSounds()); } [BackgroundDependencyLoader] From 449038d07000f3e9dfed4d29abe2b6aa3f331806 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 8 Sep 2025 13:54:06 +0900 Subject: [PATCH 148/267] Split main menu buttons into multiplayer section (#136) Co-authored-by: Dean Herbert --- .../UserInterface/TestSceneMainMenuButton.cs | 23 ------------- osu.Game/Screens/Menu/ButtonSystem.cs | 34 +++++++++++++++++-- osu.Game/Screens/Menu/MatchmakingButton.cs | 19 ----------- .../Screens/MatchmakingIntroScreen.cs | 2 +- 4 files changed, 32 insertions(+), 46 deletions(-) delete mode 100644 osu.Game/Screens/Menu/MatchmakingButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index 793bc3cd66..86659675a0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -177,28 +177,5 @@ namespace osu.Game.Tests.Visual.UserInterface })); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); } - - [Test] - public void TestMatchmaking() - { - AddStep("add content", () => - { - Children = new Drawable[] - { - new DependencyProvidingContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Child = new MatchmakingButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - ButtonSystemState = ButtonSystemState.TopLevel, - }, - }, - }; - }); - } } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 48d745562c..ea36532db5 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -13,6 +13,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -85,6 +86,7 @@ namespace osu.Game.Screens.Menu private readonly List buttonsTopLevel = new List(); private readonly List buttonsPlay = new List(); + private readonly List buttonsMulti = new List(); private readonly List buttonsEdit = new List(); private Sample? sampleBackToLogo; @@ -110,7 +112,19 @@ namespace osu.Game.Screens.Menu { Padding = new MarginPadding { Right = WEDGE_WIDTH }, }, - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) => State = ButtonSystemState.TopLevel) + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) => + { + switch (State) + { + case ButtonSystemState.Multi: + State = ButtonSystemState.Play; + break; + + default: + State = ButtonSystemState.TopLevel; + break; + } + }) { Padding = new MarginPadding { Right = WEDGE_WIDTH }, VisibleStateMin = ButtonSystemState.Play, @@ -138,12 +152,18 @@ namespace osu.Game.Screens.Menu { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M)); - buttonsPlay.Add(new MatchmakingButton(@"button-default-select", new Color4(94, 63, 186, 255), onMatchmaking, Key.N)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), (_, _) => State = ButtonSystemState.Multi, Key.M)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L)); buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); + buttonsMulti.Add(new MainMenuButton("lounge", @"button-default-select", FontAwesome.Solid.Couch, new Color4(94, 63, 186, 255), onMultiplayer, Key.B) + { + Padding = new MarginPadding { Left = WEDGE_WIDTH } + }); + buttonsMulti.Add(new MainMenuButton("quick play", @"button-default-select", FontAwesome.Solid.Bolt, new Color4(94, 63, 186, 255), onMatchmaking, Key.Q)); + buttonsMulti.ForEach(b => b.VisibleState = ButtonSystemState.Multi); + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, Key.E) { @@ -164,6 +184,7 @@ namespace osu.Game.Screens.Menu if (host.CanExit) buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q)); + buttonArea.AddRange(buttonsMulti); buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsEdit); buttonArea.AddRange(buttonsTopLevel); @@ -331,6 +352,7 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Edit: case ButtonSystemState.Play: + case ButtonSystemState.Multi: StopSamplePlayback(); backButton.TriggerClick(); return true; @@ -343,6 +365,7 @@ namespace osu.Game.Screens.Menu public void StopSamplePlayback() { buttonsPlay.ForEach(button => button.StopSamplePlayback()); + buttonsMulti.ForEach(button => button.StopSamplePlayback()); buttonsTopLevel.ForEach(button => button.StopSamplePlayback()); logo?.StopSamplePlayback(); } @@ -366,6 +389,10 @@ namespace osu.Game.Screens.Menu buttonsPlay.First().TriggerClick(); return false; + case ButtonSystemState.Multi: + buttonsPlay.First().TriggerClick(); + return false; + case ButtonSystemState.Edit: buttonsEdit.First().TriggerClick(); return false; @@ -487,6 +514,7 @@ namespace osu.Game.Screens.Menu Initial, TopLevel, Play, + Multi, Edit, EnteringMode, } diff --git a/osu.Game/Screens/Menu/MatchmakingButton.cs b/osu.Game/Screens/Menu/MatchmakingButton.cs deleted file mode 100644 index b65f08fe03..0000000000 --- a/osu.Game/Screens/Menu/MatchmakingButton.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.Graphics.Sprites; -using osu.Framework.Input.Events; -using osuTK.Graphics; -using osuTK.Input; - -namespace osu.Game.Screens.Menu -{ - public partial class MatchmakingButton : MainMenuButton - { - public MatchmakingButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) - : base("matchmaking", sampleName, FontAwesome.Solid.Newspaper, colour, clickAction, triggerKeys) - { - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs index 3fabd95e6c..8a42712905 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs @@ -96,7 +96,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Matchmaking", + Text = "Quick Play", Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, Shear = -OsuGame.SHEAR, Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), From 06c1b81c1b131a34e6ee9adb8a4ea81701cd59a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Sep 2025 15:12:49 +0900 Subject: [PATCH 149/267] Change debounce method in rank display to allow more immediate updates --- osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs | 11 ++++------- osu.Game/Skinning/LegacyRankDisplay.cs | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index 5ea0e75956..d768fedca4 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -33,11 +33,11 @@ namespace osu.Game.Screens.Play.HUD private SkinnableSound rankUpSample = null!; private Bindable lastSamplePlayback = null!; - private double timeSinceChange; + private double lastChangeTime; private ScoreRank? displayedRank; - private const int time_before_commit = 1500; + private const int time_between_changes = 1500; public DefaultRankDisplay() { @@ -77,12 +77,9 @@ namespace osu.Game.Screens.Play.HUD var currentRank = scoreProcessor.Rank.Value; if (currentRank == displayedRank) - { - timeSinceChange = 0; return; - } - if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value || currentRank == ScoreRank.F) + if (Time.Current - lastChangeTime >= time_between_changes || scoreProcessor.HasCompleted.Value || currentRank == ScoreRank.F) updateRank(currentRank); } @@ -105,7 +102,7 @@ namespace osu.Game.Screens.Play.HUD } displayedRank = rank; - timeSinceChange = 0; + lastChangeTime = Time.Current; } } } diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index 3109f68e9f..64376f0dbd 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -35,11 +35,11 @@ namespace osu.Game.Skinning private SkinnableSound rankUpSample = null!; private Bindable lastSamplePlayback = null!; - private double timeSinceChange; + private double lastChangeTime; private ScoreRank? displayedRank; - private const int time_before_commit = 1500; + private const int time_between_changes = 1500; public LegacyRankDisplay() { @@ -81,12 +81,9 @@ namespace osu.Game.Skinning var currentRank = scoreProcessor.Rank.Value; if (currentRank == displayedRank) - { - timeSinceChange = 0; return; - } - if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value || currentRank == ScoreRank.F) + if (Time.Current - lastChangeTime >= time_between_changes || scoreProcessor.HasCompleted.Value || currentRank == ScoreRank.F) updateRank(currentRank); } @@ -129,7 +126,7 @@ namespace osu.Game.Skinning } displayedRank = rank; - timeSinceChange = 0; + lastChangeTime = Time.Current; } } } From 335bc6cdf6bdb278686f6f97ba9aac1e9893e90b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Sep 2025 14:45:15 +0900 Subject: [PATCH 150/267] Guard against `ArgonJudgementCounterDisplay` crashing due to fuckery I can't see how this can happen in a normal flow, so just doing it as a safety measure. Pointed out in https://github.com/ppy/osu/issues/34940 but likely due to a third party fuck being loaded. --- .../Skinning/Components/ArgonJudgementCounterDisplay.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs b/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs index 62b6d8ecc7..885b3922f2 100644 --- a/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounterDisplay.cs @@ -103,8 +103,13 @@ namespace osu.Game.Skinning.Components private void updateWireframeDigits() { + var visibleCounters = CounterFlow.Children.Where(counter => counter.State.Value == Visibility.Visible).ToArray(); + + if (visibleCounters.Length == 0) + return; + wireframeDigits.Value = FlowDirection.Value == Direction.Vertical - ? Math.Max(2, CounterFlow.Children.Where(counter => counter.State.Value == Visibility.Visible).Max(counter => counter.Result.ResultCount.Value).ToString().Length) + ? Math.Max(2, visibleCounters.Max(counter => counter.Result.ResultCount.Value).ToString().Length) : null; } From f94d5004ea8ab51fb2d6bcdcb85ef3e50dd39ac1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Sep 2025 15:02:57 +0900 Subject: [PATCH 151/267] Adjust BPM filtering at song select to be less precise Closes https://github.com/ppy/osu/issues/34942. --- .../NonVisual/Filtering/FilterQueryParserTest.cs | 13 +++++++++++-- osu.Game/Screens/Select/FilterQueryParser.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 9968647cb2..8bef6b04a7 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -178,6 +178,16 @@ namespace osu.Game.Tests.NonVisual.Filtering [Test] public void TestApplyBPMQueries() + { + const string query = "bpm=200"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(filterCriteria.BPM.Min, 199.5d); + Assert.AreEqual(filterCriteria.BPM.Max, 200.5d); + } + + [Test] + public void TestApplyBPMRangeQueries() { const string query = "bpm>:200 gotta go fast"; var filterCriteria = new FilterCriteria(); @@ -185,8 +195,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim()); Assert.AreEqual(3, filterCriteria.SearchTerms.Length); Assert.IsNotNull(filterCriteria.BPM.Min); - Assert.Greater(filterCriteria.BPM.Min, 199.99d); - Assert.Less(filterCriteria.BPM.Min, 200.00d); + Assert.AreEqual(filterCriteria.BPM.Min, 199.5d); Assert.IsNull(filterCriteria.BPM.Max); } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 7d66a61884..8cf3bda1c5 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select return TryUpdateCriteriaRange(ref criteria.OverallDifficulty, op, value); case "bpm": - return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2); + return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.5f); case "length": return tryUpdateLengthRange(criteria, op, value); From 16594f29868cdc054e77eae983b389fca610398c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Sep 2025 18:01:06 +0900 Subject: [PATCH 152/267] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 32e1b41ad8..a5f2cf1b32 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 6362fed097cc41c8859fd1cfdcab0f6af37256c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Sep 2025 18:02:02 +0900 Subject: [PATCH 153/267] Update framework --- 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 5d9158a45a..498d6f267e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 8e269d292d..4ce5be23bc 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From a436372b05552b4d2e1c3cc6843ea9de67369dbe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 9 Sep 2025 18:09:43 +0900 Subject: [PATCH 154/267] Allow exit during matchmaking intro (#137) --- .../Matchmaking/Screens/MatchmakingIntroScreen.cs | 14 +++++--------- .../Matchmaking/Screens/MatchmakingQueueScreen.cs | 2 ++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs index 8a42712905..34c113c39f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs @@ -37,11 +37,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens [Resolved] private MusicController musicController { get; set; } = null!; - [Resolved] - private MatchmakingController controller { get; set; } = null!; - - public override bool AllowUserExit => !ValidForResume; - private Sample? dateWindupSample; private Sample? dateImpactSample; private Sample? beatmapWindupSample; @@ -56,6 +51,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens protected override BackgroundScreen CreateBackground() => new MatchmakingIntroBackgroundScreen(colourProvider); + public MatchmakingIntroScreen() + { + ValidForResume = false; + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { @@ -123,14 +123,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens updateAnimationState(); playDateWindupSample(); - - controller.SearchInForeground(); } public override void OnSuspending(ScreenTransitionEvent e) { - ValidForResume = false; - duckOperation?.Dispose(); this.FadeOut(800, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs index f906a0e06f..8ec1505c1b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs @@ -169,6 +169,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens { base.OnEntering(e); + controller.SearchInForeground(); + client.MatchmakingJoinLobby().FireAndForget(); using (BeginDelayedSequence(800)) From 2bd734918af6b78723e56fefa904e7cd8938767c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Sep 2025 19:11:50 +0900 Subject: [PATCH 155/267] Adjust song select debounce to be high than `repeat_initial_delay` I'm doing this silently to see if any users complain without being told about the change. Without this, when holding left arrow for short bursts, precisely *one* beatmap change happens before actual key repeat kicks in, which feels really weird (updates the leaderboard / background unexpectedly). This is the simplest way to resolve the issue, so if users aren't offended by it I think we should commit to it. Personally it's still fast enough to not annoy me at all. --- osu.Game/Screens/SelectV2/SongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index cc8c6afec2..e8d6a8d2ac 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -68,9 +68,9 @@ namespace osu.Game.Screens.SelectV2 /// A debounce that governs how long after a panel is selected before the rest of song select (and the game at large) /// updates to show that selection. /// - /// This is intentionally slightly higher than key repeat, but low enough to not impede user experience. + /// This is intentionally slightly higher than initial key repeat, but low enough to not impede user experience. /// - public const int SELECTION_DEBOUNCE = 150; + public const int SELECTION_DEBOUNCE = 260; /// /// A general "global" debounce to be applied to anything aggressive difficulty calculation at song select, From a6e42fb0cb97a127208dc8b598c23591a0c4a9ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 9 Sep 2025 20:26:41 +0900 Subject: [PATCH 156/267] Fix mangled initial undo state on fresh skins --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 11 +++++++++-- osu.Game/Skinning/SkinnableContainer.cs | 13 +++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index f4a1bb7562..823456dddd 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -391,10 +391,10 @@ namespace osu.Game.Overlays.SkinEditor return; } - if (skinComponentsContainer.IsLoaded) + if (skinComponentsContainer.ComponentsLoaded) bindChangeHandler(skinComponentsContainer); else - skinComponentsContainer.OnLoadComplete += d => Schedule(() => bindChangeHandler((SkinnableContainer)d)); + skinComponentsContainer.OnComponentsLoaded += onComponentsLoaded; content.Child = new SkinBlueprintContainer(skinComponentsContainer); @@ -428,6 +428,13 @@ namespace osu.Game.Overlays.SkinEditor RequestPlacement = requestPlacement }); + void onComponentsLoaded(Drawable d) + { + SkinnableContainer container = (SkinnableContainer)d; + container.OnComponentsLoaded -= onComponentsLoaded; + Schedule(() => bindChangeHandler(container)); + } + void requestPlacement(Type type) { if (!(Activator.CreateInstance(type) is ISerialisableDrawable component)) diff --git a/osu.Game/Skinning/SkinnableContainer.cs b/osu.Game/Skinning/SkinnableContainer.cs index aad95ca779..720699e708 100644 --- a/osu.Game/Skinning/SkinnableContainer.cs +++ b/osu.Game/Skinning/SkinnableContainer.cs @@ -21,6 +21,11 @@ namespace osu.Game.Skinning /// public partial class SkinnableContainer : SkinReloadableDrawable, ISerialisableDrawableContainer { + /// + /// Invoked when the skinnable components of this container finish loading. + /// + public event Action? OnComponentsLoaded; + private Container? content; /// @@ -67,6 +72,7 @@ namespace osu.Game.Skinning AddInternal(wrapper); components.AddRange(wrapper.Children.OfType()); ComponentsLoaded = true; + OnComponentsLoaded?.Invoke(this); }, (cancellationSource = new CancellationTokenSource()).Token); } @@ -106,5 +112,12 @@ namespace osu.Game.Skinning Reload(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + OnComponentsLoaded = null; + } } } From 0cd3894fa6235bfaef67ed5f595f5dd7e7aec0b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Sep 2025 01:31:15 +0900 Subject: [PATCH 157/267] Fix multiple failing song select tests --- osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 553205d400..945ec5d207 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; @@ -310,7 +311,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestFilteringRunsAfterReturningFromGameplay() { - AddStep("import actual beatmap", () => Beatmaps.Import(TestResources.GetQuickTestBeatmapForImport())); + AddStep("import actual beatmap", () => Beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely()); + LoadSongSelect(); AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType().Single().FilterCount, () => Is.EqualTo(1)); @@ -590,7 +592,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 LoadSongSelect(); ImportBeatmapForRuleset(0); - AddAssert("options enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddUntilStep("options enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("click", () => this.ChildrenOfType().Single().TriggerClick()); AddUntilStep("popover displayed", () => this.ChildrenOfType().Any(p => p.IsPresent)); @@ -647,7 +649,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ImportBeatmapForRuleset(0); - AddAssert("options enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddUntilStep("options enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("delete all beatmaps", () => Beatmaps.Delete()); AddAssert("beatmap selected", () => !Beatmap.IsDefault); From 014b55602dfe526d87e251b8236cbdaeec2010b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Sep 2025 02:41:27 +0900 Subject: [PATCH 158/267] Fix one more failing test --- .../Visual/SongSelectV2/TestSceneSongSelectFiltering.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 16ad970239..076d84479a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -302,7 +302,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkMatchedBeatmaps(2); - AddAssert("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.Not.EqualTo(hiddenBeatmap)); + AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.Not.EqualTo(hiddenBeatmap)); AddStep("restore", () => Beatmaps.Restore(hiddenBeatmap!)); From b3abd09517a592f5b1457bad9fcabeef96f0a16d Mon Sep 17 00:00:00 2001 From: CloneWith Date: Wed, 10 Sep 2025 07:48:09 +0800 Subject: [PATCH 159/267] Add HumanisedLocalisableDate for l10n --- osu.Game/Graphics/DrawableDate.cs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 641a4d80ce..a5171388c5 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -72,12 +72,33 @@ namespace osu.Game.Graphics Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate); } - protected virtual LocalisableString Format() => HumanizerUtils.Humanize(Date); + protected virtual LocalisableString Format() => new LocalisableString(new HumanisedLocalisableDate(Date)); private void updateTime() => Text = Format(); public ITooltip GetCustomTooltip() => new DateTooltip(); public DateTimeOffset TooltipContent => Date; + + private class HumanisedLocalisableDate : IEquatable, ILocalisableStringData + { + public readonly DateTimeOffset Date; + + public HumanisedLocalisableDate(DateTimeOffset date) + { + Date = date; + } + + public bool Equals(HumanisedLocalisableDate? other) + => other?.Date != null && Date.Equals(other.Date); + + public bool Equals(ILocalisableStringData? other) + => other is HumanisedLocalisableDate otherDate && Equals(otherDate); + + public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date); + + // Override for default string interpolations + public override string ToString() => HumanizerUtils.Humanize(Date); + } } } From c8c87089e58fa67ef7bd334eb9d0da4a92d2e579 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Wed, 10 Sep 2025 07:49:14 +0800 Subject: [PATCH 160/267] Use LocalisableString interpolation to make strings update properly --- osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 149b0a25d8..848d510826 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -266,8 +266,8 @@ namespace osu.Game.Tournament.Screens.Schedule } protected override LocalisableString Format() => Date < DateTimeOffset.Now - ? $"Started {base.Format()}" - : $"Starting {base.Format()}"; + ? LocalisableString.Interpolate($"Started {base.Format()}") + : LocalisableString.Interpolate($"Starting {base.Format()}"); } public partial class ScheduleContainer : Container diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs index 86a79ef0d6..4a98efb225 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components var diffToNow = Date.Subtract(DateTimeOffset.Now); if (diffToNow.TotalSeconds < -5) - return $"Closed {base.Format()}"; + return LocalisableString.Interpolate($"Closed {base.Format()}"); if (diffToNow.TotalSeconds < 0) return "Closed"; @@ -79,7 +79,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (diffToNow.TotalSeconds < 5) return "Closing soon"; - return $"Closing {base.Format()}"; + return LocalisableString.Interpolate($"Closing {base.Format()}"); } protected override void Dispose(bool isDisposing) From 482b7b6d3f2a66a8f48cff7e97e05651bf795765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Sep 2025 15:33:46 +0900 Subject: [PATCH 161/267] Change class name I suggested it myself but on revisiting it's a bit of a mouthful. --- osu.Game/Graphics/DrawableDate.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index a5171388c5..aa00a76bc3 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -72,7 +72,7 @@ namespace osu.Game.Graphics Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate); } - protected virtual LocalisableString Format() => new LocalisableString(new HumanisedLocalisableDate(Date)); + protected virtual LocalisableString Format() => new LocalisableString(new HumanisedDate(Date)); private void updateTime() => Text = Format(); @@ -80,20 +80,20 @@ namespace osu.Game.Graphics public DateTimeOffset TooltipContent => Date; - private class HumanisedLocalisableDate : IEquatable, ILocalisableStringData + private class HumanisedDate : IEquatable, ILocalisableStringData { public readonly DateTimeOffset Date; - public HumanisedLocalisableDate(DateTimeOffset date) + public HumanisedDate(DateTimeOffset date) { Date = date; } - public bool Equals(HumanisedLocalisableDate? other) + public bool Equals(HumanisedDate? other) => other?.Date != null && Date.Equals(other.Date); public bool Equals(ILocalisableStringData? other) - => other is HumanisedLocalisableDate otherDate && Equals(otherDate); + => other is HumanisedDate otherDate && Equals(otherDate); public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date); From 6660406ee9555e68c036794ddd4d2565c7adee2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Sep 2025 15:34:18 +0900 Subject: [PATCH 162/267] Change `ToString()` override to match pre-existing conventions --- osu.Game/Graphics/DrawableDate.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index aa00a76bc3..7af4df2d25 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -97,8 +97,7 @@ namespace osu.Game.Graphics public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date); - // Override for default string interpolations - public override string ToString() => HumanizerUtils.Humanize(Date); + public override string ToString() => GetLocalised(LocalisationParameters.DEFAULT); } } } From 278b232318fe48f8fb38cfa53c258e6c944d51c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Sep 2025 16:19:47 +0900 Subject: [PATCH 163/267] Fix realm not being cached in beatmap carousel tests --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index a180097863..02c017f570 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -72,6 +72,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Scheduler.AddDelayed(updateStats, 100, true); } + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(Realm); + } + protected void CreateCarousel() { AddStep("create components", () => From fa6b830a13f00a47285c2debd254f8df37ebbd59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Sep 2025 02:36:29 +0900 Subject: [PATCH 164/267] Add test coverage showing selection not being held post-filter when difficulties are being split out --- .../TestSceneBeatmapCarouselUpdateHandling.cs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index 86ef2cffba..17f328b549 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -76,12 +76,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("drawables unchanged", () => Carousel.ChildrenOfType(), () => Is.EqualTo(originalDrawables)); } - [Test] - public void TestScrollPositionMaintainedWhenSetUpdated() + [TestCase(true)] + [TestCase(false)] + public void TestScrollPositionMaintainedWhenSetUpdated(bool difficultySort) { - PanelBeatmapSet panel = null!; + if (difficultySort) + { + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); + assertDidFilter(1); + } - AddStep("find panel", () => panel = Carousel.ChildrenOfType().Single(p => p.ChildrenOfType().Any(t => t.Text.ToString() == "beatmap"))); + Panel panel = null!; + + AddStep("find panel", () => panel = Carousel.ChildrenOfType().First(p => p.Item != null && p.ChildrenOfType().Any(t => t.Text.ToString() == "beatmap"))); AddStep("select panel", () => panel.TriggerClick()); @@ -105,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }; }); - assertDidFilter(); + assertDidFilter(difficultySort ? 2 : 1); WaitForFiltering(); AddAssert("scroll is still at end", () => Carousel.ChildrenOfType().Single().IsScrolledToEnd()); @@ -180,12 +187,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 assertDidNotFilter(); } - [TestCase(false)] - [TestCase(true)] - public void TestSelectionHeld(bool hashChanged) + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void TestSelectionHeld(bool difficultySort, bool hashChanged) { SelectNextSet(); + if (difficultySort) + { + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); + assertDidFilter(1); + } + WaitForSetSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -196,10 +211,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 b.Hash = "new hash"; }); + int baseFilterCount = difficultySort ? 1 : 0; + if (hashChanged) - assertDidFilter(); + assertDidFilter(baseFilterCount + 1); else - assertDidNotFilter(); + assertDidFilter(baseFilterCount); WaitForFiltering(); @@ -413,7 +430,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder)); } - private void assertDidFilter() => AddAssert("did filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count + 1)); + private void assertDidFilter(int count = 1) => AddAssert("did filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count + count)); private void assertDidNotFilter() => AddAssert("did not filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count)); From 699892c6a2347e17d8ca75e1a07879134982fae2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Sep 2025 02:36:24 +0900 Subject: [PATCH 165/267] Fix beatmap carousel not holding selection after refilter in some cases Closes https://github.com/ppy/osu/issues/34923. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index bcac74662e..52d5989c8f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -261,9 +261,10 @@ namespace osu.Game.Screens.SelectV2 // TODO: should this exist in song select instead of here? // we need to ensure the global beatmap is also updated alongside changes. if (CurrentBeatmap != null && beatmap.Equals(CurrentBeatmap)) - // we don't know in which group the matching new beatmap is, but that's fine - we can leave it null for now. - // we are about to modify `Items`, which will trigger a re-filter, which will pick a correct group - if one is present - via `HandleFilterCompleted()`. - RequestSelection(new GroupedBeatmap(null, matchingNewBeatmap)); + // we don't know in which group the matching new beatmap is, but that's fine - we can keep the previous one for now. + // we are about to modify `Items`, which - if required - will trigger a re-filter, + // which will pick a correct group - if one is present - via `HandleFilterCompleted()`. + RequestSelection(new GroupedBeatmap(CurrentGroupedBeatmap?.Group, matchingNewBeatmap)); Items.ReplaceRange(previousIndex, 1, [matchingNewBeatmap]); newSetBeatmaps.Remove(matchingNewBeatmap); From 1ea17129cc3c2fdfeb340093a2b6f114fd1804c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Sep 2025 13:25:24 +0900 Subject: [PATCH 166/267] Adjust debounce again to handle key down state --- osu.Game/Screens/SelectV2/SongSelect.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index e8d6a8d2ac..9947ffc6bc 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -21,6 +21,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; @@ -68,9 +69,9 @@ namespace osu.Game.Screens.SelectV2 /// A debounce that governs how long after a panel is selected before the rest of song select (and the game at large) /// updates to show that selection. /// - /// This is intentionally slightly higher than initial key repeat, but low enough to not impede user experience. + /// This is intentionally slightly higher than key repeat, but low enough to not impede user experience. /// - public const int SELECTION_DEBOUNCE = 260; + public const int SELECTION_DEBOUNCE = 150; /// /// A general "global" debounce to be applied to anything aggressive difficulty calculation at song select, @@ -149,6 +150,8 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + private InputManager inputManager = null!; + private readonly RealmPopulatingOnlineLookupSource onlineLookupSource = new RealmPopulatingOnlineLookupSource(); private Bindable configBackgroundBlur = null!; @@ -345,6 +348,8 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); + inputManager = GetContainingInputManager()!; + filterControl.CriteriaChanged += criteriaChanged; modSelectOverlay.State.BindValueChanged(v => @@ -405,13 +410,17 @@ namespace osu.Game.Screens.SelectV2 double elapsed = Clock.ElapsedFrameTime; + // When a key is being held, assume the user is traversing the carousel using key repeat. + // We want to change panels less often in this state (basically making debounce longer than initial key repeat, at least). + double debounceInterval = inputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed ? SELECTION_DEBOUNCE * 2 : SELECTION_DEBOUNCE; + // avoid debounce running early if there's a single long frame. if (!DebugUtils.IsNUnitRunning && Clock.FramesPerSecond > 0) elapsed = Math.Min(1000 / Clock.FramesPerSecond, elapsed); debounceElapsedTime += elapsed; - if (debounceElapsedTime >= SELECTION_DEBOUNCE) + if (debounceElapsedTime >= debounceInterval) performDebounceSelection(); } From 83c6e579840b1d40e7bce5539d04b22223e48248 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Sep 2025 15:00:21 +0900 Subject: [PATCH 167/267] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2f6c15de30..122a927abe 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 69a0ac6c76268f951553750d737eedabc5dc6c7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Sep 2025 16:00:41 +0900 Subject: [PATCH 168/267] For tachyon release From 0c68a91b4c208044cc3499fef5fb4a7fbd9fbff3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Sep 2025 15:52:24 +0900 Subject: [PATCH 169/267] Fix main menu key tests --- .../UserInterface/TestSceneButtonSystem.cs | 32 +++++++++++-------- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index d8baca6d23..ba5cc56f34 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -68,14 +68,15 @@ namespace osu.Game.Tests.Visual.UserInterface } [TestCase(Key.P, Key.P)] - [TestCase(Key.M, Key.P)] - [TestCase(Key.L, Key.P)] - [TestCase(Key.B, Key.E)] - [TestCase(Key.S, Key.E)] - [TestCase(Key.D, null)] - [TestCase(Key.Q, null)] - [TestCase(Key.O, null)] - public void TestShortcutKeys(Key key, Key? subMenuEnterKey) + [TestCase(Key.M, Key.M, Key.L)] + [TestCase(Key.M, Key.M, Key.M)] + [TestCase(Key.L, Key.L)] + [TestCase(Key.B, Key.E, Key.B)] + [TestCase(Key.S, Key.E, Key.S)] + [TestCase(Key.D)] + [TestCase(Key.Q)] + [TestCase(Key.O)] + public void TestShortcutKeys(params Key[] keys) { int activationCount = -1; AddStep("set up action", () => @@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface activationCount = 0; void action() => activationCount++; - switch (key) + switch (keys.First()) { case Key.P: buttons.OnSolo = action; @@ -119,16 +120,19 @@ namespace osu.Game.Tests.Visual.UserInterface } }); - AddStep($"press {key}", () => InputManager.Key(key)); + // trigger out of idle state + AddStep($"press {keys.First()}", () => InputManager.Key(keys.First())); AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel); - if (subMenuEnterKey != null) + for (int i = 0; i < keys.Length; i++) { - AddStep($"press {subMenuEnterKey}", () => InputManager.Key(subMenuEnterKey.Value)); - AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel); + var key = keys[i]; + AddStep($"press {key}", () => InputManager.Key(key)); + + if (i > 0) + AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel); } - AddStep($"press {key}", () => InputManager.Key(key)); AddAssert("action triggered", () => activationCount == 1); } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index ea36532db5..46a98dd5da 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -157,7 +157,7 @@ namespace osu.Game.Screens.Menu buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsMulti.Add(new MainMenuButton("lounge", @"button-default-select", FontAwesome.Solid.Couch, new Color4(94, 63, 186, 255), onMultiplayer, Key.B) + buttonsMulti.Add(new MainMenuButton("lounge", @"button-default-select", FontAwesome.Solid.Couch, new Color4(94, 63, 186, 255), onMultiplayer, Key.L, Key.M) { Padding = new MarginPadding { Left = WEDGE_WIDTH } }); From bcff6be5f61fcd42ab9eb1fb3a040461d1e98d10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Sep 2025 16:21:03 +0900 Subject: [PATCH 170/267] Add temporary workaround for rider bug --- .editorconfig | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7aecde95ee..e882357691 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,14 +20,14 @@ indent_size = 4 trim_trailing_whitespace = true #license header -file_header_template = Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text. +file_header_template = Copyright (c) ppy Pty Ltd .Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text. #Roslyn naming styles #PascalCase for public and protected members dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event +dotnet_naming_symbols.public_members.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.public_members.applicable_kinds = property, method, field, event dotnet_naming_rule.public_members_pascalcase.severity = error dotnet_naming_rule.public_members_pascalcase.symbols = public_members dotnet_naming_rule.public_members_pascalcase.style = pascalcase @@ -36,7 +36,7 @@ dotnet_naming_rule.public_members_pascalcase.style = pascalcase dotnet_naming_style.camelcase.capitalization = camel_case dotnet_naming_symbols.private_members.applicable_accessibilities = private -dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event +dotnet_naming_symbols.private_members.applicable_kinds = property, method, field, event dotnet_naming_rule.private_members_camelcase.severity = warning dotnet_naming_rule.private_members_camelcase.symbols = private_members dotnet_naming_rule.private_members_camelcase.style = camelcase @@ -58,7 +58,7 @@ dotnet_naming_rule.private_const_all_lower.symbols = private_constants dotnet_naming_rule.private_const_all_lower.style = all_lower dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private -dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly.required_modifiers = static, readonly dotnet_naming_symbols.private_static_readonly.applicable_kinds = field dotnet_naming_rule.private_static_readonly_all_lower.severity = warning dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly @@ -74,15 +74,15 @@ dotnet_naming_rule.local_const_all_lower.style = all_lower dotnet_naming_style.all_upper.capitalization = all_upper dotnet_naming_style.all_upper.word_separator = _ -dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_constants.applicable_accessibilities = public, internal, protected, protected_internal, private_protected dotnet_naming_symbols.public_constants.required_modifiers = const dotnet_naming_symbols.public_constants.applicable_kinds = field dotnet_naming_rule.public_const_all_upper.severity = warning dotnet_naming_rule.public_const_all_upper.symbols = public_constants dotnet_naming_rule.public_const_all_upper.style = all_upper -dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.public_static_readonly.required_modifiers = static, readonly dotnet_naming_symbols.public_static_readonly.applicable_kinds = field dotnet_naming_rule.public_static_readonly_all_upper.severity = warning dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly @@ -140,7 +140,7 @@ csharp_style_var_elsewhere = true:silent #Style - modifiers dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning -csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:warning #Style - parentheses # Skipped because roslyn cannot separate +-*/ with << >> @@ -206,4 +206,5 @@ indent_size = 2 trim_trailing_whitespace = true dotnet_diagnostic.OLOC001.words_in_name = 5 -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. +dotnet_diagnostic.OLOC001.license_header = +// Copyright (c) ppy Pty Ltd .Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. From e5fbf62ff55058babf822b9c59ebe2e72bad3503 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Sep 2025 16:21:40 +0900 Subject: [PATCH 171/267] Revert "Add temporary workaround for rider bug" This reverts commit bcff6be5f61fcd42ab9eb1fb3a040461d1e98d10. --- .editorconfig | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.editorconfig b/.editorconfig index e882357691..7aecde95ee 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,14 +20,14 @@ indent_size = 4 trim_trailing_whitespace = true #license header -file_header_template = Copyright (c) ppy Pty Ltd .Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text. +file_header_template = Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text. #Roslyn naming styles #PascalCase for public and protected members dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public, internal, protected, protected_internal, private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property, method, field, event +dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event dotnet_naming_rule.public_members_pascalcase.severity = error dotnet_naming_rule.public_members_pascalcase.symbols = public_members dotnet_naming_rule.public_members_pascalcase.style = pascalcase @@ -36,7 +36,7 @@ dotnet_naming_rule.public_members_pascalcase.style = pascalcase dotnet_naming_style.camelcase.capitalization = camel_case dotnet_naming_symbols.private_members.applicable_accessibilities = private -dotnet_naming_symbols.private_members.applicable_kinds = property, method, field, event +dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event dotnet_naming_rule.private_members_camelcase.severity = warning dotnet_naming_rule.private_members_camelcase.symbols = private_members dotnet_naming_rule.private_members_camelcase.style = camelcase @@ -58,7 +58,7 @@ dotnet_naming_rule.private_const_all_lower.symbols = private_constants dotnet_naming_rule.private_const_all_lower.style = all_lower dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private -dotnet_naming_symbols.private_static_readonly.required_modifiers = static, readonly +dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly dotnet_naming_symbols.private_static_readonly.applicable_kinds = field dotnet_naming_rule.private_static_readonly_all_lower.severity = warning dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly @@ -74,15 +74,15 @@ dotnet_naming_rule.local_const_all_lower.style = all_lower dotnet_naming_style.all_upper.capitalization = all_upper dotnet_naming_style.all_upper.word_separator = _ -dotnet_naming_symbols.public_constants.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected dotnet_naming_symbols.public_constants.required_modifiers = const dotnet_naming_symbols.public_constants.applicable_kinds = field dotnet_naming_rule.public_const_all_upper.severity = warning dotnet_naming_rule.public_const_all_upper.symbols = public_constants dotnet_naming_rule.public_const_all_upper.style = all_upper -dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public, internal, protected, protected_internal, private_protected -dotnet_naming_symbols.public_static_readonly.required_modifiers = static, readonly +dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly dotnet_naming_symbols.public_static_readonly.applicable_kinds = field dotnet_naming_rule.public_static_readonly_all_upper.severity = warning dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly @@ -140,7 +140,7 @@ csharp_style_var_elsewhere = true:silent #Style - modifiers dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning -csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:warning +csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning #Style - parentheses # Skipped because roslyn cannot separate +-*/ with << >> @@ -206,5 +206,4 @@ indent_size = 2 trim_trailing_whitespace = true dotnet_diagnostic.OLOC001.words_in_name = 5 -dotnet_diagnostic.OLOC001.license_header = -// Copyright (c) ppy Pty Ltd .Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. From 644b7977343e6d08a990e33d8b91a1b5ab662c39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Sep 2025 16:21:55 +0900 Subject: [PATCH 172/267] Add temporary workaround for rider bug (attempt 2) --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index 7aecde95ee..e42b8b6a8a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,6 +19,9 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true +# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references +resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint + #license header file_header_template = Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text. From 9b9e7a8f7512c070b256a97bc60fd8318c1beef7 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 12 Sep 2025 18:23:02 +0900 Subject: [PATCH 173/267] Refactor selection roulette SFX logic --- .../Screens/Pick/BeatmapSelectionGrid.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs index 66cae72616..813e8efa0d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs @@ -45,8 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick private bool allowSelection = true; - private readonly Sample[] rouletteSamples = new Sample[8]; - private Sample? rouletteResultSample; + private readonly Sample?[] spinSamples = new Sample?[5]; + private static readonly int[] spin_sample_sequence = [0, 1, 2, 3, 4, 2, 3, 4]; + private Sample? resultSample; private Sample? swooshSample; private double? lastSamplePlayback; @@ -77,15 +78,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick [BackgroundDependencyLoader] private void load(AudioManager audio) { - rouletteSamples[0] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-0"); - rouletteSamples[1] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-1"); - rouletteSamples[2] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-2"); - rouletteSamples[3] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-3"); - rouletteSamples[4] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-4"); - rouletteSamples[5] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-2"); - rouletteSamples[6] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-3"); - rouletteSamples[7] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-4"); - rouletteResultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-result"); + for (int i = 0; i < spinSamples.Length; i++) + spinSamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Selection/roulette-{i}"); + + resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result"); swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); } @@ -306,8 +302,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) { - int sampleIdx = ii % (rouletteSamples.Length); - rouletteSamples[sampleIdx].Play(); + int sequenceIdx = ii % spin_sample_sequence.Length; + spinSamples[spin_sample_sequence[sequenceIdx]]?.Play(); lastSamplePlayback = Time.Current; } @@ -338,7 +334,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) .ScaleTo(1.5f, 1000, Easing.OutExpo); - rouletteResultSample?.Play(); + resultSample?.Play(); }); } } From ccc5ca5d806b7a798101efba35a5b2de7ff892f6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 12 Sep 2025 18:25:08 +0900 Subject: [PATCH 174/267] Rework matchmaking cloud SFX --- osu.Game/Configuration/SessionStatics.cs | 8 ---- .../Matchmaking/MatchmakingCloud.cs | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 0c0b2a989d..59e107a23e 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -8,7 +8,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Users; @@ -29,7 +28,6 @@ namespace osu.Game.Configuration SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.LastRankChangeSamplePlaybackTime, (double?)null); - SetDefault(Static.LastMatchmakingCloudSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); SetDefault(Static.LastLocalUserScore, null); @@ -83,12 +81,6 @@ namespace osu.Game.Configuration /// LastRankChangeSamplePlaybackTime, - /// - /// The last playback time in milliseconds for the 'user appear' sample in . - /// Used to debounce sample playback to avoid volume saturation from multiple simultaneous playback. - /// - LastMatchmakingCloudSamplePlaybackTime, - /// /// Whether the last positional input received was a touch input. /// Used in touchscreen detection scenarios (). diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs index d2b2b72f02..3fab5ab207 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; -using osu.Game.Configuration; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Ranking; using osuTK; @@ -22,6 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private APIUser[] users = []; private Container usersContainer = null!; + private readonly Bindable lastSamplePlayback = new Bindable(); + public APIUser[] Users { get => users; @@ -32,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking foreach (var u in usersContainer) u.Delay(RNG.Next(0, 1000)).FadeOut(500).Expire(); - LoadComponentsAsync(users.Select(u => new MovingAvatar(u)), avatars => + LoadComponentsAsync(users.Select(u => new MovingAvatar(u, lastSamplePlayback)), avatars => { if (usersContainer.Count == 0) { @@ -69,24 +70,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private float targetScale; private float targetAlpha; - private Bindable lastSamplePlaybackTime = null!; + private readonly Bindable lastSamplePlayback = new Bindable(); + private const int num_appear_samples = 6; private Sample? playerAppearSample; - public MovingAvatar(APIUser apiUser) + public MovingAvatar(APIUser apiUser, Bindable lastSamplePlayback) : base(apiUser) { RelativePositionAxes = Axes.Both; Scale = new Vector2(2); Origin = Anchor.Centre; + this.lastSamplePlayback.BindTo(lastSamplePlayback); } [BackgroundDependencyLoader] - private void load(AudioManager audio, SessionStatics statics) + private void load(AudioManager audio) { - playerAppearSample = audio.Samples.Get(@"UI/toolbar-hover"); - lastSamplePlaybackTime = statics.GetBindable(Static.LastMatchmakingCloudSamplePlaybackTime); + playerAppearSample = audio.Samples.Get($@"Multiplayer/Matchmaking/Cloud/appear-{RNG.Next(0, num_appear_samples)}"); } protected override void LoadComplete() @@ -103,20 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Hide(); int appearDelay = RNG.Next(0, 1000); this.Delay(appearDelay).FadeTo(targetAlpha, 2000, Easing.OutQuint); - Scheduler.AddDelayed(() => - { - bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; - if (!enoughTimeElapsed) return; - - var chan = playerAppearSample?.GetChannel(); - - if (chan == null) return; - - chan.Frequency.Value = 1f + RNG.NextDouble(0.25f); - chan.Play(); - - lastSamplePlaybackTime.Value = Time.Current; - }, appearDelay); + Scheduler.AddDelayed(playAppearSample, appearDelay); } private void updateParams() @@ -128,6 +117,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Scheduler.AddDelayed(updateParams, RNG.Next(500, 5000)); } + private void playAppearSample() + { + bool enoughTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + if (!enoughTimeElapsed) return; + + var chan = playerAppearSample?.GetChannel(); + if (chan == null) return; + + chan.Frequency.Value = 0.5f + RNG.NextDouble(1.5f); + chan.Balance.Value = MathF.Cos(angle) * OsuGameBase.SFX_STEREO_STRENGTH; + chan.Play(); + + lastSamplePlayback.Value = Time.Current; + } + protected override void Update() { base.Update(); From 9a2513230cc3661898b6aa8212856ede5741f115 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 12 Sep 2025 18:27:16 +0900 Subject: [PATCH 175/267] Add SFX for stage progression feedback --- .../OnlinePlay/Matchmaking/StageBubble.cs | 16 +++++++++++++++- .../Screens/OnlinePlay/Matchmaking/StageText.cs | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs index 281374ba71..2ebd3376d3 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -31,6 +33,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private DateTimeOffset countdownStartTime; private DateTimeOffset countdownEndTime; + private Sample? stageProgressSample; + private double? lastSamplePlayback; + public StageBubble(MatchmakingStage stage, LocalisableString displayText) { this.stage = stage; @@ -40,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { InternalChild = new CircularContainer { @@ -68,6 +73,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } } }; + + stageProgressSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); } protected override void LoadComplete() @@ -98,6 +105,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; progressBar.Width = (float)(elapsed.TotalMilliseconds / duration.TotalMilliseconds); + + bool enoughTimeElapsed = lastSamplePlayback == null || Time.Current - lastSamplePlayback >= 1000f; + if (elapsed.TotalMilliseconds < 1000f || !enoughTimeElapsed || elapsed.TotalMilliseconds >= duration.TotalMilliseconds) + return; + + stageProgressSample?.Play(); + lastSamplePlayback = Time.Current; } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs index ab2627474e..b47e135004 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,13 +22,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private OsuSpriteText text = null!; + private Sample? textChangedSample; + private double? lastSamplePlayback; + public StageText() { AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { InternalChild = text = new OsuSpriteText { @@ -34,6 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Font = OsuFont.Default, AlwaysPresent = true, }; + + textChangedSample = audio.Samples.Get(@"Multiplayer/Matchmaking/stage-message"); } protected override void LoadComplete() @@ -50,6 +57,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking return; text.Text = getTextForStatus(matchmakingState.Stage); + + if (text.Text == string.Empty || (lastSamplePlayback != null && Time.Current - lastSamplePlayback < OsuGameBase.SAMPLE_DEBOUNCE_TIME)) + return; + + textChangedSample?.Play(); + lastSamplePlayback = Time.Current; }); private LocalisableString getTextForStatus(MatchmakingStage status) From a9c021ce04c45ee2c7d0172d2ae6a8aedcc7be87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Sep 2025 10:50:33 +0900 Subject: [PATCH 176/267] Demonstrate failure in test --- .../Visual/Gameplay/TestSceneArgonJudgementCounter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs index e08af79032..e5886aa607 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs @@ -183,6 +183,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All); AddWaitStep("wait some", 2); AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 1); + AddToggleStep("toggle wireframe display", t => counterDisplay.WireframeOpacity.Value = t ? 0.3f : 0); + AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical); + AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal); } private int hiddenCount() From e73e9275baeeaa024f3e3401309b7e56a5029533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Sep 2025 11:11:45 +0900 Subject: [PATCH 177/267] Fix argon judgement counter looking misaligned with wireframe off Closes https://github.com/ppy/osu/issues/34959. `ArgonCounterTextComponent` is pretty terrible and prevents being able to fix the issue easily. The core issue is that this is the first instance of the component's usage where the label text can be longer than the counter in the X dimension, so the total width of any counter is equal to max(label width, counter width), and the label will be aligned to the left of that width, while the counter will be aligned to the right of that width. The fix sort of relies on the fact that I don't expect *any* consumer of `ArgonCounterTextComponent` that meaningfully uses the wireframe digits to want the non-wireframe digits to be aligned to the *left* rather than the right. It's not what I'd expect any segmented display to work. (There are usages that specify `TopLeft` anchor, but they usually display the same number of wireframe and non-wireframe digits, so for them it doesn't really matter if the digits are left-aligned to the wireframes or not.) --- osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs | 8 ++++---- osu.Game/Skinning/Components/ArgonJudgementCounter.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 3789fb1645..d55bf46f97 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -74,13 +74,13 @@ namespace osu.Game.Screens.Play.HUD { wireframesPart = new ArgonCounterSpriteText(wireframesLookup) { - Anchor = anchor, - Origin = anchor, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, }, textPart = new ArgonCounterSpriteText(textLookup) { - Anchor = anchor, - Origin = anchor, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, }, } } diff --git a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs index 6fe8ac7ecd..84973aab3e 100644 --- a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs @@ -37,7 +37,7 @@ namespace osu.Game.Skinning.Components Result = result; AutoSizeAxes = Axes.Both; - AddInternal(textComponent = new ArgonCounterTextComponent(Anchor.TopRight, result.DisplayName.ToUpper())); + AddInternal(textComponent = new ArgonCounterTextComponent(Anchor.TopLeft, result.DisplayName.ToUpper())); } private void updateWireframe() From e34b0659da8d7dcdd182151327ac4c56aef6a5bd Mon Sep 17 00:00:00 2001 From: Valerus9 Date: Sat, 13 Sep 2025 06:44:29 +0200 Subject: [PATCH 178/267] Fix testtooltip failure --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index d5fc1363bb..1950f8b66e 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Localisation; @@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Mods get { if (!SpeedChange.IsDefault) - yield return ("Speed change", $"{SpeedChange.Value:N2}x"); + yield return ("Speed change", $"{SpeedChange.Value.ToString("N2", CultureInfo.InvariantCulture)}x"); } } From 4ccfebe8424c4cd7912b9b08f274ee79284cef56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 14 Sep 2025 14:15:16 +0900 Subject: [PATCH 179/267] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 122a927abe..d64fadee97 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 9577472c9e3c9444fb6682aaf3266832f64cb707 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Sep 2025 12:49:42 +0900 Subject: [PATCH 180/267] Fix errors in gameplay stage of matchmaking --- .../OnlinePlay/Matchmaking/MatchmakingScreen.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs index ba2e5593bf..b02583103d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs @@ -217,6 +217,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void updateGameplayState() { MultiplayerPlaylistItem item = client.Room!.CurrentPlaylistItem; + + if (item.Expired) + return; + RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = ruleset.CreateInstance(); @@ -228,9 +232,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); if (Beatmap.Value is DummyWorkingBeatmap) - client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); + { + if (client.LocalUser!.State == MultiplayerUserState.Ready) + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); + } else - client.ChangeState(MultiplayerUserState.Ready).FireAndForget(); + { + if (client.LocalUser!.State == MultiplayerUserState.Idle) + client.ChangeState(MultiplayerUserState.Ready).FireAndForget(); + } client.ChangeBeatmapAvailability(beatmapAvailabilityTracker.Availability.Value).FireAndForget(); } From 37f58e5c802b06a7ea7f1e8a597e723a6a1c29a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Sep 2025 13:19:00 +0900 Subject: [PATCH 181/267] Add client-side support for TOTP authentication Closes https://github.com/ppy/osu/issues/34972. --- .../Visual/Menus/TestSceneLoginOverlay.cs | 151 +++++++++++++++++- osu.Game/Online/API/APIAccess.cs | 18 ++- osu.Game/Online/API/DummyAPIAccess.cs | 21 ++- osu.Game/Online/API/IAPIProvider.cs | 7 +- .../Online/API/Requests/Responses/APIMe.cs | 17 +- .../VerificationMailFallbackRequest.cs | 20 +++ .../API/Requests/VerifySessionRequest.cs | 20 +++ .../Overlays/Login/SecondFactorAuthForm.cs | 145 ++++++++++++----- 8 files changed, 351 insertions(+), 48 deletions(-) create mode 100644 osu.Game/Online/API/Requests/VerificationMailFallbackRequest.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 3c97b291ee..0dfe055040 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.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 System.Net; using NUnit.Framework; @@ -9,10 +10,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Login; using osu.Game.Overlays.Settings; @@ -54,7 +57,7 @@ namespace osu.Game.Tests.Visual.Menus } [Test] - public void TestLoginSuccess() + public void TestLoginSuccess_EmailVerification() { AddStep("logout", () => API.Logout()); assertAPIState(APIState.Offline); @@ -94,6 +97,152 @@ namespace osu.Game.Tests.Visual.Menus assertDropdownState(UserAction.DoNotDisturb); } + [Test] + public void TestLoginSuccess_TOTPVerification() + { + AddStep("logout", () => API.Logout()); + assertAPIState(APIState.Offline); + AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + + assertAPIState(APIState.RequiresSecondFactorAuth); + AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + + AddStep("set up verification handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case VerifySessionRequest verifySessionRequest: + if (verifySessionRequest.VerificationKey == "012345") + verifySessionRequest.TriggerSuccess(); + else + verifySessionRequest.TriggerFailure(new WebException()); + return true; + } + + return false; + }); + + AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "012345"); + assertAPIState(APIState.Online); + assertDropdownState(UserAction.Online); + + AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); }); + AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); }); + + AddStep("clear handler", () => dummyAPI.HandleRequest = null); + + assertDropdownState(UserAction.Online); + AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb)); + assertDropdownState(UserAction.DoNotDisturb); + } + + [Test] + public void TestLoginSuccess_TOTPVerification_FallbackToEmail() + { + AddStep("logout", () => API.Logout()); + assertAPIState(APIState.Offline); + AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + + assertAPIState(APIState.RequiresSecondFactorAuth); + AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + + AddStep("set up verification handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case VerifySessionRequest verifySessionRequest: + if (verifySessionRequest.VerificationKey == "deadbeef") + verifySessionRequest.TriggerSuccess(); + else + verifySessionRequest.TriggerFailure(new WebException()); + return true; + + case VerificationMailFallbackRequest verificationMailFallbackRequest: + verificationMailFallbackRequest.TriggerSuccess(); + return true; + } + + return false; + }); + + AddStep("request fallback to email", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single(t => t.Text.ToString().Contains("email", StringComparison.InvariantCultureIgnoreCase))); + InputManager.Click(MouseButton.Left); + }); + + AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "deadbeef"); + assertAPIState(APIState.Online); + assertDropdownState(UserAction.Online); + + AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); }); + AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); }); + + AddStep("clear handler", () => dummyAPI.HandleRequest = null); + + assertDropdownState(UserAction.Online); + AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb)); + assertDropdownState(UserAction.DoNotDisturb); + } + + [Test] + public void TestLoginSuccess_TOTPVerification_TurnedOffMidwayThrough() + { + bool firstAttemptHandled = false; + + AddStep("logout", () => API.Logout()); + assertAPIState(APIState.Offline); + AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + + assertAPIState(APIState.RequiresSecondFactorAuth); + AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + + AddStep("set up verification handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case VerifySessionRequest verifySessionRequest: + verifySessionRequest.RequiredVerificationMethod = SessionVerificationMethod.EmailMessage; + verifySessionRequest.TriggerFailure(new WebException()); + firstAttemptHandled = true; + return true; + } + + return false; + }); + + AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "123456"); + AddUntilStep("first verification attempt handled", () => firstAttemptHandled); + assertAPIState(APIState.RequiresSecondFactorAuth); + + AddStep("set up verification handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case VerifySessionRequest verifySessionRequest: + if (verifySessionRequest.VerificationKey == "deadbeef") + verifySessionRequest.TriggerSuccess(); + else + verifySessionRequest.TriggerFailure(new WebException()); + return true; + } + + return false; + }); + AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "deadbeef"); + assertAPIState(APIState.Online); + assertDropdownState(UserAction.Online); + } + private void assertDropdownState(UserAction state) { AddAssert($"dropdown state is {state}", () => loginOverlay.ChildrenOfType().First().Current.Value, () => Is.EqualTo(state)); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 54eed58c13..58171a2f8a 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using osu.Framework.Bindables; using osu.Framework.Development; +using osu.Framework.Extensions; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -52,6 +53,8 @@ namespace osu.Game.Online.API public string ProvidedUsername { get; private set; } + public SessionVerificationMethod? SessionVerificationMethod { get; set; } + public string SecondFactorCode { get; private set; } private string password; @@ -292,7 +295,17 @@ namespace osu.Game.Online.API verificationRequest.Failure += ex => { state.Value = APIState.RequiresSecondFactorAuth; - LastLoginError = ex; + + if (verificationRequest.RequiredVerificationMethod != null) + { + SessionVerificationMethod = verificationRequest.RequiredVerificationMethod; + LastLoginError = new APIException($"Must use {SessionVerificationMethod.GetDescription().ToLowerInvariant()} to complete verification.", ex); + } + else + { + LastLoginError = ex; + } + SecondFactorCode = null; }; @@ -337,7 +350,8 @@ namespace osu.Game.Online.API localUser.Value = me; configSupporter.Value = me.IsSupporter; - state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; + SessionVerificationMethod = me.SessionVerificationMethod; + state.Value = SessionVerificationMethod == null ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; }; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 74e0ca2873..9750fccb74 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Localisation; using osu.Game.Online.API.Requests; @@ -62,7 +63,8 @@ namespace osu.Game.Online.API private bool shouldFailNextLogin; private bool stayConnectingNextLogin; - private bool requiredSecondFactorAuth = true; + + public SessionVerificationMethod? SessionVerificationMethod { get; set; } = Requests.Responses.SessionVerificationMethod.EmailMessage; /// /// The current connectivity state of the API. @@ -130,14 +132,14 @@ namespace osu.Game.Online.API Id = DUMMY_USER_ID, }; - if (requiredSecondFactorAuth) + if (SessionVerificationMethod != null) { state.Value = APIState.RequiresSecondFactorAuth; } else { onSuccessfulLogin(); - requiredSecondFactorAuth = true; + SessionVerificationMethod = null; } } @@ -147,7 +149,16 @@ namespace osu.Game.Online.API request.Failure += e => { state.Value = APIState.RequiresSecondFactorAuth; - LastLoginError = e; + + if (request.RequiredVerificationMethod != null) + { + SessionVerificationMethod = request.RequiredVerificationMethod; + LastLoginError = new APIException($"Must use {SessionVerificationMethod.GetDescription().ToLowerInvariant()} to complete verification.", e); + } + else + { + LastLoginError = e; + } }; state.Value = APIState.Connecting; @@ -204,7 +215,7 @@ namespace osu.Game.Online.API /// /// Skip 2FA requirement for next login. /// - public void SkipSecondFactor() => requiredSecondFactorAuth = false; + public void SkipSecondFactor() => SessionVerificationMethod = null; /// /// During the next simulated login, the process will fail immediately. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 2634ea137f..f3ced9b1ce 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -107,10 +107,15 @@ namespace osu.Game.Online.API /// The user's password. void Login(string username, string password); + /// + /// The requested by the server to complete verification. + /// + SessionVerificationMethod? SessionVerificationMethod { get; } + /// /// Provide a second-factor authentication code for authentication. /// - /// The 2FA code. + /// The 2FA code. void AuthenticateSecondFactor(string code); /// diff --git a/osu.Game/Online/API/Requests/Responses/APIMe.cs b/osu.Game/Online/API/Requests/Responses/APIMe.cs index 3cbddbe5e7..f1fa9d5f2b 100644 --- a/osu.Game/Online/API/Requests/Responses/APIMe.cs +++ b/osu.Game/Online/API/Requests/Responses/APIMe.cs @@ -1,13 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; +using System.Runtime.Serialization; using Newtonsoft.Json; namespace osu.Game.Online.API.Requests.Responses { public class APIMe : APIUser { - [JsonProperty("session_verified")] - public bool SessionVerified { get; set; } + [JsonProperty("session_verification_method")] + public SessionVerificationMethod? SessionVerificationMethod { get; set; } + } + + public enum SessionVerificationMethod + { + [Description("Timed one-time password")] + [EnumMember(Value = "totp")] + TimedOneTimePassword, + + [Description("E-mail")] + [EnumMember(Value = "mail")] + EmailMessage, } } diff --git a/osu.Game/Online/API/Requests/VerificationMailFallbackRequest.cs b/osu.Game/Online/API/Requests/VerificationMailFallbackRequest.cs new file mode 100644 index 0000000000..6ea652d647 --- /dev/null +++ b/osu.Game/Online/API/Requests/VerificationMailFallbackRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class VerificationMailFallbackRequest : APIRequest + { + protected override string Target => @"session/verify/mail-fallback"; + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + return req; + } + } +} diff --git a/osu.Game/Online/API/Requests/VerifySessionRequest.cs b/osu.Game/Online/API/Requests/VerifySessionRequest.cs index b39ec5b79a..d8f622348b 100644 --- a/osu.Game/Online/API/Requests/VerifySessionRequest.cs +++ b/osu.Game/Online/API/Requests/VerifySessionRequest.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Net.Http; +using Newtonsoft.Json; using osu.Framework.IO.Network; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { @@ -13,6 +15,16 @@ namespace osu.Game.Online.API.Requests public VerifySessionRequest(string verificationKey) { VerificationKey = verificationKey; + + Failure += _ => + { + string? response = WebRequest?.GetResponseString(); + if (string.IsNullOrEmpty(response)) + return; + + var responseObject = JsonConvert.DeserializeObject(response); + RequiredVerificationMethod = responseObject?.RequiredSessionVerificationMethod; + }; } protected override WebRequest CreateWebRequest() @@ -26,5 +38,13 @@ namespace osu.Game.Online.API.Requests } protected override string Target => @"session/verify"; + + public SessionVerificationMethod? RequiredVerificationMethod { get; internal set; } + + private class VerificationFailureResponse + { + [JsonProperty("method")] + public SessionVerificationMethod RequiredSessionVerificationMethod { get; set; } + } } } diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 74db58e225..2cdc4bf6a6 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Settings; using osu.Game.Resources.Localisation.Web; using osuTK; @@ -21,11 +22,11 @@ namespace osu.Game.Overlays.Login { public partial class SecondFactorAuthForm : Container { - private OsuTextBox codeTextBox = null!; - private LinkFlowContainer explainText = null!; private ErrorTextFlowContainer errorText = null!; private LoadingLayer loading = null!; + private FillFlowContainer contentFlow = null!; + private OsuTextBox codeTextBox = null!; [Resolved] private IAPIProvider api { get; set; } = null!; @@ -36,6 +37,8 @@ namespace osu.Game.Overlays.Login RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }; + Children = new Drawable[] { new FillFlowContainer @@ -46,46 +49,18 @@ namespace osu.Game.Overlays.Login Spacing = new Vector2(0, SettingsSection.ITEM_SPACING), Children = new Drawable[] { - new FillFlowContainer + contentFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, Direction = FillDirection.Vertical, Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING), - Children = new Drawable[] - { - new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = "An email has been sent to you with a verification code. Enter the code.", - }, - codeTextBox = new OsuTextBox - { - InputProperties = new TextInputProperties(TextInputType.Code), - PlaceholderText = "Enter code", - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - }, - explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - errorText = new ErrorTextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - }, - }, }, - new LinkFlowContainer + errorText = new ErrorTextFlowContainer { - Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Alpha = 0, }, } }, @@ -95,6 +70,56 @@ namespace osu.Game.Overlays.Login } }; + if (api.LastLoginError?.Message is string error) + { + errorText.Alpha = 1; + errorText.AddErrors(new[] { error }); + } + + showContent(api.SessionVerificationMethod!.Value); + } + + private void showContent(SessionVerificationMethod sessionVerificationMethod) + { + switch (sessionVerificationMethod) + { + case SessionVerificationMethod.EmailMessage: + showEmailVerification(); + break; + + case SessionVerificationMethod.TimedOneTimePassword: + showTotpVerification(); + break; + } + } + + private void showEmailVerification() + { + LinkFlowContainer explainText; + + contentFlow.Clear(); + contentFlow.AddRange(new Drawable[] + { + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = "An email has been sent to you with a verification code. Enter the code.", + }, + codeTextBox = new OsuTextBox + { + InputProperties = new TextInputProperties(TextInputType.Code), + PlaceholderText = "Enter code", + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + }, + explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }); + explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam); // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something). explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the "); @@ -131,12 +156,58 @@ namespace osu.Game.Overlays.Login codeTextBox.Current.Disabled = true; } }); + } - if (api.LastLoginError?.Message is string error) + private void showTotpVerification() + { + LinkFlowContainer explainText; + + contentFlow.Clear(); + contentFlow.AddRange(new Drawable[] { - errorText.Alpha = 1; - errorText.AddErrors(new[] { error }); - } + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = "Please enter the code from your authenticator app.", + }, + codeTextBox = new OsuNumberBox + { + InputProperties = new TextInputProperties(TextInputType.NumericalPassword), + PlaceholderText = "Enter code", + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + }, + explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }); + + // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something). + explainText.AddParagraph("If you can't access your app, "); + explainText.AddLink("you can verify using email instead", () => + { + var fallbackRequest = new VerificationMailFallbackRequest(); + fallbackRequest.Success += showEmailVerification; + fallbackRequest.Failure += ex => errorText.Text = ex.Message; + Task.Run(() => api.Perform(fallbackRequest)); + }); + explainText.AddText(". You can also "); + explainText.AddLink(UserVerificationStrings.BoxInfoLogoutLink, () => { api.Logout(); }); + explainText.AddText("."); + + codeTextBox.Current.BindValueChanged(code => + { + string trimmedCode = code.NewValue.Trim(); + + if (trimmedCode.Length == 6) + { + api.AuthenticateSecondFactor(trimmedCode); + codeTextBox.Current.Disabled = true; + } + }); } public override bool AcceptsFocus => true; From e0c86b3048f74495571cd07fd61c27df3569d42e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 16 Sep 2025 20:30:32 -0700 Subject: [PATCH 182/267] Match profile badge centre alignment with web --- osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs index 5f100bc882..7e4c747ce8 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs @@ -31,6 +31,8 @@ namespace osu.Game.Overlays.Profile.Header.Components { Child = new Sprite { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, FillMode = FillMode.Fit, RelativeSizeAxes = Axes.Both, Texture = textures.Get(badge.ImageUrl), From 6dc343273554b59fce91a3f188ce3b3ce47731e7 Mon Sep 17 00:00:00 2001 From: AeroKoder Date: Wed, 17 Sep 2025 12:44:43 -0700 Subject: [PATCH 183/267] Fix certain slider shapes incorrectly registering as a horizontal/vertical only slider. --- .../Edit/OsuSelectionHandler.cs | 2 +- .../Edit/OsuSelectionScaleHandler.cs | 8 ++------ osu.Game/Utils/GeometryUtils.cs | 18 +++++++++++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 3a1ff34fb9..c591b79b29 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); + Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects, true); Vector2 delta = Vector2.Zero; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 9a5d3c3bc1..3072e5d11b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -81,12 +81,8 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); - OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider - ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) - : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); - originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2 - ? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position)) - : GeometryUtils.GetConvexHull(objectsInScale.Keys); + OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); + originalConvexHull = GeometryUtils.GetConvexHull(objectsInScale.Keys); defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1; } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index eac86a9c02..185b1cc4f1 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -144,8 +144,9 @@ namespace osu.Game.Utils /// Returns a gamefield-space quad surrounding the provided hit objects. /// /// The hit objects to calculate a quad for. - public static Quad GetSurroundingQuad(IEnumerable hitObjects) => - GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects)); + /// Whether to only include the start and end positions of the slider, or include every control point in the slider. + public static Quad GetSurroundingQuad(IEnumerable hitObjects, bool startAndEndOnly = false) => + GetSurroundingQuad(startAndEndOnly ? enumerateStartAndEndPositions(hitObjects) : enumeratePositions(hitObjects)); /// /// Returns the points that make up the convex hull of the provided points. @@ -202,7 +203,7 @@ namespace osu.Game.Utils } public static List GetConvexHull(IEnumerable hitObjects) => - GetConvexHull(enumerateStartAndEndPositions(hitObjects)); + GetConvexHull(enumeratePositions(hitObjects)); private static IEnumerable enumerateStartAndEndPositions(IEnumerable hitObjects) => hitObjects.SelectMany(h => @@ -220,6 +221,17 @@ namespace osu.Game.Utils return new[] { h.Position }; }); + private static IEnumerable enumeratePositions(IEnumerable hitObjects) => + hitObjects.SelectMany(h => + { + if (h is IHasPath path) + { + return path.Path.ControlPoints.Select(p => h.Position + p.Position); + } + + return new[] { h.Position }; + }); + #region Welzl helpers // Function to check whether a point lies inside or on the boundaries of the circle From b26a1b6330f345ac56464cf751854a60d6a986d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Sep 2025 15:16:02 +0900 Subject: [PATCH 184/267] Add display style to PlayerPanelList --- .../Matchmaking/TestSceneMatchmakingScreen.cs | 14 + .../Visual/Matchmaking/TestScenePickScreen.cs | 2 +- .../Matchmaking/TestScenePlayerPanelList.cs | 137 ++++++++++ .../Matchmaking/Screens/Idle/IdleScreen.cs | 11 +- .../Matchmaking/Screens/Idle/PlayerPanel.cs | 5 +- .../Screens/Idle/PlayerPanelList.cs | 245 ++++++++++++++++-- .../Screens/MatchmakingScreenStack.cs | 71 ++--- .../Screens/MatchmakingSubScreen.cs | 16 +- .../Matchmaking/Screens/Pick/PickScreen.cs | 30 ++- .../Screens/Results/ResultsScreen.cs | 18 +- .../RoundResults/RoundResultsScreen.cs | 4 + 11 files changed, 473 insertions(+), 80 deletions(-) create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index 416811d345..c155cd2aed 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -53,6 +53,20 @@ namespace osu.Game.Tests.Visual.Matchmaking WaitForJoined(); + AddStep("join users", () => + { + for (int i = 0; i < 7; i++) + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"User {i}" + } + }); + } + }); + setupRequestHandler(); AddStep("load match", () => diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index 16f687d772..6d9e802b65 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Matchmaking PickScreen screen = null!; - AddStep("add screen", () => LoadScreen(screen = new PickScreen())); + AddStep("add screen", () => Child = screen = new PickScreen()); AddStep("select maps", () => { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs new file mode 100644 index 0000000000..151bd3f02b --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs @@ -0,0 +1,137 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Tests.Visual.Multiplayer; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestScenePlayerPanelList : MultiplayerTestScene + { + private PlayerPanelList list = null!; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add list", () => Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Child = list = new PlayerPanelList() + }); + } + + [Test] + public void TestChangeDisplayMode() + { + AddStep("join users", () => + { + for (int i = 0; i < 7; i++) + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"User {i}" + } + }); + } + }); + + AddStep("change to split mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Split); + AddStep("change to grid mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Grid); + AddStep("change to hidden mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Hidden); + } + + [Test] + public void AddPanelsGrid() + { + AddStep("change to grid mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Grid); + + int userId = 0; + + AddRepeatStep("join user", () => + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(userId) + { + User = new APIUser + { + Username = $"User {userId}" + } + }); + + userId++; + }, 8); + } + + [Test] + public void AddPanelsSplit() + { + AddStep("change to split mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Split); + + int userId = 0; + + AddRepeatStep("join user", () => + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(userId) + { + User = new APIUser + { + Username = $"User {userId}" + } + }); + + userId++; + }, 8); + } + + [Test] + public void ChangeRankings() + { + AddStep("join users", () => + { + for (int i = 0; i < 7; i++) + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"User {i}" + } + }); + } + }); + + AddStep("set random placements", () => + { + MultiplayerRoom room = MultiplayerClient.ServerRoom!; + + int[] placements = Enumerable.Range(1, room.Users.Count).ToArray(); + Random.Shared.Shuffle(placements); + + MatchmakingRoomState state = new MatchmakingRoomState(); + + for (int i = 0; i < room.Users.Count; i++) + state.Users[room.Users[i].UserID].Placement = placements[i]; + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs index e67e2a520a..6f982d89f2 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs @@ -1,7 +1,6 @@ // 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.Framework.Screens; @@ -9,14 +8,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle { public partial class IdleScreen : MatchmakingSubScreen { - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new PlayerPanelList - { - RelativeSizeAxes = Axes.Both - }; - } + public override PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle => PlayerPanelList.PanelDisplayStyle.Grid; + public override Drawable PlayersDisplayArea => this; public override void OnEntering(ScreenTransitionEvent e) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs index eaddb0f2e4..1a0e24d5ba 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs @@ -18,6 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle { public partial class PlayerPanel : UserPanel { + public static readonly Vector2 SIZE_HORIZONTAL = new Vector2(250, 100); + public static readonly Vector2 SIZE_VERTICAL = new Vector2(150, 200); + public readonly MultiplayerRoomUser RoomUser; [Resolved] @@ -141,7 +144,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle double duration = instant ? 0 : 1000; avatar.MoveTo(avatarPosition, duration, Easing.OutPow10); - this.ResizeTo(horizontal ? new Vector2(250, 100) : new Vector2(150, 200), duration, Easing.OutPow10); + this.ResizeTo(horizontal ? SIZE_HORIZONTAL : SIZE_VERTICAL, duration, Easing.OutPow10); rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10); username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs index aa294f5bd3..111471273a 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs @@ -1,11 +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; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osuTK; @@ -17,19 +19,47 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle [Resolved] private MultiplayerClient client { get; set; } = null!; - public bool Horizontal { get; init; } + private Container panels = null!; + private PlayerPanelCellContainer gridLayout = null!; + private PlayerPanelCellContainer splitLayoutLeft = null!; + private PlayerPanelCellContainer splitLayoutRight = null!; - private FillFlowContainer panels = null!; + private PanelDisplayStyle displayStyle; + private Drawable? displayArea; + private bool isAnimatingToDisplayArea; [BackgroundDependencyLoader] private void load() { - InternalChild = panels = new FillFlowContainer + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Spacing = new Vector2(20, 5), - LayoutEasing = Easing.InOutQuint, - LayoutDuration = 500 + gridLayout = new PlayerPanelCellContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(20, 5), + }, + splitLayoutLeft = new PlayerPanelCellContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20, 5), + }, + splitLayoutRight = new PlayerPanelCellContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20, 5), + }, + panels = new Container + { + RelativeSizeAxes = Axes.Both + } }; } @@ -37,6 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle { base.LoadComplete(); + // Set position/size so we don't initially animate. + Position = getFinalPosition(); + Size = getFinalSize(); + client.MatchRoomStateChanged += onRoomStateChanged; client.UserJoined += onUserJoined; client.UserLeft += onUserLeft; @@ -47,36 +81,117 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle foreach (var user in client.Room.Users) onUserJoined(user); } + + updateDisplay(); + } + + public PanelDisplayStyle DisplayStyle + { + set + { + displayStyle = value; + if (IsLoaded) + updateDisplay(); + } + } + + public Drawable? DisplayArea + { + set + { + displayArea = value; + isAnimatingToDisplayArea = true; + } } private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => { panels.Add(new PlayerPanel(user) { - Horizontal = Horizontal, Anchor = Anchor.Centre, Origin = Anchor.Centre, }); + + updateDisplay(); }); private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => { panels.Single(p => p.RoomUser.Equals(user)).Expire(); + updateDisplay(); }); - private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(updateDisplay); + + private void updateDisplay() { - if (state is not MatchmakingRoomState matchmakingState) - return; + gridLayout.ReleasePanels(); + splitLayoutLeft.ReleasePanels(); + splitLayoutRight.ReleasePanels(); - foreach (var panel in panels) + switch (displayStyle) { - if (matchmakingState.Users.UserDictionary.TryGetValue(panel.User.Id, out MatchmakingUser? user)) - panels.SetLayoutPosition(panel, user.Placement); - else - panels.SetLayoutPosition(panel, float.MaxValue); + case PanelDisplayStyle.Grid: + foreach (var panel in panels) + { + panel.FadeTo(1, 200); + panel.Horizontal = false; + } + + gridLayout.AcquirePanels(panels.ToArray()); + break; + + case PanelDisplayStyle.Split: + foreach (var panel in panels) + { + panel.FadeTo(1, 200); + panel.Horizontal = true; + } + + int leftCount = (int)Math.Ceiling(panels.Count / 2f); + + splitLayoutLeft.AcquirePanels(panels.Take(leftCount).ToArray()); + splitLayoutRight.AcquirePanels(panels.Skip(leftCount).ToArray()); + break; + + case PanelDisplayStyle.Hidden: + foreach (var panel in panels) + panel.FadeTo(0, 200); + return; } - }); + } + + protected override void Update() + { + base.Update(); + + var targetPos = getFinalPosition(); + var targetSize = getFinalSize(); + + double duration = isAnimatingToDisplayArea ? 60 : 0; + + if (Time.Elapsed > 0) + { + Position = new Vector2( + (float)Interpolation.DampContinuously(Position.X, targetPos.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(Position.Y, targetPos.Y, duration, Time.Elapsed) + ); + + Size = new Vector2( + (float)Interpolation.DampContinuously(Size.X, targetSize.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(Size.Y, targetSize.Y, duration, Time.Elapsed) + ); + } + + // If we don't track the animating state, the animation will also occur when resizing the window. + isAnimatingToDisplayArea &= !Precision.AlmostEquals(Size, targetSize, 0.5f); + } + + private Vector2 getFinalPosition() + => displayArea == null ? Vector2.Zero : Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.TopLeft); + + private Vector2 getFinalSize() + => displayArea == null ? Parent!.DrawSize : Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.BottomRight) - Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.TopLeft); protected override void Dispose(bool isDisposing) { @@ -89,5 +204,101 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle client.UserLeft -= onUserLeft; } } + + private partial class PlayerPanelCellContainer : FillFlowContainer + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + public void AcquirePanels(PlayerPanel[] panels) + { + while (Count < panels.Length) + { + Add(new PlayerPanelCell + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + while (Count > panels.Length) + Remove(Children[^1], true); + + for (int i = 0; i < panels.Length; i++) + { + // We'll invalidate the layout position to represent the new placements and the re-flow will happen in UpdateAfterChildren(). + // But the cells expect their positions to be valid as they're updated, which won't be the case until the re-flow happens. + int i2 = i; + ScheduleAfterChildren(() => Children[i2].AcquirePanel(panels[i2])); + + if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState) + continue; + + if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user)) + SetLayoutPosition(Children[i], user.Placement); + else + SetLayoutPosition(Children[i], float.MaxValue); + } + } + + public void ReleasePanels() + { + foreach (var panel in Children) + panel.ReleasePanel(); + } + } + + private partial class PlayerPanelCell : Drawable + { + private PlayerPanel? panel; + private bool isAnimating; + + public void AcquirePanel(PlayerPanel panel) + { + this.panel = panel; + isAnimating = true; + } + + public void ReleasePanel() + { + panel = null; + } + + protected override void Update() + { + base.Update(); + + if (panel == null) + return; + + Size = panel.Horizontal ? PlayerPanel.SIZE_HORIZONTAL : PlayerPanel.SIZE_VERTICAL; + Size *= panel.Scale; + + var targetPos = getFinalPosition(); + + double duration = isAnimating ? 60 : 0; + + if (Time.Elapsed > 0) + { + panel.Position = new Vector2( + (float)Interpolation.DampContinuously(panel.Position.X, targetPos.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(panel.Position.Y, targetPos.Y, duration, Time.Elapsed) + ); + } + + // If we don't track the animating state, the animation will also occur when resizing the window. + isAnimating &= !Precision.AlmostEquals(panel.Position, targetPos, 0.5f); + + Vector2 getFinalPosition() + => panel.Parent!.ToLocalSpace(ScreenSpaceDrawQuad.Centre) - panel.AnchorPosition; + } + } + + public enum PanelDisplayStyle + { + Grid, + Split, + Hidden + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs index cba5c89385..0b34beacc7 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs @@ -13,7 +13,6 @@ using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults; -using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens { @@ -23,6 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private MultiplayerClient client { get; set; } = null!; private ScreenStack screenStack = null!; + private PlayerPanelList playersList = null!; [BackgroundDependencyLoader] private void load() @@ -30,40 +30,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens RelativeSizeAxes = Axes.Both; Padding = new MarginPadding(10); - InternalChild = new GridContainer + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize) }, - Content = new Drawable[][] + new GridContainer { - [ - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.AutoSize) }, - Padding = new MarginPadding { Bottom = 20 }, - Content = new Drawable?[][] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize) }, + Content = new Drawable[][] + { + [ + screenStack = new ScreenStack(), + ], + [ + new StageDisplay { - [ - screenStack = new ScreenStack(), - null, - new PlayerPanelList - { - Horizontal = true, - RelativeSizeAxes = Axes.Y, - Width = 250, - Scale = new Vector2(0.8f), - } - ] + RelativeSizeAxes = Axes.X } - } - ], - [ - new StageDisplay - { - RelativeSizeAxes = Axes.X - } - ] + ] + } + }, + playersList = new PlayerPanelList + { + DisplayArea = this } }; } @@ -72,12 +60,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens { base.LoadComplete(); + screenStack.ScreenPushed += onScreenPushed; + screenStack.ScreenExited += onScreenExited; + screenStack.Push(new IdleScreen()); client.MatchRoomStateChanged += onMatchRoomStateChanged; onMatchRoomStateChanged(client.Room!.MatchState); } + private void onScreenPushed(IScreen lastScreen, IScreen newScreen) + { + if (newScreen is not MatchmakingSubScreen matchmakingSubScreen) + return; + + playersList.DisplayStyle = matchmakingSubScreen.PlayersDisplayStyle; + playersList.DisplayArea = matchmakingSubScreen.PlayersDisplayArea; + } + + private void onScreenExited(IScreen lastScreen, IScreen newScreen) + { + if (newScreen is not MatchmakingSubScreen matchmakingSubScreen) + return; + + playersList.DisplayStyle = matchmakingSubScreen.PlayersDisplayStyle; + playersList.DisplayArea = matchmakingSubScreen.PlayersDisplayArea; + } + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => { if (state is not MatchmakingRoomState matchmakingState) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs index 86a46546ca..d14739c021 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs @@ -3,12 +3,16 @@ using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens { - public partial class MatchmakingSubScreen : Screen + public abstract partial class MatchmakingSubScreen : Screen { - public MatchmakingSubScreen() + public abstract PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle { get; } + public abstract Drawable? PlayersDisplayArea { get; } + + protected MatchmakingSubScreen() { RelativePositionAxes = Axes.X; } @@ -16,19 +20,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); - this.MoveToX(1).MoveToX(0, 200); + this.FadeInFromZero(200); } public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); - this.MoveToX(-1, 200); + this.FadeOutFromOne(200); } public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - this.MoveToX(0, 200); + this.FadeInFromZero(200); } public override bool OnExiting(ScreenExitEvent e) @@ -36,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens if (base.OnExiting(e)) return true; - this.MoveToX(1, 200); + this.FadeOutFromOne(200); return false; } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs index 73e2188273..2a49030adf 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs @@ -8,26 +8,42 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { - public partial class PickScreen : OsuScreen + public partial class PickScreen : MatchmakingSubScreen { - private BeatmapSelectionGrid selectionGrid = null!; + public override PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle => PlayerPanelList.PanelDisplayStyle.Split; + public override Drawable PlayersDisplayArea { get; } + + private readonly BeatmapSelectionGrid selectionGrid; [Resolved] private MultiplayerClient client { get; set; } = null!; - [BackgroundDependencyLoader] - private void load() + public PickScreen() { - InternalChild = new Container + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = selectionGrid = new BeatmapSelectionGrid + new Container { RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 200 }, + Child = selectionGrid = new BeatmapSelectionGrid + { + RelativeSizeAxes = Axes.Both, + }, }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5 }, + Child = PlayersDisplayArea = Empty().With(d => + { + d.RelativeSizeAxes = Axes.Both; + }) + } }; } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs index 3fe4cc6d7a..83a7f0b7b6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs @@ -21,15 +21,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results { private const float grid_spacing = 5; + public override PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle => PlayerPanelList.PanelDisplayStyle.Grid; + public override Drawable PlayersDisplayArea { get; } + [Resolved] private MultiplayerClient client { get; set; } = null!; - private OsuSpriteText placementText = null!; - private FillFlowContainer userStatistics = null!; - private FillFlowContainer roomStatistics = null!; + private readonly OsuSpriteText placementText; + private readonly FillFlowContainer userStatistics; + private readonly FillFlowContainer roomStatistics; - [BackgroundDependencyLoader] - private void load() + public ResultsScreen() { InternalChild = new GridContainer { @@ -113,10 +115,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results } }, null, - new PlayerPanelList + PlayersDisplayArea = Empty().With(d => { - RelativeSizeAxes = Axes.Both - } + d.RelativeSizeAxes = Axes.Both; + }) ] } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs index d7837e96c6..71d19c1791 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; using osu.Game.Screens.Ranking; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults @@ -29,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults { private const int panel_spacing = 5; + public override PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle => PlayerPanelList.PanelDisplayStyle.Hidden; + public override Drawable? PlayersDisplayArea => null; + [Resolved] private IAPIProvider api { get; set; } = null!; From 5eaf376a607ee677e083e73928d5d699a061c8ef Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Sep 2025 15:16:48 +0900 Subject: [PATCH 185/267] Decrease scale of panels --- .../Matchmaking/Screens/Idle/PlayerPanel.cs | 69 ++++++++++--------- .../Screens/Idle/PlayerPanelList.cs | 1 + 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs index 1a0e24d5ba..d24e17b9b1 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs @@ -35,6 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle private MatchmakingAvatar avatar = null!; private OsuSpriteText username = null!; + private Container scaleContainer = null!; private Container mainContent = null!; public bool Horizontal @@ -62,41 +63,47 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle Masking = true; CornerRadius = 10; - Add(mainContent = new Container + Add(scaleContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Child = mainContent = new Container { - avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.TopLeft, - Origin = Anchor.Centre, - Size = new Vector2(80), - }, - rankText = new OsuSpriteText - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomCentre, - Blending = BlendingParameters.Additive, - Margin = new MarginPadding(4), - Font = OsuFont.Style.Title.With(size: 70), - }, - username = new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = User.Username, - Font = OsuFont.Style.Heading1, - }, - scoreText = new OsuSpriteText - { - Margin = new MarginPadding(10), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Font = OsuFont.Style.Heading2, - Text = "0 pts" + avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.Centre, + Size = new Vector2(80), + }, + rankText = new OsuSpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomCentre, + Blending = BlendingParameters.Additive, + Margin = new MarginPadding(4), + Font = OsuFont.Style.Title.With(size: 70), + }, + username = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Text = User.Username, + Font = OsuFont.Style.Heading1, + }, + scoreText = new OsuSpriteText + { + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Style.Heading2, + Text = "0 pts" + } } } }); @@ -153,14 +160,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle protected override bool OnHover(HoverEvent e) { - this.ScaleTo(1.02f, 1000, Easing.OutQuint); + scaleContainer.ScaleTo(1.02f, 1000, Easing.OutQuint); mainContent.ScaleTo(1.03f, 1000, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - this.ScaleTo(1f, 500, Easing.OutQuint); + scaleContainer.ScaleTo(1f, 500, Easing.OutQuint); mainContent.ScaleTo(1, 500, Easing.OutQuint); mainContent.MoveTo(Vector2.Zero, 500, Easing.OutElasticHalf); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs index 111471273a..3683198821 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs @@ -110,6 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Scale = new Vector2(0.8f) }); updateDisplay(); From c08d88eb7f2c862d104acfaa01126cf9db55b6c3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Sep 2025 16:11:40 +0900 Subject: [PATCH 186/267] Adjust namespaces --- osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs | 2 +- osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs | 2 +- .../OnlinePlay/Matchmaking/{Screens/Idle => }/PlayerPanel.cs | 2 +- .../Matchmaking/{Screens/Idle => }/PlayerPanelList.cs | 2 +- .../OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs | 1 - .../Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs | 1 - .../OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs | 1 - .../Matchmaking/Screens/RoundResults/RoundResultsScreen.cs | 1 - 8 files changed, 4 insertions(+), 8 deletions(-) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Idle => }/PlayerPanel.cs (99%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Idle => }/PlayerPanelList.cs (99%) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index dafb2d9f03..f98a6aac99 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs index 151bd3f02b..17423c9852 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; using osuTK; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs similarity index 99% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs index d24e17b9b1..42b1edde9b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs @@ -14,7 +14,7 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Users; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle +namespace osu.Game.Screens.OnlinePlay.Matchmaking { public partial class PlayerPanel : UserPanel { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs similarity index 99% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs index 3683198821..fa2c515f77 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs @@ -12,7 +12,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle +namespace osu.Game.Screens.OnlinePlay.Matchmaking { public partial class PlayerPanelList : CompositeDrawable { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs index d14739c021..fc41b7db84 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Screens; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs index 2a49030adf..96cfa67642 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs index 83a7f0b7b6..83c587e7cd 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; using osu.Game.Utils; using osuTK; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs index 71d19c1791..8fd56877eb 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; using osu.Game.Screens.Ranking; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults From 55a4c75e769432141a9f28038017b922f99f941f Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Thu, 18 Sep 2025 21:26:49 +0200 Subject: [PATCH 187/267] Allow slider control points to snap to nearby objects and a bit of code cleanup to reduce code duplication with the slider head anchor snapping --- .../Components/PathControlPointVisualiser.cs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 5ae9b194be..b6b1185816 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -440,21 +440,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Vector2 oldPosition = hitObject.Position; double oldStartTime = hitObject.StartTime; + SnapResult snapControlPoint(Vector2 newScreenSpacePosition, bool trySnapToDistanceGrid) + { + var result = positionSnapProvider?.TrySnapToNearbyObjects(newScreenSpacePosition, oldStartTime); + if (trySnapToDistanceGrid) + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newScreenSpacePosition, limitedDistanceSnap.Value ? oldStartTime : null); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newScreenSpacePosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newScreenSpacePosition, oldStartTime); + return result; + } + if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0])) { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - - var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); - result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null); - if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) - result = gridSnapResult; - result ??= new SnapResult(newHeadPosition, oldStartTime); - - Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position; + var snapResult = snapControlPoint(newHeadPosition, true); + Vector2 movementDelta = Parent!.ToLocalSpace(snapResult.ScreenSpacePosition) - hitObject.Position; hitObject.Position += movementDelta; - hitObject.StartTime = result.Time ?? hitObject.StartTime; + hitObject.StartTime = snapResult.Time ?? hitObject.StartTime; for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { @@ -469,9 +474,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition)); - - Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; + Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition); + var snapResult = snapControlPoint(newControlPointPosition, false); + Vector2 movementDelta = Parent!.ToLocalSpace(snapResult.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; for (int i = 0; i < controlPoints.Count; ++i) { From 0e523d3eb78d3657c03174c29135b604ed642705 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Fri, 19 Sep 2025 00:30:31 +0200 Subject: [PATCH 188/267] Allow snapping to visible slider control points --- .../Sliders/SliderSelectionBlueprint.cs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 363533ae76..5164eb9112 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -626,10 +626,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); - protected override Vector2[] ScreenSpaceAdditionalNodes => new[] - { + protected override Vector2[] ScreenSpaceAdditionalNodes => getScreenSpaceControlPointNodes().Prepend( DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation) - }; + ).ToArray(); + + private IEnumerable getScreenSpaceControlPointNodes() + { + // Return all control point positions which are noticeable on the slider body + // This excludes inherited control points which don't sit on the slider body: Bezier and B-Spline + // And inherited control points which are smooth: Perfect and Catmull + if (DrawableObject.SliderBody == null) + yield break; + + PathType? currentPathType = DrawableObject.HitObject.Path.ControlPoints.FirstOrDefault()?.Type; + + // Skip the first control point because it is already covered by the slider head + // Skip the last control point because its always either not on the slider body or exactly on the slider end + foreach (var controlPoint in DrawableObject.HitObject.Path.ControlPoints.Skip(0).SkipLast(1)) + { + if (controlPoint.Type is null && currentPathType != PathType.LINEAR) + continue; + + if (controlPoint.Type is not null) + currentPathType = controlPoint.Type; + + var screenSpacePosition = DrawableObject.SliderBody.ToScreenSpace(DrawableObject.SliderBody.PathOffset + controlPoint.Position); + yield return screenSpacePosition; + } + } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { From 7c5278ea45927c22d15443fc94c5ff7644638311 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Fri, 19 Sep 2025 01:09:42 +0200 Subject: [PATCH 189/267] Fix incorrect skip count --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 5164eb9112..553ee94bbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -642,7 +642,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Skip the first control point because it is already covered by the slider head // Skip the last control point because its always either not on the slider body or exactly on the slider end - foreach (var controlPoint in DrawableObject.HitObject.Path.ControlPoints.Skip(0).SkipLast(1)) + foreach (var controlPoint in DrawableObject.HitObject.Path.ControlPoints.Skip(1).SkipLast(1)) { if (controlPoint.Type is null && currentPathType != PathType.LINEAR) continue; From 9fddce92e96511a20e28bd096b30df4d2ae16ff7 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Fri, 19 Sep 2025 01:22:35 +0200 Subject: [PATCH 190/267] Reword inline comments --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 553ee94bbd..a7016bdae0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -632,16 +632,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private IEnumerable getScreenSpaceControlPointNodes() { - // Return all control point positions which are noticeable on the slider body - // This excludes inherited control points which don't sit on the slider body: Bezier and B-Spline - // And inherited control points which are smooth: Perfect and Catmull + // Returns the positions of control points that produce visible kinks on the slider's path + // This excludes inherited control points from Bezier, B-Spline, Perfect, and Catmull curves if (DrawableObject.SliderBody == null) yield break; PathType? currentPathType = DrawableObject.HitObject.Path.ControlPoints.FirstOrDefault()?.Type; // Skip the first control point because it is already covered by the slider head - // Skip the last control point because its always either not on the slider body or exactly on the slider end + // Skip the last control point because its always either not on the slider path or exactly on the slider end foreach (var controlPoint in DrawableObject.HitObject.Path.ControlPoints.Skip(1).SkipLast(1)) { if (controlPoint.Type is null && currentPathType != PathType.LINEAR) From b91ff8a5c514eb5bf58baf145a6e5de41238be89 Mon Sep 17 00:00:00 2001 From: qinvvv <88759424+qinvvv@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:53:25 -0300 Subject: [PATCH 191/267] Fix osu!mania legacy skin WidthForNoteHeightScale not being used (#35050) * Add osu!mania legacy skin widthForNoteHeightScale * Ensure WidthForNoteHeightScale correctly defaults to MinimumColumnWidth --- .../Skinning/Legacy/LegacyNotePiece.cs | 9 ++++----- osu.Game/Skinning/LegacySkin.cs | 6 ++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs index 4291ec3c13..c9c655ef7d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private Drawable noteAnimation = null!; - private float? minimumColumnWidth; + private float? widthForNoteHeightScale; public LegacyNotePiece() { @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - minimumColumnWidth = skin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value; + widthForNoteHeightScale = skin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale))?.Value; InternalChild = directionContainer = new Container { @@ -60,9 +60,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (texture != null) { - // The height is scaled to the minimum column width, if provided. - float minimumWidth = minimumColumnWidth ?? DrawWidth; - noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), texture.DisplayWidth); + float noteHeight = widthForNoteHeightScale ?? DrawWidth; + noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, noteHeight), texture.DisplayWidth); } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index b648299787..11b3b5c71d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -145,8 +145,10 @@ namespace osu.Game.Skinning return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value])); case LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale: - Debug.Assert(maniaLookup.ColumnIndex != null); - return SkinUtils.As(new Bindable(existing.WidthForNoteHeightScale)); + float width = existing.WidthForNoteHeightScale; + if (width <= 0) + width = existing.MinimumColumnWidth; + return SkinUtils.As(new Bindable(width)); case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); From 1840363713d5cdeb3aba446e5a742422b8e00297 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 21 Sep 2025 13:09:27 +0900 Subject: [PATCH 192/267] Add one more temoprary workaround for rider failings --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index e42b8b6a8a..a145efc348 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,6 +21,8 @@ trim_trailing_whitespace = true # temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint +# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false +dotnet_diagnostic.CS1591.severity = none #license header file_header_template = Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text. From 87061959ea3258b2487a6c4c7699638bac71f496 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Sep 2025 12:49:34 +0900 Subject: [PATCH 193/267] Fix failing screen test --- osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index 6d9e802b65..b12fff385b 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; @@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Matchmaking PickScreen screen = null!; - AddStep("add screen", () => Child = screen = new PickScreen()); + AddStep("add screen", () => Child = new ScreenStack(screen = new PickScreen())); AddStep("select maps", () => { From 3789010dd56f6d65a47091b0bd07200f1fff0404 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Sep 2025 15:09:45 +0900 Subject: [PATCH 194/267] Attempt to fix intermittent test --- osu.Game/Tests/Visual/EditorSavingTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs index d2b216caa8..8d27618c00 100644 --- a/osu.Game/Tests/Visual/EditorSavingTestScene.cs +++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual SoloSongSelect songSelect = null; PushAndConfirm(() => songSelect = new SoloSongSelect()); - AddUntilStep("wait for carousel load", () => songSelect.CarouselItemsPresented); + AddUntilStep("wait for carousel load", () => songSelect.CarouselItemsPresented && !songSelect.IsFiltering); AddStep("Present same beatmap", () => Game.PresentBeatmap(Game.BeatmapManager.QueryBeatmapSet(set => set.ID == beatmapSetGuid)!.Value, beatmap => beatmap.ID == beatmapGuid)); AddUntilStep("Wait for beatmap selected", () => Game.Beatmap.Value.BeatmapInfo.ID == beatmapGuid); From aba0d2c1d3548e03dd0d227b1aae02086be3e8a1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Sep 2025 17:52:33 +0900 Subject: [PATCH 195/267] Play gameplay start sample in matchmaking --- .../OnlinePlay/Matchmaking/MatchmakingScreen.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs index b02583103d..dd4bb97703 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; @@ -67,8 +69,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking [Resolved] private IDialogOverlay dialogOverlay { get; set; } = null!; + [Resolved] + private AudioManager audio { get; set; } = null!; + private readonly MultiplayerRoom room; + private Sample? sampleStart; private CancellationTokenSource? downloadCheckCancellation; private int? lastDownloadCheckedBeatmapId; @@ -83,6 +89,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking [BackgroundDependencyLoader] private void load() { + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -248,6 +256,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void onLoadRequested() => Scheduler.Add(() => { updateGameplayState(); + + if (Beatmap.IsDefault) + { + Logger.Log("Aborting gameplay start - beatmap not downloaded."); + return; + } + + sampleStart?.Play(); + this.Push(new MultiplayerPlayerLoader(() => new MatchmakingPlayer(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray()))); }); From ff6c6083b4cbd43599ca37dbd922e532cce75633 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Sep 2025 18:17:51 +0900 Subject: [PATCH 196/267] Mark spinner rewind test as flaky --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs index 4a72690da2..c4e643dcdf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -191,6 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] + [FlakyTest] public void TestRewind() { AddStep("set manual clock", () => manualClock = new ManualClock From 37e661b27e9f25e44f764850557cd63465436fbe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Sep 2025 18:23:40 +0900 Subject: [PATCH 197/267] Fix intermittent collection dropdown tests --- .../Visual/SongSelect/TestSceneCollectionDropdown.cs | 8 ++++---- .../Visual/SongSelectV2/TestSceneCollectionDropdown.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs index db004b1d0d..8fcbcb2fbc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs @@ -86,11 +86,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); - AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + AddUntilStep("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddUntilStep("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] @@ -185,11 +185,11 @@ namespace osu.Game.Tests.Visual.SongSelect assertFirstButtonIs(FontAwesome.Solid.PlusSquare); addClickAddOrRemoveButtonStep(1); - AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + AddUntilStep("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); assertFirstButtonIs(FontAwesome.Solid.MinusSquare); addClickAddOrRemoveButtonStep(1); - AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + AddUntilStep("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); assertFirstButtonIs(FontAwesome.Solid.PlusSquare); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs index 5c4969f9ad..774d4a00ce 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs @@ -87,11 +87,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); - AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + AddUntilStep("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddUntilStep("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] @@ -186,11 +186,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 assertFirstButtonIs(FontAwesome.Solid.PlusSquare); addClickAddOrRemoveButtonStep(1); - AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + AddUntilStep("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); assertFirstButtonIs(FontAwesome.Solid.MinusSquare); addClickAddOrRemoveButtonStep(1); - AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + AddUntilStep("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); assertFirstButtonIs(FontAwesome.Solid.PlusSquare); } From bf63b6f9f0e611c24b4c4e628ee273cc67c29797 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Sep 2025 22:55:43 +0900 Subject: [PATCH 198/267] Simplify lookup method to appease inspection See https://github.com/ppy/osu/pull/35100/files. --- osu.Game/Skinning/RealmBackedResourceStore.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index f41bd89b7a..0932485349 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -53,13 +53,8 @@ namespace osu.Game.Skinning } } - private string? getPathForFile(string filename) - { - if (fileToStoragePathMapping.Value.TryGetValue(filename.ToLowerInvariant(), out string? path)) - return path; - - return null; - } + private string? getPathForFile(string filename) => + fileToStoragePathMapping.Value.GetValueOrDefault(filename.ToLowerInvariant()); private void invalidateCache() => fileToStoragePathMapping = new Lazy>(initialiseFileCache); From 597a06ac38ba3d859d383e2bed4f35c7681f817a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 13:42:55 +0900 Subject: [PATCH 199/267] Set initial matchmaking room state --- .../Multiplayer/TestMultiplayerClient.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 1cea38667e..3fe66d3cda 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -17,6 +17,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; @@ -248,6 +249,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Host = localUser }; + await changeMatchType(ServerRoom.Settings.MatchType).ConfigureAwait(false); await updatePlaylistOrder(ServerRoom).ConfigureAwait(false); await updateCurrentItem(ServerRoom, false).ConfigureAwait(false); @@ -260,10 +262,6 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override void OnRoomJoined() { Debug.Assert(ServerRoom != null); - - // emulate the server sending this after the join room. scheduler required to make sure the join room event is fired first (in Join). - changeMatchType(ServerRoom.Settings.MatchType).WaitSafely(); - RoomJoined = true; } @@ -592,6 +590,18 @@ namespace osu.Game.Tests.Visual.Multiplayer await ((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).ConfigureAwait(false); } + break; + + case MatchType.Matchmaking: + ServerRoom.MatchState = new MatchmakingRoomState(); + await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).ConfigureAwait(false); + + foreach (var user in ServerRoom.Users) + { + user.MatchState = null; + await ((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).ConfigureAwait(false); + } + break; } } From 9df3bd9a98618e2dec46684c645f2ddc407793d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 14:07:43 +0900 Subject: [PATCH 200/267] Add stage-changing helper, use in test scenes --- .../Matchmaking/TestSceneMatchmakingScreen.cs | 115 ++++-------------- .../TestSceneMatchmakingScreenStack.cs | 70 +++++------ .../Matchmaking/TestSceneStageDisplay.cs | 20 +-- .../Visual/Matchmaking/TestSceneStageText.cs | 19 ++- .../Multiplayer/TestMultiplayerClient.cs | 19 +++ 5 files changed, 89 insertions(+), 154 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index c155cd2aed..27de959ff8 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -6,9 +6,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; -using osu.Framework.Graphics.Primitives; using osu.Framework.Screens; -using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -19,10 +17,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; using osu.Game.Tests.Visual.Multiplayer; -using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.Matchmaking { @@ -41,6 +36,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => { var room = CreateDefaultRoom(); + room.Type = MatchType.Matchmaking; room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem { ID = i, @@ -97,106 +93,49 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestGameplayFlow() { - // Initial "ready" status of the room". - AddWaitStep("wait", 5); - - AddStep("round start", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + for (int round = 1; round <= 2; round++) { - Stage = MatchmakingStage.RoundWarmupTime - }).WaitSafely()); + AddLabel($"Round {round}"); - // Next round starts with picks. - AddWaitStep("wait", 5); - - AddStep("pick", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.UserBeatmapSelect - }).WaitSafely()); - - // Make some selections - AddWaitStep("wait", 5); - - for (int i = 0; i < 3; i++) - { - int j = i * 2; - AddStep("click a beatmap", () => + int r = round; + changeStage(MatchmakingStage.RoundWarmupTime, state => state.CurrentRound = r); + changeStage(MatchmakingStage.UserBeatmapSelect); + changeStage(MatchmakingStage.ServerBeatmapFinalised, state => { - Quad panelQuad = this.ChildrenOfType().ElementAt(j).ScreenSpaceDrawQuad; + MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + }).ToArray(); - InputManager.MoveMouseTo(new Vector2(panelQuad.Centre.X, panelQuad.TopLeft.Y + 5)); - InputManager.Click(MouseButton.Left); - }); + state.CandidateItems = beatmaps.Select(b => b.ID).ToArray(); + state.CandidateItem = beatmaps[0].ID; + }, waitTime: 35); - AddWaitStep("wait", 2); + changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload); + changeStage(MatchmakingStage.GameplayWarmupTime); + changeStage(MatchmakingStage.Gameplay); + changeStage(MatchmakingStage.ResultsDisplaying); } - // Lock in the gameplay beatmap - - AddStep("selection", () => + changeStage(MatchmakingStage.Ended, state => { - MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - }).ToArray(); - - MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.ServerBeatmapFinalised, - CandidateItems = beatmaps.Select(b => b.ID).ToArray(), - CandidateItem = beatmaps[0].ID - }).WaitSafely(); - }); - - // Prepare gameplay. - AddWaitStep("wait", 25); - - AddStep("prepare gameplay", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.GameplayWarmupTime - }).WaitSafely()); - - // Start gameplay. - AddWaitStep("wait", 5); - - AddStep("gameplay", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.Gameplay - }).WaitSafely()); - - AddStep("start gameplay", () => MultiplayerClient.StartMatch().WaitSafely()); - // AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); - - // Finish gameplay. - AddWaitStep("wait", 5); - - AddStep("round end", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.ResultsDisplaying - }).WaitSafely()); - - AddWaitStep("wait", 10); - - AddStep("room end", () => - { - MatchmakingRoomState state = new MatchmakingRoomState - { - CurrentRound = 1, - Stage = MatchmakingStage.Ended - }; - int localUserId = API.LocalUser.Value.OnlineID; state.Users[localUserId].Placement = 1; state.Users[localUserId].Rounds[1].Placement = 1; state.Users[localUserId].Rounds[1].TotalScore = 1; state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1; - - MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); } + private void changeStage(MatchmakingStage stage, Action? prepare = null, int waitTime = 5) + { + AddStep($"stage: {stage}", () => MultiplayerClient.MatchmakingChangeStage(stage, prepare).WaitSafely()); + AddWaitStep("wait", waitTime); + } + private void setupRequestHandler() { AddStep("setup request handler", () => diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs index be3d7463d6..24784db472 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs @@ -65,55 +65,49 @@ namespace osu.Game.Tests.Visual.Matchmaking } [Test] - public void TestStatus() + public void TestChangeStage() { - AddWaitStep("wait for scroll", 5); - AddStep("pick", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + for (int round = 1; round <= 2; round++) { - Stage = MatchmakingStage.UserBeatmapSelect - }).WaitSafely()); + AddLabel($"Round {round}"); - AddWaitStep("wait for scroll", 5); - AddStep("selection", () => + int r = round; + changeStage(MatchmakingStage.RoundWarmupTime, state => state.CurrentRound = r); + changeStage(MatchmakingStage.UserBeatmapSelect); + changeStage(MatchmakingStage.ServerBeatmapFinalised, state => + { + MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + }).ToArray(); + + state.CandidateItems = beatmaps.Select(b => b.ID).ToArray(); + state.CandidateItem = beatmaps[0].ID; + }, waitTime: 35); + + changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload); + changeStage(MatchmakingStage.GameplayWarmupTime); + changeStage(MatchmakingStage.Gameplay); + changeStage(MatchmakingStage.ResultsDisplaying); + } + + changeStage(MatchmakingStage.Ended, state => { - MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - }).ToArray(); - - beatmaps = Random.Shared.GetItems(beatmaps, 8); - - MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.ServerBeatmapFinalised, - CandidateItems = beatmaps.Select(b => b.ID).ToArray(), - CandidateItem = beatmaps[0].ID - }).WaitSafely(); - }); - - AddWaitStep("wait for scroll", 35); - AddStep("room end", () => - { - var state = new MatchmakingRoomState - { - CurrentRound = 1, - Stage = MatchmakingStage.Ended - }; - int localUserId = API.LocalUser.Value.OnlineID; state.Users[localUserId].Placement = 1; state.Users[localUserId].Rounds[1].Placement = 1; state.Users[localUserId].Rounds[1].TotalScore = 1; state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1; - - state.Users[1].Placement = 2; - state.Users[1].Rounds[1].Placement = 2; - - MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); } + + private void changeStage(MatchmakingStage stage, Action? prepare = null, int waitTime = 5) + { + AddStep($"stage: {stage}", () => MultiplayerClient.MatchmakingChangeStage(stage, prepare).WaitSafely()); + AddWaitStep("wait", waitTime); + } } } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs index 49680acd64..87af7577a9 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs @@ -5,7 +5,6 @@ using System; using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; @@ -31,24 +30,11 @@ namespace osu.Game.Tests.Visual.Matchmaking } [Test] - public void TestStartCountdown() + public void TestChangeStage() { - foreach (var status in Enum.GetValues()) + foreach (var stage in Enum.GetValues()) { - AddStep($"{status}", () => - { - MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = status - }).WaitSafely(); - - MultiplayerClient.StartCountdown(new MatchmakingStageCountdown - { - Stage = status, - TimeRemaining = TimeSpan.FromSeconds(5) - }).WaitSafely(); - }); - + AddStep($"{stage}", () => MultiplayerClient.MatchmakingChangeStage(stage).WaitSafely()); AddWaitStep("wait a bit", 10); } } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs index 0094c7645a..2d7f6b4db1 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.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 NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -26,18 +27,14 @@ namespace osu.Game.Tests.Visual.Matchmaking }); } - [TestCase(MatchmakingStage.WaitingForClientsJoin)] - [TestCase(MatchmakingStage.RoundWarmupTime)] - [TestCase(MatchmakingStage.UserBeatmapSelect)] - [TestCase(MatchmakingStage.ServerBeatmapFinalised)] - [TestCase(MatchmakingStage.WaitingForClientsBeatmapDownload)] - [TestCase(MatchmakingStage.GameplayWarmupTime)] - [TestCase(MatchmakingStage.Gameplay)] - [TestCase(MatchmakingStage.ResultsDisplaying)] - [TestCase(MatchmakingStage.Ended)] - public void TestStatus(MatchmakingStage status) + [Test] + public void TestChangeStage() { - AddStep("set status", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState { Stage = status }).WaitSafely()); + foreach (var stage in Enum.GetValues()) + { + AddStep($"{stage}", () => MultiplayerClient.MatchmakingChangeStage(stage).WaitSafely()); + AddWaitStep("wait a bit", 10); + } } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 3fe66d3cda..5a69c6fcba 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -824,6 +824,25 @@ namespace osu.Game.Tests.Visual.Multiplayer await ((IMatchmakingClient)this).MatchmakingItemSelected(clone(userId), clone(playlistItemId)).ConfigureAwait(false); } + public async Task MatchmakingChangeStage(MatchmakingStage stage, Action? prepare = null) + { + MatchmakingRoomState state = clone((MatchmakingRoomState)ServerRoom!.MatchState!); + + state.Stage = stage; + + if (stage == MatchmakingStage.RoundWarmupTime) + state.CurrentRound++; + + prepare?.Invoke(state); + + await ChangeMatchRoomState(state).ConfigureAwait(false); + await StartCountdown(new MatchmakingStageCountdown + { + Stage = stage, + TimeRemaining = TimeSpan.FromSeconds(10) + }).ConfigureAwait(false); + } + #region API Room Handling public IReadOnlyList ServerSideRooms From 83dafb4ef99abd0f12d8525a113be3e78bb8b4f5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 14:09:50 +0900 Subject: [PATCH 201/267] Fix incorrect default room types --- osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs | 3 ++- .../Visual/Matchmaking/TestSceneMatchmakingScreen.cs | 3 +-- .../Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs | 2 +- osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs | 2 +- osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs | 3 ++- osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs | 3 ++- osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs | 3 ++- .../Visual/Matchmaking/TestSceneRoundResultsScreen.cs | 2 +- osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs | 3 ++- osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs | 3 ++- osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs | 3 ++- osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 4 ++-- 12 files changed, 20 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs index 49daedb6a3..174a77390e 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Screens; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; using osu.Game.Tests.Visual.Multiplayer; using osuTK; @@ -26,7 +27,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); AddStep("add list", () => diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index 27de959ff8..2abe9ac08b 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -35,8 +35,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => { - var room = CreateDefaultRoom(); - room.Type = MatchType.Matchmaking; + var room = CreateDefaultRoom(MatchType.Matchmaking); room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem { ID = i, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs index 24784db472..02c74b1d07 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => { - var room = CreateDefaultRoom(); + var room = CreateDefaultRoom(MatchType.Matchmaking); room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem { ID = i, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index b12fff385b..1c12b4d2d0 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => { - var room = CreateDefaultRoom(); + var room = CreateDefaultRoom(MatchType.Matchmaking); room.Playlist = items; JoinRoom(room); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index f98a6aac99..805e83a76d 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; @@ -21,7 +22,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs index 17423c9852..5e43a37e07 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; using osuTK; @@ -24,7 +25,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); AddStep("add list", () => Child = new Container diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs index 5fd5b1c906..6b3d42694b 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Screens; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; using osu.Game.Tests.Visual.Multiplayer; @@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); AddStep("add results screen", () => diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs index e19d228c85..561c994945 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); setupRequestHandler(); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs index 6349f01f28..3cac86b14f 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; @@ -19,7 +20,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); AddStep("add bubble", () => Child = new StageBubble(MatchmakingStage.RoundWarmupTime, "Next Round") diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs index 87af7577a9..3ec721d432 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; @@ -17,7 +18,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); AddStep("add bubble", () => Child = new StageDisplay diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs index 2d7f6b4db1..bc465f53a4 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; @@ -17,7 +18,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); AddStep("create display", () => Child = new StageText diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index ac587d3bb2..316e90d7d3 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -24,12 +24,12 @@ namespace osu.Game.Tests.Visual.Multiplayer public bool RoomJoined => MultiplayerClient.RoomJoined; - protected Room CreateDefaultRoom() + protected Room CreateDefaultRoom(MatchType type = MatchType.HeadToHead) { return new Room { Name = "test name", - Type = MatchType.HeadToHead, + Type = type, Playlist = [ new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) From 3556d6c8c882bac84246ef805fa499d656711597 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 14:19:07 +0900 Subject: [PATCH 202/267] Reduce number of picks shown --- osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs | 2 +- .../Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index 2abe9ac08b..b64424db92 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Matchmaking changeStage(MatchmakingStage.UserBeatmapSelect); changeStage(MatchmakingStage.ServerBeatmapFinalised, state => { - MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 8).Select(i => new MultiplayerPlaylistItem { ID = i, BeatmapID = i, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs index 02c74b1d07..94547dd115 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Matchmaking changeStage(MatchmakingStage.UserBeatmapSelect); changeStage(MatchmakingStage.ServerBeatmapFinalised, state => { - MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 8).Select(i => new MultiplayerPlaylistItem { ID = i, BeatmapID = i, From 13cb5deca3ba96027bce1919b6d4b715a69fa147 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 14:44:21 +0900 Subject: [PATCH 203/267] Fix players positioning on next matchmaking round --- .../Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs index fa2c515f77..a226ce19be 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs @@ -244,8 +244,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking public void ReleasePanels() { - foreach (var panel in Children) - panel.ReleasePanel(); + // Matches the schedule in AcquirePanels. + ScheduleAfterChildren(() => + { + foreach (var panel in Children) + panel.ReleasePanel(); + }); } } From 57e5fe7265796754e6480537fdfce22fa3004499 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 24 Sep 2025 09:53:16 +0300 Subject: [PATCH 204/267] Improve FailRetryDisplay performance (#35101) --- .../BeatmapMetadataWedge_FailRetryDisplay.cs | 82 ++++++++++++++----- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs index 048ec3c40d..9ee61b7c5c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs @@ -8,6 +8,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; +using osu.Framework.Graphics.Shaders; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -95,6 +97,14 @@ namespace osu.Game.Screens.SelectV2 } } + private IShader shader = null!; + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle"); + } + protected override void Update() { base.Update(); @@ -123,6 +133,8 @@ namespace osu.Game.Screens.SelectV2 private Vector2 drawSize; private float[] displayedData = null!; + private IShader shader = null!; + private IVertexBatch? quadBatch; public GraphDrawNode(GraphDrawable source) : base(source) @@ -136,6 +148,7 @@ namespace osu.Game.Screens.SelectV2 drawSize = source.DrawSize; displayedData = source.displayedData; + shader = source.shader; } protected override void Draw(IRenderer renderer) @@ -150,6 +163,9 @@ namespace osu.Game.Screens.SelectV2 float totalSpacing = drawSize.X - barWidth * displayedData.Length; float spacing = totalSpacing / (displayedData.Length - 1); + quadBatch ??= renderer.CreateQuadBatch(displayedData.Length * 4, 1); + shader.Bind(); + for (int i = 0; i < displayedData.Length; i++) { float barHeight = MathF.Max(drawSize.Y * displayedData[i], barWidth); @@ -158,35 +174,61 @@ namespace osu.Game.Screens.SelectV2 position += barWidth + spacing; } + + shader.Unbind(); } private void drawBar(IRenderer renderer, float position, float width, float height) { - float cornerRadius = width / 2f; - - Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); - float blendRange = (scale.X + scale.Y) / 2; + // Since bars have corner radius, to avoid masking usage and draw all bars in a single draw call + // we are using FastCircle implementation. + // Not using FastCircle directly to minimize drawable count. RectangleF drawRectangle = new RectangleF(new Vector2(position, drawSize.Y - height), new Vector2(width, height)); + Vector4 textureRectangle = new Vector4(0, 0, drawRectangle.Width, drawRectangle.Height); Quad screenSpaceDrawQuad = Quad.FromRectangle(drawRectangle) * DrawInfo.Matrix; - renderer.PushMaskingInfo(new MaskingInfo - { - ScreenSpaceAABB = screenSpaceDrawQuad.AABB, - MaskingRect = drawRectangle.Normalize(), - ConservativeScreenSpaceQuad = screenSpaceDrawQuad, - ToMaskingSpace = DrawInfo.MatrixInverse, - CornerRadius = cornerRadius, - CornerExponent = 2f, - // We are setting the linear blend range to the approximate size of a _pixel_ here. - // This results in the optimal trade-off between crispness and smoothness of the - // edges of the masked region according to sampling theory. - BlendRange = blendRange, - AlphaExponent = 1, - }); + var blend = new Vector2(Math.Min(drawRectangle.Width, drawRectangle.Height) / Math.Min(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height)); - renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour); - renderer.PopMaskingInfo(); + quadBatch?.AddAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.BottomLeft, + TexturePosition = new Vector2(0, drawRectangle.Height), + TextureRect = textureRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.BottomLeft.SRGB, + }); + quadBatch?.AddAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.BottomRight, + TexturePosition = new Vector2(drawRectangle.Width, drawRectangle.Height), + TextureRect = textureRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.BottomRight.SRGB, + }); + quadBatch?.AddAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.TopRight, + TexturePosition = new Vector2(drawRectangle.Width, 0), + TextureRect = textureRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.TopRight.SRGB, + }); + quadBatch?.AddAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.TopLeft, + TexturePosition = Vector2.Zero, + TextureRect = textureRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.TopLeft.SRGB, + }); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + quadBatch?.Dispose(); } } } From 67291c1a421da1ca4bc50e8d6f25236ad41c471e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Sep 2025 18:55:02 +0900 Subject: [PATCH 205/267] Fix `match-found` not playing due to incorrect case in path --- .../OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs index 8ec1505c1b..d23ff9bf84 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs @@ -148,7 +148,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens [BackgroundDependencyLoader] private void load(AudioManager audio) { - matchFoundSample = audio.Samples.Get(@"Multiplayer/matchmaking/match-found"); + matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found"); } private void onMatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status) => Scheduler.Add(() => From b3cfded8f209ffcc18b5e9e3c0ca8ab714af4601 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 18:58:01 +0900 Subject: [PATCH 206/267] Fix matchmaking chat not working --- osu.Game/Online/Rooms/Room.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 4200fed0dd..dda069bba0 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -358,6 +358,7 @@ namespace osu.Game.Online.Rooms public Room(MultiplayerRoom room) { RoomID = room.RoomID; + ChannelId = room.ChannelID; Name = room.Settings.Name; Password = room.Settings.Password; Type = room.Settings.MatchType; From 183ccbf792001f720166dccf3dfe3b5b15c1fa8e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 19:17:27 +0900 Subject: [PATCH 207/267] Add left/right keybinds to pool selector --- .../Matchmaking/MatchmakingPoolSelector.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs index 43e6acfaf7..8e4d1e8d2b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs @@ -1,11 +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; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -13,6 +15,7 @@ using osu.Game.Online.Matchmaking; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking { @@ -53,6 +56,35 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking }, true); } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Key != Key.Left && e.Key != Key.Right) + return false; + + if (SelectedPool.Value == null) + { + SelectedPool.Value = AvailablePools.Value[0]; + return true; + } + + int currentPoolIndex = Array.IndexOf(AvailablePools.Value, SelectedPool.Value); + + switch (e.Key) + { + case Key.Left: + SelectedPool.Value = currentPoolIndex == 0 + ? AvailablePools.Value[^1] + : AvailablePools.Value[(currentPoolIndex - 1) % AvailablePools.Value.Length]; + break; + + case Key.Right: + SelectedPool.Value = AvailablePools.Value[(currentPoolIndex + 1) % AvailablePools.Value.Length]; + break; + } + + return true; + } + private partial class SelectorButton : CompositeDrawable { public readonly Bindable SelectedPool = new Bindable(); From 3176006510de5ec57a4f9416578e1b6b5c794142 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 19:25:12 +0900 Subject: [PATCH 208/267] Add select keybind to queue screen buttons --- .../Screens/MatchmakingQueueScreen.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs index d23ff9bf84..d90943fdb4 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs @@ -15,11 +15,14 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Matchmaking; @@ -346,7 +349,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens Text = "Found a match!", Font = OsuFont.GetFont(size: 32, weight: FontWeight.Regular, typeface: Typeface.TorusAlternate), }, - new ShearedButton(200) + new SelectionButton(200) { DarkerColour = colours.YellowDark, LighterColour = colours.YellowLight, @@ -436,7 +439,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens InRoom } - private partial class BeginQueueingButton : ShearedButton + private partial class BeginQueueingButton : SelectionButton { public readonly IBindable SelectedPool = new Bindable(); @@ -452,5 +455,29 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens SelectedPool.BindValueChanged(p => Enabled.Value = p.NewValue != null, true); } } + + private partial class SelectionButton : ShearedButton, IKeyBindingHandler + { + public SelectionButton(float? width = null, float height = DEFAULT_HEIGHT) + : base(width, height) + { + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action != GlobalAction.Select) + return false; + + if (e.Repeat) + return true; + + Action(); + return true; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } } } From 3c37fb11be03ea3bf52975e9dbfe83ad9173a32c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Sep 2025 19:47:13 +0900 Subject: [PATCH 209/267] Add sounds --- .../UserInterface/HoverClickSounds.cs | 33 ++++++++++--------- .../Matchmaking/MatchmakingPoolSelector.cs | 19 ++++++++--- .../Screens/MatchmakingQueueScreen.cs | 5 +++ 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs index 884834ebe8..fea33bfa9d 100644 --- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -42,18 +42,20 @@ namespace osu.Game.Graphics.UserInterface this.buttons = buttons ?? new[] { MouseButton.Left }; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select") + ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + + sampleClickDisabled = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select-disabled") + ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select-disabled"); + } + protected override bool OnClick(ClickEvent e) { if (buttons.Contains(e.Button)) - { - var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel(); - - if (channel != null) - { - channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); - channel.Play(); - } - } + PlayClickSample(); return base.OnClick(e); } @@ -66,14 +68,15 @@ namespace osu.Game.Graphics.UserInterface base.PlayHoverSample(); } - [BackgroundDependencyLoader] - private void load(AudioManager audio) + public void PlayClickSample() { - sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select") - ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel(); - sampleClickDisabled = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select-disabled") - ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select-disabled"); + if (channel != null) + { + channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); + channel.Play(); + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs index 8e4d1e8d2b..3a7f57eb40 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs @@ -27,6 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking public readonly Bindable SelectedPool = new Bindable(); private FillFlowContainer poolFlow = null!; + private HoverClickSounds clickSounds = null!; public MatchmakingPoolSelector() { @@ -36,11 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking [BackgroundDependencyLoader] private void load() { - InternalChild = poolFlow = new FillFlowContainer + InternalChildren = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(3) + poolFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3) + }, + clickSounds = new HoverClickSounds(HoverSampleSet.TabSelect) + { + // Click samples are played manually + Alpha = 0 + } }; } @@ -61,6 +70,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking if (e.Key != Key.Left && e.Key != Key.Right) return false; + clickSounds.PlayClickSample(); + if (SelectedPool.Value == null) { SelectedPool.Value = AvailablePools.Value[0]; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs index d90943fdb4..a52528bf93 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs @@ -458,6 +458,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private partial class SelectionButton : ShearedButton, IKeyBindingHandler { + private HoverClickSounds clickSounds = null!; + public SelectionButton(float? width = null, float height = DEFAULT_HEIGHT) : base(width, height) { @@ -471,6 +473,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens if (e.Repeat) return true; + clickSounds.PlayClickSample(); Action(); return true; } @@ -478,6 +481,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens public void OnReleased(KeyBindingReleaseEvent e) { } + + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => clickSounds = (HoverClickSounds)base.CreateHoverSounds(sampleSet); } } } From ff54908687a0cc0e99df0159d45090ffe1d2c952 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Sep 2025 15:16:22 +0900 Subject: [PATCH 210/267] Matchmaking stage display / screen layout design improvements (#35118) --- .../TestSceneBeatmapSelectionGrid.cs | 9 + .../Matchmaking/TestSceneMatchmakingScreen.cs | 2 +- .../Matchmaking/TestSceneStageBubble.cs | 3 +- .../Matchmaking/TestSceneStageDisplay.cs | 30 +- .../Matchmaking/MatchmakingPoolSelector.cs | 90 +++++- .../Matchmaking/MatchmakingScreen.cs | 45 +-- .../OnlinePlay/Matchmaking/PlayerPanel.cs | 16 +- .../Screens/MatchmakingScreenStack.cs | 26 +- .../Matchmaking/Screens/Pick/BeatmapPanel.cs | 34 +-- .../Screens/Pick/BeatmapSelectionPanel.cs | 25 +- .../OnlinePlay/Matchmaking/StageBubble.cs | 172 +++++++---- .../OnlinePlay/Matchmaking/StageDisplay.cs | 270 ++++++++++++++---- .../OnlinePlay/Matchmaking/StageText.cs | 33 ++- 13 files changed, 517 insertions(+), 238 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs index 79ed79e388..e74bcda33d 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs @@ -75,6 +75,15 @@ namespace osu.Game.Tests.Visual.Matchmaking AddWaitStep("wait for panels", 3); } + [Test] + public void TestBasic() + { + AddStep("do nothing", () => + { + // test scene is weird. + }); + } + [Test] public void TestCompleteRollAnimation() { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index b64424db92..d8767fbe85 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestGameplayFlow() { - for (int round = 1; round <= 2; round++) + for (int round = 1; round <= 3; round++) { AddLabel($"Round {round}"); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs index 3cac86b14f..a317121335 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs @@ -23,11 +23,10 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); - AddStep("add bubble", () => Child = new StageBubble(MatchmakingStage.RoundWarmupTime, "Next Round") + AddStep("add bubble", () => Child = new StageBubble(null, MatchmakingStage.RoundWarmupTime, "Next Round") { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 100 }); } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs index 3ec721d432..9fde9f156c 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs @@ -1,12 +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; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Tests.Visual.Multiplayer; @@ -14,6 +15,9 @@ namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneStageDisplay : MultiplayerTestScene { + [Cached] + protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + public override void SetUpSteps() { base.SetUpSteps(); @@ -21,23 +25,37 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); - AddStep("add bubble", () => Child = new StageDisplay + AddStep("add display", () => Child = new StageDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, - Width = 0.5f, }); } [Test] public void TestChangeStage() { - foreach (var stage in Enum.GetValues()) + addStage(MatchmakingStage.WaitingForClientsJoin); + + for (int i = 1; i <= 5; i++) { - AddStep($"{stage}", () => MultiplayerClient.MatchmakingChangeStage(stage).WaitSafely()); - AddWaitStep("wait a bit", 10); + addStage(MatchmakingStage.RoundWarmupTime); + addStage(MatchmakingStage.UserBeatmapSelect); + addStage(MatchmakingStage.ServerBeatmapFinalised); + addStage(MatchmakingStage.WaitingForClientsBeatmapDownload); + addStage(MatchmakingStage.GameplayWarmupTime); + addStage(MatchmakingStage.Gameplay); + addStage(MatchmakingStage.ResultsDisplaying); } + + addStage(MatchmakingStage.Ended); + } + + private void addStage(MatchmakingStage stage) + { + AddStep($"{stage}", () => MultiplayerClient.MatchmakingChangeStage(stage).WaitSafely()); + AddWaitStep("wait a bit", 10); } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs index 43e6acfaf7..8976b1f3b0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs @@ -6,10 +6,12 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Matchmaking; +using osu.Game.Overlays; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -18,7 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { public partial class MatchmakingPoolSelector : CompositeDrawable { - private const float icon_size = 36; + private const float icon_size = 48; public readonly Bindable AvailablePools = new Bindable(); public readonly Bindable SelectedPool = new Bindable(); @@ -35,9 +37,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { InternalChild = poolFlow = new FillFlowContainer { - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = icon_size * 1.2f, Direction = FillDirection.Horizontal, - Spacing = new Vector2(3) + Spacing = new Vector2(5), }; } @@ -48,12 +51,20 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking AvailablePools.BindValueChanged(pools => { poolFlow.Clear(); + foreach (var p in pools.NewValue) - poolFlow.Add(new SelectorButton(p) { SelectedPool = { BindTarget = SelectedPool } }); + { + poolFlow.Add(new SelectorButton(p) + { + SelectedPool = { BindTarget = SelectedPool }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } }, true); } - private partial class SelectorButton : CompositeDrawable + private partial class SelectorButton : OsuAnimatedButton { public readonly Bindable SelectedPool = new Bindable(); @@ -63,6 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private readonly MatchmakingPool pool; private Drawable iconSprite = null!; + private Box flashLayer = null!; + public SelectorButton(MatchmakingPool pool) { this.pool = pool; @@ -71,14 +84,39 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { - InternalChild = new OsuAnimatedButton + Content.Masking = true; + Content.CornerRadius = 20; + Content.CornerExponent = 10; + + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = iconSprite = createIcon(), - Action = () => SelectedPool.Value = pool + new Box + { + Colour = colourProvider.Background2, + Alpha = 0.4f, + RelativeSizeAxes = Axes.Both, + }, + flashLayer = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Children = new[] + { + iconSprite = createIcon(), + } + }, }; + + Action = () => SelectedPool.Value = pool; } protected override void LoadComplete() @@ -89,12 +127,36 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking FinishTransforms(true); } + protected override bool OnHover(HoverEvent e) + { + if (!isSelected) + flashLayer.FadeTo(0.05f, 200, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + if (!isSelected) + flashLayer.FadeTo(0f, 200, Easing.OutQuint); + base.OnHoverLost(e); + } + + private bool isSelected => SelectedPool.Value?.Equals(pool) == true; + private void onSelectionChanged(ValueChangedEvent selection) { - if (selection.NewValue?.Equals(pool) == true) + if (isSelected) + { + this.ScaleTo(1.2f, 200, Easing.OutQuint); iconSprite.FadeColour(Color4.Gold, 100, Easing.OutQuint); + flashLayer.FadeTo(0.1f, 200, Easing.OutQuint); + } else + { + this.ScaleTo(1f, 200, Easing.OutQuint); iconSprite.FadeColour(OsuColour.Gray(0.5f), 100); + flashLayer.FadeOut(200, Easing.OutQuint); + } } private Drawable createIcon() @@ -108,7 +170,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking if (pool.Variant == 0) return icon; - return new BufferedContainer + return new BufferedContainer(pixelSnapping: true) { RelativeSizeAxes = Axes.Both, Children = new[] @@ -118,7 +180,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Size = new Vector2(14, 10), + Size = icon_size * new Vector2(0.4f, 0.28f), Children = new Drawable[] { new Box @@ -130,7 +192,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = $"{pool.Variant}K", - Font = OsuFont.Default.With(size: 8, fixedWidth: true, weight: FontWeight.Bold), + Font = OsuFont.Default.With(size: icon_size * 0.3f, weight: FontWeight.Bold), UseFullGlyphHeight = false, Blending = new BlendingParameters { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs index dd4bb97703..7dde7a480b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,19 +18,16 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Cursor; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; -using osu.Game.Screens.Footer; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Users; -using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking { @@ -78,6 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private CancellationTokenSource? downloadCheckCancellation; private int? lastDownloadCheckedBeatmapId; + private MatchChatDisplay chat = null!; + public MatchmakingScreen(MultiplayerRoom room) { this.room = room; @@ -87,7 +85,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); @@ -107,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Bottom = ScreenFooter.HEIGHT + 20 + Top = row_padding, }, RowDimensions = new[] { @@ -128,7 +126,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + Colour = colourProvider.Background6, }, new MatchmakingScreenStack(), } @@ -138,31 +136,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking [ new Container { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 700, + Height = 130, + Padding = new MarginPadding { Bottom = row_padding }, + Child = chat = new MatchChatDisplay(new Room(room)) { - new Container - { - RelativeSizeAxes = Axes.X, - Height = 100, - Padding = new MarginPadding - { - Horizontal = 200, - }, - Child = new MatchChatDisplay(new Room(room)) - { - RelativeSizeAxes = Axes.Both, - } - }, - new RoundedButton - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Text = "Don't click me", - Size = new Vector2(100, 30), - Action = () => client.MatchmakingSkipToNextStage() - } + RelativeSizeAxes = Axes.Both, } } ] @@ -183,6 +164,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking client.LoadRequested += onLoadRequested; beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); + + Footer!.Add(chat.CreateProxy()); } private void onRoomUpdated() diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs index 42b1edde9b..117893bb8c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs @@ -160,18 +160,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking protected override bool OnHover(HoverEvent e) { - scaleContainer.ScaleTo(1.02f, 1000, Easing.OutQuint); - mainContent.ScaleTo(1.03f, 1000, Easing.OutQuint); + scaleContainer.ScaleTo(1.03f, 750, Easing.OutPow10); + mainContent.ScaleTo(1.03f, 750, Easing.OutPow10); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - scaleContainer.ScaleTo(1f, 500, Easing.OutQuint); - mainContent.ScaleTo(1, 500, Easing.OutQuint); + scaleContainer.ScaleTo(1f, 750, Easing.OutPow10); + mainContent.ScaleTo(1, 750, Easing.OutPow10); - mainContent.MoveTo(Vector2.Zero, 500, Easing.OutElasticHalf); - avatar.MoveTo(avatarPosition, 1500, Easing.OutElastic); + mainContent.MoveTo(Vector2.Zero, 1250, Easing.OutPow10); + avatar.MoveTo(avatarPosition, 1250, Easing.OutPow10); base.OnHoverLost(e); } @@ -179,8 +179,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { var offset = (avatar.ToLocalSpace(e.ScreenSpaceMousePosition) - avatar.DrawSize / 2) * 0.02f; - mainContent.MoveTo(offset * 0.5f, 1000, Easing.OutQuint); - avatar.MoveTo(avatarPosition + offset, 400, Easing.OutQuint); + mainContent.MoveTo(offset * 0.5f, 1000, Easing.OutPow10); + avatar.MoveTo(avatarPosition + offset, 400, Easing.OutPow10); return base.OnMouseMove(e); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs index 0b34beacc7..2e13c59055 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs @@ -28,30 +28,30 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private void load() { RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding(10); InternalChildren = new Drawable[] { - new GridContainer + new Container { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize) }, - Content = new Drawable[][] + Padding = new MarginPadding(10) { - [ - screenStack = new ScreenStack(), - ], - [ - new StageDisplay - { - RelativeSizeAxes = Axes.X - } - ] + Bottom = StageDisplay.HEIGHT, + }, + Children = new Drawable[] + { + screenStack = new ScreenStack(), } }, playersList = new PlayerPanelList { DisplayArea = this + }, + new StageDisplay + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X } }; } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs index d3e5249c73..807c7d3355 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { @@ -58,36 +57,24 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick InternalChildren = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3 - }, - cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0) + cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0, timeBeforeUnload: 10000) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.1f), Color4.White.Opacity(0.3f)) + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal( + colourProvider.Background4.Opacity(0.7f), + colourProvider.Background4.Opacity(0.4f) + ) }, content = new Container { RelativeSizeAxes = Axes.Both, }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 6, - BorderThickness = 2, - BorderColour = ColourInfo.GradientVertical(colourProvider.Background1, colourProvider.Background1.Opacity(0)), - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, - }, OverlayLayer, }; } @@ -146,12 +133,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode), Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), - Shadow = false, RelativeSizeAxes = Axes.X, }, new TextFlowContainer(s => { - s.Shadow = false; s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); }).With(d => { @@ -178,7 +163,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { Text = beatmap.DifficultyName, Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Shadow = false, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs index 029bf48e30..090c275bef 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs @@ -12,7 +12,6 @@ using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick @@ -29,7 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick private readonly BeatmapSelectionOverlay selectionOverlay; private readonly Container border; private readonly Box flash; - private readonly Container shadow; public bool AllowSelection; @@ -54,24 +52,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick Origin = Anchor.Centre, Children = new Drawable[] { - shadow = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(4), - Y = 8, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 7, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.15f, - } - } - }, new Container { RelativeSizeAxes = Axes.Both, @@ -94,6 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { flash = new Box { + Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, Alpha = 0, }, @@ -154,8 +135,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick if (e.Button == MouseButton.Left) { scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); - shadow.MoveToY(4, 400, Easing.OutExpo) - .TransformTo(nameof(Padding), new MarginPadding(2), 400, Easing.OutExpo); return true; } @@ -169,8 +148,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick if (e.Button == MouseButton.Left) { scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); - shadow.MoveToY(8, 500, Easing.OutElasticHalf) - .TransformTo(nameof(Padding), new MarginPadding(4), 400, Easing.OutExpo); } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs index 2ebd3376d3..da6de711ff 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs @@ -5,76 +5,114 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; 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.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osuTK.Graphics; +using osu.Game.Overlays; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking { internal partial class StageBubble : CompositeDrawable { - private readonly Color4 backgroundColour = Color4.Salmon; - [Resolved] private MultiplayerClient client { get; set; } = null!; + public readonly int? Round; + private readonly MatchmakingStage stage; + private readonly LocalisableString displayText; private Drawable progressBar = null!; private DateTimeOffset countdownStartTime; private DateTimeOffset countdownEndTime; + private SpriteIcon arrow = null!; - private Sample? stageProgressSample; + private Sample? countdownTickSample; private double? lastSamplePlayback; - public StageBubble(MatchmakingStage stage, LocalisableString displayText) + public bool Active { get; private set; } + + public float Progress => progressBar.Width; + + public StageBubble(int? round, MatchmakingStage stage, LocalisableString displayText) { + Round = round; this.stage = stage; this.displayText = displayText; - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, OverlayColourProvider colourProvider) { - InternalChild = new CircularContainer + InternalChild = new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Masking = true, - Children = new[] + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] { - new Box + arrow = new SpriteIcon { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour.Darken(0.2f) + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Alpha = 0.5f, + Size = new Vector2(16), + Icon = FontAwesome.Solid.ArrowRight, + Margin = new MarginPadding { Horizontal = 10 } }, - progressBar = new Box + new Container { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = displayText, - Padding = new MarginPadding(10) + Masking = true, + CornerRadius = 5, + CornerExponent = 10, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = + ColourInfo.GradientVertical( + colourProvider.Dark2, + colourProvider.Dark1 + ), + }, + progressBar = new Box + { + Blending = BlendingParameters.Additive, + EdgeSmoothness = new Vector2(1), + RelativeSizeAxes = Axes.Both, + Width = 0, + Colour = colourProvider.Dark3, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = displayText, + Padding = new MarginPadding(10) + } + } } } }; - stageProgressSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); + Alpha = 0.5f; + countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); } protected override void LoadComplete() @@ -97,65 +135,87 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { base.Update(); - TimeSpan duration = countdownEndTime - countdownStartTime; + if (!Active) + return; - if (duration.TotalMilliseconds == 0) - progressBar.Width = 0; - else + TimeSpan total = countdownEndTime - countdownStartTime; + TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; + + if (total.TotalMilliseconds <= 0) { - TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; - progressBar.Width = (float)(elapsed.TotalMilliseconds / duration.TotalMilliseconds); + progressBar.Width = 0; + return; + } - bool enoughTimeElapsed = lastSamplePlayback == null || Time.Current - lastSamplePlayback >= 1000f; - if (elapsed.TotalMilliseconds < 1000f || !enoughTimeElapsed || elapsed.TotalMilliseconds >= duration.TotalMilliseconds) - return; + progressBar.Width = (float)Math.Clamp(elapsed.TotalMilliseconds / total.TotalMilliseconds, 0, 1); - stageProgressSample?.Play(); - lastSamplePlayback = Time.Current; + int secondsRemaining = Math.Max(0, (int)Math.Ceiling((total.TotalMilliseconds - elapsed.TotalMilliseconds) / 1000)); + + if (total.TotalMilliseconds - elapsed.TotalMilliseconds <= 3000 + && lastSamplePlayback != secondsRemaining) + { + countdownTickSample?.Play(); + lastSamplePlayback = secondsRemaining; } } private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => { - if (state is not MatchmakingRoomState matchmakingState) + bool wasActive = Active; + + Active = false; + + if (state is not MatchmakingRoomState roomState) return; - if (matchmakingState.Stage == MatchmakingStage.RoundWarmupTime) + if (Round != null && roomState.CurrentRound != Round) + return; + + Active = stage == roomState.Stage; + + if (wasActive) + progressBar.Width = 1; + + bool isPreparing = + (stage == MatchmakingStage.RoundWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsJoin) || + (stage == MatchmakingStage.GameplayWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsBeatmapDownload) || + (stage == MatchmakingStage.ResultsDisplaying && roomState.Stage == MatchmakingStage.Gameplay); + + if (isPreparing) { - countdownStartTime = countdownEndTime = DateTimeOffset.Now; - activate(); + arrow.FadeTo(1, 500) + .Then() + .FadeTo(0.5f, 500) + .Loop(); } }); private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() => { - if (countdown is not MatchmakingStageCountdown matchmakingStatusCountdown || matchmakingStatusCountdown.Stage != stage) + if (!Active) + return; + + if (countdown is not MatchmakingStageCountdown) return; countdownStartTime = DateTimeOffset.Now; countdownEndTime = countdownStartTime + countdown.TimeRemaining; - activate(); + arrow.FadeIn(500, Easing.OutQuint); + + this.FadeIn(200); }); private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() => { - if (countdown is not MatchmakingStageCountdown matchmakingStatusCountdown || matchmakingStatusCountdown.Stage != stage) + if (!Active) + return; + + if (countdown is not MatchmakingStageCountdown) return; countdownEndTime = DateTimeOffset.Now; - deactivate(); }); - private void activate() - { - this.FadeTo(1, 200); - } - - private void deactivate() - { - this.FadeTo(0.5f, 200); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs index 1f426ec8e6..a5bb72c4b6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs @@ -1,91 +1,249 @@ // 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 System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking { public partial class StageDisplay : CompositeDrawable { - public static readonly (MatchmakingStage status, LocalisableString text)[] DISPLAYED_STAGES = - [ - (MatchmakingStage.RoundWarmupTime, "Next Round"), - (MatchmakingStage.UserBeatmapSelect, "Beatmap Selection"), - (MatchmakingStage.GameplayWarmupTime, "Get Ready"), - (MatchmakingStage.ResultsDisplaying, "Results"), - (MatchmakingStage.Ended, "Match End") - ]; + public const float HEIGHT = 96; + + // TODO: get this from somewhere? + private const int round_count = 5; + + private OsuScrollContainer scroll = null!; + private FillFlowContainer flow = null!; + + private CurrentRoundDisplay roundDisplay = null!; public StageDisplay() { + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { - List columnDimensions = new List(); - List columnContent = new List(); - - for (int i = 0; i < DISPLAYED_STAGES.Length; i++) + InternalChildren = new Drawable[] { - if (i > 0) + new Box { - columnDimensions.Add(new Dimension(GridSizeMode.AutoSize)); - columnContent.Add(new SpriteIcon + Colour = colourProvider.Dark6, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = HEIGHT, + Children = new Drawable[] + { + scroll = new StageScrollContainer + { + ScrollbarOverlapsContent = false, + ScrollbarVisible = false, + ClampExtension = 0, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 36, + Child = flow = new FillFlowContainer + { + Padding = new MarginPadding { Horizontal = 2000 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }, + }, + new StageText + { + Y = 32, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + new Box + { + Colour = ColourInfo.GradientHorizontal( + colourProvider.Dark4, + colourProvider.Dark5.Opacity(0) + ), + RelativeSizeAxes = Axes.Y, + Width = 240, + }, + roundDisplay = new CurrentRoundDisplay + { + X = 12, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + }; + + flow.Add(new StageBubble(null, MatchmakingStage.WaitingForClientsJoin, "Waiting for other users")); + + for (int i = 1; i <= round_count; i++) + { + flow.Add(new StageBubble(i, MatchmakingStage.RoundWarmupTime, "Next Round")); + flow.Add(new StageBubble(i, MatchmakingStage.UserBeatmapSelect, "Beatmap Selection")); + flow.Add(new StageBubble(i, MatchmakingStage.GameplayWarmupTime, "Get Ready")); + flow.Add(new StageBubble(i, MatchmakingStage.ResultsDisplaying, "Results")); + } + + flow.Add(new StageBubble(null, MatchmakingStage.Ended, "Match End")); + } + + protected override void Update() + { + base.Update(); + var bubble = flow.OfType().FirstOrDefault(b => b.Active); + + if (bubble != null) + { + scroll.ScrollTo(flow.Padding.Left + bubble.X + bubble.Progress * bubble.DrawWidth - scroll.DrawWidth / 2); + roundDisplay.Round = bubble.Round; + } + } + + private partial class StageScrollContainer : OsuScrollContainer + { + public override bool HandlePositionalInput => false; + public override bool HandleNonPositionalInput => false; + + public StageScrollContainer() + : base(Direction.Horizontal) + { + } + } + + private partial class CurrentRoundDisplay : CompositeDrawable + { + private OsuSpriteText text = null!; + + private Circle innerCircle = null!; + private CircularProgress progress = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { + Size = new Vector2(76); + + InternalChildren = new Drawable[] + { + new Circle + { + Colour = ColourInfo.GradientVertical( + colours.Dark2, + colours.Dark4 + ), + RelativeSizeAxes = Axes.Both, + }, + progress = new CircularProgress { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(16), - Icon = FontAwesome.Solid.ArrowRight, - Margin = new MarginPadding { Horizontal = 10 } - }); - } - - columnDimensions.Add(new Dimension()); - columnContent.Add(new StageBubble(DISPLAYED_STAGES[i].status, DISPLAYED_STAGES[i].text) - { - RelativeSizeAxes = Axes.X - }); + Colour = ColourInfo.GradientVertical( + colours.Light1, + colours.Dark2 + ), + InnerRadius = 0.1f, + RelativeSizeAxes = Axes.Both, + }, + innerCircle = new Circle + { + Alpha = 0.2f, + Blending = BlendingParameters.Additive, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = ColourInfo.GradientVertical( + colours.Dark1, + colours.Dark2 + ), + Scale = new Vector2(0.9f), + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Y = 10, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Style.Caption2, + Text = "Round", + }, + text = new OsuSpriteText + { + Font = OsuFont.Style.Heading1, + Position = new Vector2(-8, -3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "1" + }, + new OsuSpriteText + { + Font = OsuFont.Style.Heading2, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = 4, + Text = "/" + }, + new OsuSpriteText + { + Font = OsuFont.Style.Heading1, + Position = new Vector2(10, 11), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = $"{round_count}" + }, + }; } - InternalChild = new GridContainer + private int round; + + public int? Round { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - RowDimensions = - [ - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize) - ], - Content = new Drawable[][] + set { - [ - new GridContainer + value ??= 1; + + if (round == value) + return; + + round = value.Value; + + this.ScaleTo(6, 500, Easing.OutQuart) + .MoveToY(-300, 500, Easing.OutQuart) + .Then() + .MoveToY(0, 500, Easing.InQuart) + .ScaleTo(1, 500, Easing.InQuart); + + Scheduler.AddDelayed(() => + { + progress.ProgressTo((float)round / round_count, 500, Easing.InOutQuart); + Scheduler.AddDelayed(() => { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = columnDimensions.ToArray(), - RowDimensions = [new Dimension(GridSizeMode.AutoSize)], - Content = new[] { columnContent.ToArray() } - } - ], - [ - new StageText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - } - ] + innerCircle + .FadeTo(1, 250, Easing.OutQuint) + .Then() + .FadeTo(0.2f, 5000, Easing.OutQuint); + + text.Text = $"{round}"; + }, 150); + }, 250); } - }; + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs index b47e135004..677906ee9b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs @@ -27,7 +27,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking public StageText() { - AutoSizeAxes = Axes.Both; + AutoSizeAxes = Axes.X; + Height = 16; } [BackgroundDependencyLoader] @@ -35,8 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { InternalChild = text = new OsuSpriteText { + Alpha = 0, Height = 16, - Font = OsuFont.Default, + Font = OsuFont.Style.Caption1, AlwaysPresent = true, }; @@ -63,6 +65,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking textChangedSample?.Play(); lastSamplePlayback = Time.Current; + + LocalisableString textForStatus = getTextForStatus(matchmakingState.Stage); + + if (string.IsNullOrEmpty(textForStatus.ToString())) + { + text.FadeOut(); + return; + } + + text.RotateTo(2f) + .RotateTo(0, 500, Easing.OutQuint); + + text.FadeInFromZero(500, Easing.OutQuint); + + using (text.BeginDelayedSequence(500)) + { + text + .FadeTo(0.6f, 400, Easing.In) + .Then() + .FadeTo(1, 400, Easing.Out) + .Loop(); + } + + text.ScaleTo(0.3f) + .ScaleTo(1, 500, Easing.OutQuint); + + text.Text = textForStatus; }); private LocalisableString getTextForStatus(MatchmakingStage status) From 12e29f0bcc9f80c221c5e97693613c8ca4ba94de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Sep 2025 15:55:37 +0900 Subject: [PATCH 211/267] Attempt to fix flaky `TestGameplaySettingsDoesNotExpandWhenSkinOverlayPresent` See https://github.com/ppy/osu/pull/35118/checks?check_run_id=51202910556. --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 02b2db6e31..0e1fa63439 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -278,6 +278,8 @@ namespace osu.Game.Tests.Visual.Navigation { advanceToSongSelect(); openSkinEditor(); + AddUntilStep("skin editor visible", () => skinEditor.State.Value == Visibility.Visible); + AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() }); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -290,8 +292,9 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0)); toggleSkinEditor(); + AddUntilStep("skin editor hidden", () => skinEditor.State.Value == Visibility.Hidden); - AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(1))); + AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(2))); AddUntilStep("settings visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.GreaterThan(0)); AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0))); From df795da0700ca71611dc2a6656f39eb7fc4e975e Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Thu, 25 Sep 2025 10:41:01 +0200 Subject: [PATCH 212/267] Fix composition tool tooltip not changing text when enabled --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b38b0291e8..27ea7863bf 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -266,7 +266,7 @@ namespace osu.Game.Rulesets.Edit { item.Selected.DisabledChanged += isDisabled => { - item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).TooltipText; + item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).Tool.TooltipText; }; } From ef88a3530a414f2085d4000cbe9d6a55c665742b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 25 Sep 2025 11:46:28 -0700 Subject: [PATCH 213/267] Use silver S/SS terminology when grouping by rank/grade in song select --- .../BeatmapCarouselFilterGroupingTest.cs | 2 +- .../SongSelectV2/TestSceneSongSelectGrouping.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 12 +++++++----- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 5f3cd26d55..dcd7a5a8fc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var groupModel = (GroupDefinition)groupItem.Model; - Assert.That(groupModel.Title, Is.EqualTo(expectedTitle)); + Assert.That(groupModel.Title.ToString(), Is.EqualTo(expectedTitle)); Assert.That(itemsInGroup.Select(i => i.Model).OfType().Select(gb => gb.Beatmap), Is.EquivalentTo(expectedBeatmaps)); totalItems += itemsInGroup.Count() + 1; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs index aa80321033..e65c9553c2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs @@ -245,7 +245,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 GroupBy(GroupMode.RankAchieved); WaitForFiltering(); - assertGroupPresent("S+", () => new[] { beatmapSets[0] }); + assertGroupPresent("Silver S", () => new[] { beatmapSets[0] }); assertGroupPresent("A", () => new[] { beatmapSets[1] }); assertGroupPresent("C", () => new[] { beatmapSets[2] }); assertGroupPresent("Unplayed", () => new[] { beatmapSets[3], beatmapSets[4] }); @@ -316,7 +316,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddAssert($"\"{name}\" present", () => { - var group = grouping.GroupItems.Single(g => g.Key.Title == name); + var group = grouping.GroupItems.Single(g => g.Key.Title.ToString() == name); var actualBeatmaps = group.Value.Select(i => i.Model).OfType().Select(gb => gb.Beatmap).OrderBy(b => b.ID); var expectedBeatmaps = getBeatmaps().SelectMany(s => s.Beatmaps).OrderBy(b => b.ID); return actualBeatmaps.SequenceEqual(expectedBeatmaps); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 52d5989c8f..62d37d4b5f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,9 +13,11 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -1064,15 +1066,15 @@ namespace osu.Game.Screens.SelectV2 /// /// The title of this group. /// - public string Title { get; } + public LocalisableString Title { get; } - private readonly string uncasedTitle; + private readonly LocalisableString uncasedTitle; - public GroupDefinition(int order, string title) + public GroupDefinition(int order, LocalisableString title) { Order = order; Title = title; - uncasedTitle = title.ToLowerInvariant(); + uncasedTitle = title.ToLower().ToString(); } public virtual bool Equals(GroupDefinition? other) => uncasedTitle == other?.uncasedTitle; @@ -1083,7 +1085,7 @@ namespace osu.Game.Screens.SelectV2 /// /// Defines a grouping header for a set of carousel items grouped by star difficulty. /// - public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); + public record StarDifficultyGroupDefinition(int Order, LocalisableString Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); /// /// Used to represent a portion of a under a . diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 69f5596578..b3be7c5f16 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -255,7 +255,7 @@ namespace osu.Game.Screens.SelectV2 return groups.Values .OrderBy(g => g.Group!.Order) - .ThenBy(g => g.Group!.Title) + .ThenBy(g => g.Group!.Title.ToString()) .ToList(); } @@ -433,7 +433,7 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary topRankMapping) { if (topRankMapping.TryGetValue(beatmap.ID, out var rank)) - return new GroupDefinition(-(int)rank, rank.GetDescription()).Yield(); + return new GroupDefinition(-(int)rank, rank.GetLocalisableDescription()).Yield(); return new GroupDefinition(int.MaxValue, "Unplayed").Yield(); } From 8ea9e2e4bb3bc8337f3d373179ad73c0b4459082 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 25 Sep 2025 15:37:23 -0700 Subject: [PATCH 214/267] Change non-localisable sh/xh to correct terminology --- osu.Game/Online/Leaderboards/DrawableRank.cs | 2 +- osu.Game/Scoring/ScoreRank.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 3bc80c8b37..d11e200b7c 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -65,7 +65,7 @@ namespace osu.Game.Online.Leaderboards }; } - public static string GetRankName(ScoreRank rank) => rank.GetDescription().TrimEnd('+'); + public static string GetRankName(ScoreRank rank) => rank.GetDescription().Replace("Silver ", ""); /// /// Retrieves the grade text colour. diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs index 957cfc9b95..59a51c0944 100644 --- a/osu.Game/Scoring/ScoreRank.cs +++ b/osu.Game/Scoring/ScoreRank.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring S, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankSH))] - [Description(@"S+")] + [Description(@"Silver S")] // ReSharper disable once InconsistentNaming SH, @@ -43,7 +43,7 @@ namespace osu.Game.Scoring X, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankXH))] - [Description(@"SS+")] + [Description(@"Silver SS")] // ReSharper disable once InconsistentNaming XH, } From e1ba1b45b0b7b32410ef4302044e71fe833edc0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 12:17:28 +0900 Subject: [PATCH 215/267] Adjust matchmaking naming, namespaces, xmldoc (#35123) * Adjust matchmaking naming, namespaces, xmldoc * Change partial filenames to use `.` instead of `_` separator --- ...OsuHitObjectGenerationUtils.Reposition.cs} | 0 .../Matchmaking/TestSceneBeatmapPanel.cs | 28 - .../TestSceneBeatmapSelectionOverlay.cs | 68 --- .../Visual/Matchmaking/TestSceneIdleScreen.cs | 4 +- .../Matchmaking/TestSceneMatchmakingCloud.cs | 6 +- .../TestSceneMatchmakingPoolSelector.cs | 4 +- .../TestSceneMatchmakingQueueScreen.cs | 20 +- .../Matchmaking/TestSceneMatchmakingScreen.cs | 6 +- .../TestSceneMatchmakingScreenStack.cs | 4 +- ...ticPanel.cs => TestScenePanelRoomAward.cs} | 6 +- .../Visual/Matchmaking/TestScenePickScreen.cs | 6 +- .../Matchmaking/TestScenePlayerPanel.cs | 6 +- .../Matchmaking/TestSceneResultsScreen.cs | 4 +- .../TestSceneRoundResultsScreen.cs | 4 +- ...ctionGrid.cs => TestSceneSelectionGrid.cs} | 12 +- ...ionPanel.cs => TestSceneSelectionPanel.cs} | 8 +- .../Matchmaking/TestSceneStageDisplay.cs | 2 +- ...tageBubble.cs => TestSceneStageSegment.cs} | 6 +- ...eneStageText.cs => TestSceneStatusText.cs} | 6 +- ...elList.cs => TestSceneUserPanelOverlay.cs} | 18 +- ...ntainer.cs => Carousel.ScrollContainer.cs} | 0 osu.Game/OsuGame.cs | 5 +- ..._Importing.cs => OsuGameBase.Importing.cs} | 0 ...cs => KeyBindingRow.ConflictResolution.cs} | 0 ...eyButton.cs => KeyBindingRow.KeyButton.cs} | 0 ...fficultyCalculationUtils.ErrorFunction.cs} | 0 osu.Game/Screens/Menu/MainMenu.cs | 3 +- .../ScreenIntro.cs} | 12 +- .../BeatmapSelect/SelectionGrid.cs} | 22 +- .../Match/BeatmapSelect/SelectionPanel.cs | 502 ++++++++++++++++++ .../BeatmapSelect/SubScreenBeatmapSelect.cs} | 12 +- .../Gameplay/ScreenGameplay.cs} | 6 +- .../{ => Match}/MatchmakingAvatar.cs | 6 +- .../MatchmakingSubScreen.cs | 4 +- .../MatchmakingUserPanel.cs} | 10 +- .../Results/PanelRoomAward.cs} | 6 +- .../Results/PanelUserStatistic.cs} | 6 +- .../Results/SubScreenResults.cs} | 23 +- .../RoundResults/SubScreenRoundResults.cs} | 36 +- .../RoundWarmup/SubScreenRoundWarmup.cs} | 10 +- .../Match/ScreenMatchmaking.ScreenStack.cs | 133 +++++ .../ScreenMatchmaking.cs} | 15 +- .../Match/StageDisplay.StageSegment.cs | 234 ++++++++ .../Match/StageDisplay.StatusText.cs | 129 +++++ .../Matchmaking/{ => Match}/StageDisplay.cs | 21 +- .../UserPanelOverlay.cs} | 34 +- .../CloudVisualisation.cs} | 9 +- .../PoolSelector.cs} | 6 +- .../QueueController.cs} | 30 +- .../ScreenQueue.cs} | 18 +- .../Screens/MatchmakingScreenStack.cs | 130 ----- .../Matchmaking/Screens/Pick/BeatmapPanel.cs | 176 ------ .../Screens/Pick/BeatmapSelectionOverlay.cs | 157 ------ .../Screens/Pick/BeatmapSelectionPanel.cs | 190 ------- .../RoundResults/RoundResultsScorePanel.cs | 36 -- .../OnlinePlay/Matchmaking/StageBubble.cs | 231 -------- .../OnlinePlay/Matchmaking/StageText.cs | 126 ----- ...{PlayerGrid_Cell.cs => PlayerGrid.Cell.cs} | 0 ...er.cs => UserTagControl.AddTagsPopover.cs} | 0 ...g.cs => UserTagControl.DrawableUserTag.cs} | 0 ...Header.cs => BeatmapDetailsArea.Header.cs} | 0 ...cs => BeatmapDetailsArea.WedgeSelector.cs} | 0 ....cs => BeatmapLeaderboardScore.Tooltip.cs} | 0 ... BeatmapMetadataWedge.FailRetryDisplay.cs} | 0 ...> BeatmapMetadataWedge.MetadataDisplay.cs} | 0 ...atmapMetadataWedge.RatingSpreadDisplay.cs} | 0 ...eatmapMetadataWedge.SuccessRateDisplay.cs} | 0 ...ne.cs => BeatmapMetadataWedge.TagsLine.cs} | 0 ...BeatmapMetadataWedge.UserRatingDisplay.cs} | 0 ...=> BeatmapTitleWedge.DifficultyDisplay.cs} | 0 ...TitleWedge.DifficultyStatisticsDisplay.cs} | 0 ...s => BeatmapTitleWedge.FavouriteButton.cs} | 0 ...stic.cs => BeatmapTitleWedge.Statistic.cs} | 0 ... BeatmapTitleWedge.StatisticDifficulty.cs} | 0 ...> BeatmapTitleWedge.StatisticPlayCount.cs} | 0 ...=> FilterControl.DifficultyRangeSlider.cs} | 0 ...over.cs => FooterButtonOptions.Popover.cs} | 0 77 files changed, 1244 insertions(+), 1312 deletions(-) rename osu.Game.Rulesets.Osu/Utils/{OsuHitObjectGenerationUtils_Reposition.cs => OsuHitObjectGenerationUtils.Reposition.cs} (100%) delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs rename osu.Game.Tests/Visual/Matchmaking/{TestSceneRoomStatisticPanel.cs => TestScenePanelRoomAward.cs} (66%) rename osu.Game.Tests/Visual/Matchmaking/{TestSceneBeatmapSelectionGrid.cs => TestSceneSelectionGrid.cs} (93%) rename osu.Game.Tests/Visual/Matchmaking/{TestSceneBeatmapSelectionPanel.cs => TestSceneSelectionPanel.cs} (85%) rename osu.Game.Tests/Visual/Matchmaking/{TestSceneStageBubble.cs => TestSceneStageSegment.cs} (84%) rename osu.Game.Tests/Visual/Matchmaking/{TestSceneStageText.cs => TestSceneStatusText.cs} (84%) rename osu.Game.Tests/Visual/Matchmaking/{TestScenePlayerPanelList.cs => TestSceneUserPanelOverlay.cs} (89%) rename osu.Game/Graphics/Carousel/{Carousel_ScrollContainer.cs => Carousel.ScrollContainer.cs} (100%) rename osu.Game/{OsuGameBase_Importing.cs => OsuGameBase.Importing.cs} (100%) rename osu.Game/Overlays/Settings/Sections/Input/{KeyBindingRow_ConflictResolution.cs => KeyBindingRow.ConflictResolution.cs} (100%) rename osu.Game/Overlays/Settings/Sections/Input/{KeyBindingRow_KeyButton.cs => KeyBindingRow.KeyButton.cs} (100%) rename osu.Game/Rulesets/Difficulty/Utils/{DifficultyCalculationUtils_ErrorFunction.cs => DifficultyCalculationUtils.ErrorFunction.cs} (100%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/MatchmakingIntroScreen.cs => Intro/ScreenIntro.cs} (96%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Pick/BeatmapSelectionGrid.cs => Match/BeatmapSelect/SelectionGrid.cs} (94%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Pick/PickScreen.cs => Match/BeatmapSelect/SubScreenBeatmapSelect.cs} (87%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingPlayer.cs => Match/Gameplay/ScreenGameplay.cs} (77%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{ => Match}/MatchmakingAvatar.cs (87%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens => Match}/MatchmakingSubScreen.cs (88%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{PlayerPanel.cs => Match/MatchmakingUserPanel.cs} (94%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Results/RoomStatisticPanel.cs => Match/Results/PanelRoomAward.cs} (89%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Results/UserStatisticPanel.cs => Match/Results/PanelUserStatistic.cs} (87%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Results/ResultsScreen.cs => Match/Results/SubScreenResults.cs} (95%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/RoundResults/RoundResultsScreen.cs => Match/RoundResults/SubScreenRoundResults.cs} (82%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Idle/IdleScreen.cs => Match/RoundWarmup/SubScreenRoundWarmup.cs} (52%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingScreen.cs => Match/ScreenMatchmaking.cs} (95%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StatusText.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{ => Match}/StageDisplay.cs (90%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{PlayerPanelList.cs => Match/UserPanelOverlay.cs} (91%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingCloud.cs => Queue/CloudVisualisation.cs} (92%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingPoolSelector.cs => Queue/PoolSelector.cs} (97%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingController.cs => Queue/QueueController.cs} (79%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/MatchmakingQueueScreen.cs => Queue/ScreenQueue.cs} (96%) delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{PlayerGrid_Cell.cs => PlayerGrid.Cell.cs} (100%) rename osu.Game/Screens/Ranking/{UserTagControl_AddTagsPopover.cs => UserTagControl.AddTagsPopover.cs} (100%) rename osu.Game/Screens/Ranking/{UserTagControl_DrawableUserTag.cs => UserTagControl.DrawableUserTag.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapDetailsArea_Header.cs => BeatmapDetailsArea.Header.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapDetailsArea_WedgeSelector.cs => BeatmapDetailsArea.WedgeSelector.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapLeaderboardScore_Tooltip.cs => BeatmapLeaderboardScore.Tooltip.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_FailRetryDisplay.cs => BeatmapMetadataWedge.FailRetryDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_MetadataDisplay.cs => BeatmapMetadataWedge.MetadataDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_RatingSpreadDisplay.cs => BeatmapMetadataWedge.RatingSpreadDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_SuccessRateDisplay.cs => BeatmapMetadataWedge.SuccessRateDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_TagsLine.cs => BeatmapMetadataWedge.TagsLine.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_UserRatingDisplay.cs => BeatmapMetadataWedge.UserRatingDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_DifficultyDisplay.cs => BeatmapTitleWedge.DifficultyDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_DifficultyStatisticsDisplay.cs => BeatmapTitleWedge.DifficultyStatisticsDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_FavouriteButton.cs => BeatmapTitleWedge.FavouriteButton.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_Statistic.cs => BeatmapTitleWedge.Statistic.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_StatisticDifficulty.cs => BeatmapTitleWedge.StatisticDifficulty.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_StatisticPlayCount.cs => BeatmapTitleWedge.StatisticPlayCount.cs} (100%) rename osu.Game/Screens/SelectV2/{FilterControl_DifficultyRangeSlider.cs => FilterControl.DifficultyRangeSlider.cs} (100%) rename osu.Game/Screens/SelectV2/{FooterButtonOptions_Popover.cs => FooterButtonOptions.Popover.cs} (100%) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.Reposition.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs rename to osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.Reposition.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs deleted file mode 100644 index c46beba037..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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.Graphics; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; -using osu.Game.Tests.Visual.Multiplayer; -using osuTK; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneBeatmapPanel : MultiplayerTestScene - { - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("add beatmap panel", () => - { - Child = new BeatmapPanel(CreateAPIBeatmap()) - { - Size = new Vector2(300, 70), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs deleted file mode 100644 index 4e596d65cc..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs +++ /dev/null @@ -1,68 +0,0 @@ -// 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.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; -using osuTK; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneBeatmapSelectionOverlay : OsuTestScene - { - private BeatmapSelectionOverlay selectionOverlay = null!; - - [SetUpSteps] - public void SetupSteps() - { - AddStep("add drawable", () => Child = new Container - { - Width = 100, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(2), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f, - }, - selectionOverlay = new BeatmapSelectionOverlay - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - } - }); - } - - [Test] - public void TestSelectionOverlay() - { - AddStep("add maarvin", () => selectionOverlay.AddUser(new APIUser - { - Id = 6411631, - Username = "Maarvin", - }, isOwnUser: true)); - AddStep("add peppy", () => selectionOverlay.AddUser(new APIUser - { - Id = 2, - Username = "peppy", - }, false)); - AddStep("add smogipoo", () => selectionOverlay.AddUser(new APIUser - { - Id = 1040328, - Username = "smoogipoo", - }, false)); - AddStep("remove smogipoo", () => selectionOverlay.RemoveUser(1040328)); - AddStep("remove peppy", () => selectionOverlay.RemoveUser(2)); - AddStep("remove maarvin", () => selectionOverlay.RemoveUser(6411631)); - } - } -} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs index 174a77390e..08df61d629 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs @@ -11,7 +11,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundWarmup; using osu.Game.Tests.Visual.Multiplayer; using osuTK; @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Matchmaking return (user, 0); }).ToArray(); - Child = new ScreenStack(new IdleScreen()) + Child = new ScreenStack(new SubScreenRoundWarmup()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs index c25057c84b..d656971b5a 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs @@ -6,20 +6,20 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Users; namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneMatchmakingCloud : OsuTestScene { - private MatchmakingCloud cloud = null!; + private CloudVisualisation cloud = null!; protected override void LoadComplete() { base.LoadComplete(); - Child = cloud = new MatchmakingCloud + Child = cloud = new CloudVisualisation { RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs index 442a06606b..5971cd9091 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs @@ -3,7 +3,7 @@ using osu.Framework.Graphics; using osu.Game.Online.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("add selector", () => Child = new MatchmakingPoolSelector + AddStep("add selector", () => Child = new PoolSelector { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs index 72eba6e1c8..5193d58ee6 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs @@ -7,8 +7,8 @@ using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.OnlinePlay.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Intro; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; @@ -17,16 +17,16 @@ namespace osu.Game.Tests.Visual.Matchmaking public partial class TestSceneMatchmakingQueueScreen : MultiplayerTestScene { [Cached] - private readonly MatchmakingController controller = new MatchmakingController(); + private readonly QueueController controller = new QueueController(); - private MatchmakingQueueScreen? queueScreen => Stack.CurrentScreen as MatchmakingQueueScreen; + private ScreenQueue? queueScreen => Stack.CurrentScreen as ScreenQueue; [SetUpSteps] public override void SetUpSteps() { base.SetUpSteps(); - AddStep("load screen", () => LoadScreen(new MatchmakingIntroScreen())); + AddStep("load screen", () => LoadScreen(new IntroScreen())); } [Test] @@ -44,15 +44,15 @@ namespace osu.Game.Tests.Visual.Matchmaking }).ToArray(); }); - AddStep("change state to idle", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.Idle)); + AddStep("change state to idle", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.Idle)); - AddStep("change state to queueing", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.Queueing)); + AddStep("change state to queueing", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.Queueing)); - AddStep("change state to found match", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.PendingAccept)); + AddStep("change state to found match", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.PendingAccept)); - AddStep("change state to waiting for room", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.AcceptedWaitingForRoom)); + AddStep("change state to waiting for room", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.AcceptedWaitingForRoom)); - AddStep("change state to in room", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.InRoom)); + AddStep("change state to in room", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.InRoom)); } } } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index d8767fbe85..2269e1c76c 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -16,7 +16,7 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Matchmaking private const int beatmap_count = 50; private MultiplayerRoomUser[] users = null!; - private MatchmakingScreen screen = null!; + private ScreenMatchmaking screen = null!; public override void SetUpSteps() { @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Matchmaking StarRating = i / 10.0 }).ToArray(); - LoadScreen(screen = new MatchmakingScreen(new MultiplayerRoom(0) + LoadScreen(screen = new ScreenMatchmaking(new MultiplayerRoom(0) { Users = users, Playlist = beatmaps diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs index 94547dd115..ba7e27b753 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs @@ -11,7 +11,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("add carousel", () => { - Child = new MatchmakingScreenStack + Child = new ScreenMatchmaking.ScreenStack { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs similarity index 66% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs rename to osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs index 494f9b6517..494d1c411a 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs @@ -2,18 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneRoomStatisticPanel : MultiplayerTestScene + public partial class TestScenePanelRoomAward : MultiplayerTestScene { public override void SetUpSteps() { base.SetUpSteps(); - AddStep("add statistic", () => Child = new RoomStatisticPanel("Statistic description", 1) + AddStep("add statistic", () => Child = new PanelRoomAward("Statistic description", 1) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index 1c12b4d2d0..e894616f9e 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -10,7 +10,7 @@ using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking @@ -79,9 +79,9 @@ namespace osu.Game.Tests.Visual.Matchmaking { var selectedItems = new List(); - PickScreen screen = null!; + SubScreenBeatmapSelect screen = null!; - AddStep("add screen", () => Child = new ScreenStack(screen = new PickScreen())); + AddStep("add screen", () => Child = new ScreenStack(screen = new SubScreenBeatmapSelect())); AddStep("select maps", () => { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index 805e83a76d..a7c14cfd94 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -8,7 +8,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestScenePlayerPanel : MultiplayerTestScene { - private PlayerPanel panel = null!; + private MatchmakingUserPanel panel = null!; public override void SetUpSteps() { @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); - AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1) + AddStep("add panel", () => Child = panel = new MatchmakingUserPanel(new MultiplayerRoomUser(1) { User = new APIUser { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs index 6b3d42694b..d445c46a48 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -10,7 +10,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; using osu.Game.Tests.Visual.Multiplayer; using osuTK; @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("add results screen", () => { - Child = new ScreenStack(new ResultsScreen()) + Child = new ScreenStack(new SubScreenResults()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs index 561c994945..cbdbd33158 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs @@ -13,7 +13,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults; using osu.Game.Tests.Visual.Multiplayer; using osuTK; @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("load screen", () => { - Child = new ScreenStack(new RoundResultsScreen()) + Child = new ScreenStack(new SubScreenRoundResults()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs similarity index 93% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs index e74bcda33d..6fba5af070 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs @@ -11,17 +11,17 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.OnlinePlay; using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneBeatmapSelectionGrid : OnlinePlayTestScene + public partial class TestSceneSelectionGrid : OnlinePlayTestScene { private MultiplayerPlaylistItem[] items = null!; - private BeatmapSelectionGrid grid = null!; + private SelectionGrid grid = null!; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("add grid", () => Child = grid = new BeatmapSelectionGrid + AddStep("add grid", () => Child = grid = new SelectionGrid { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.Matchmaking var (candidateItems, _) = pickRandomItems(count); grid.TransferCandidatePanelsToRollContainer(candidateItems); - grid.Delay(BeatmapSelectionGrid.ARRANGE_DELAY) + grid.Delay(SelectionGrid.ARRANGE_DELAY) .Schedule(() => grid.ArrangeItemsForRollAnimation()); }); @@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("display roll order", () => { - var panels = grid.ChildrenOfType().ToArray(); + var panels = grid.ChildrenOfType().ToArray(); for (int i = 0; i < panels.Length; i++) { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs similarity index 85% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs index addb0ed3a0..6745802b30 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs @@ -7,12 +7,12 @@ using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneBeatmapSelectionPanel : MultiplayerTestScene + public partial class TestSceneSelectionPanel : MultiplayerTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -20,9 +20,9 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestBeatmapPanel() { - BeatmapSelectionPanel? panel = null; + SelectionPanel? panel = null; - AddStep("add panel", () => Child = panel = new BeatmapSelectionPanel(new MultiplayerPlaylistItem()) + AddStep("add panel", () => Child = panel = new SelectionPanel(new MultiplayerPlaylistItem()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs index 9fde9f156c..dc4f09c555 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Overlays; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageSegment.cs similarity index 84% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneStageSegment.cs index a317121335..c9d74cc99d 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageSegment.cs @@ -9,12 +9,12 @@ using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneStageBubble : MultiplayerTestScene + public partial class TestSceneStageSegment : MultiplayerTestScene { public override void SetUpSteps() { @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); - AddStep("add bubble", () => Child = new StageBubble(null, MatchmakingStage.RoundWarmupTime, "Next Round") + AddStep("add bubble", () => Child = new StageDisplay.StageSegment(null, MatchmakingStage.RoundWarmupTime, "Next Round") { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStatusText.cs similarity index 84% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneStatusText.cs index bc465f53a4..26380152b1 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStatusText.cs @@ -7,12 +7,12 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneStageText : MultiplayerTestScene + public partial class TestSceneStatusText : MultiplayerTestScene { public override void SetUpSteps() { @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); - AddStep("create display", () => Child = new StageText + AddStep("create display", () => Child = new StageDisplay.StatusText { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs similarity index 89% rename from osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs index 5e43a37e07..9ed233a507 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelList.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs @@ -11,15 +11,15 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestScenePlayerPanelList : MultiplayerTestScene + public partial class TestSceneUserPanelOverlay : MultiplayerTestScene { - private PlayerPanelList list = null!; + private UserPanelOverlay list = null!; public override void SetUpSteps() { @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Matchmaking Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Size = new Vector2(0.8f), - Child = list = new PlayerPanelList() + Child = list = new UserPanelOverlay() }); } @@ -55,15 +55,15 @@ namespace osu.Game.Tests.Visual.Matchmaking } }); - AddStep("change to split mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Split); - AddStep("change to grid mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Grid); - AddStep("change to hidden mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Hidden); + AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split); + AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid); + AddStep("change to hidden mode", () => list.DisplayStyle = PanelDisplayStyle.Hidden); } [Test] public void AddPanelsGrid() { - AddStep("change to grid mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Grid); + AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid); int userId = 0; @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void AddPanelsSplit() { - AddStep("change to split mode", () => list.DisplayStyle = PlayerPanelList.PanelDisplayStyle.Split); + AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split); int userId = 0; diff --git a/osu.Game/Graphics/Carousel/Carousel_ScrollContainer.cs b/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs similarity index 100% rename from osu.Game/Graphics/Carousel/Carousel_ScrollContainer.cs rename to osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d610bd64d5..4dd42b7fd2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -65,7 +65,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.DailyChallenge; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; @@ -80,6 +80,7 @@ using osu.Game.Utils; using osuTK; using osuTK.Graphics; using Sentry; +using IntroScreen = osu.Game.Screens.Menu.IntroScreen; using MatchType = osu.Game.Online.Rooms.MatchType; namespace osu.Game @@ -1271,7 +1272,7 @@ namespace osu.Game loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(detachedBeatmapStore = new RealmDetachedBeatmapStore(), Add, true); - loadComponentSingleFile(new MatchmakingController(), Add, true); + loadComponentSingleFile(new QueueController(), Add, true); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); diff --git a/osu.Game/OsuGameBase_Importing.cs b/osu.Game/OsuGameBase.Importing.cs similarity index 100% rename from osu.Game/OsuGameBase_Importing.cs rename to osu.Game/OsuGameBase.Importing.cs diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs similarity index 100% rename from osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs rename to osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs similarity index 100% rename from osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs rename to osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.ErrorFunction.cs similarity index 100% rename from osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs rename to osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.ErrorFunction.cs diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c74b60c5d7..c4ba3145b5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -37,7 +37,6 @@ using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.OnlinePlay.DailyChallenge; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.SelectV2; @@ -483,7 +482,7 @@ namespace osu.Game.Screens.Menu private void loadSongSelect() => this.Push(new SoloSongSelect()); - private void joinOrLeaveMatchmakingQueue() => this.Push(new MatchmakingIntroScreen()); + private void joinOrLeaveMatchmakingQueue() => this.Push(new OnlinePlay.Matchmaking.Intro.IntroScreen()); private partial class MobileDisclaimerDialog : PopupDialog { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs similarity index 96% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs index 34c113c39f..b3fff7dc00 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs @@ -14,10 +14,14 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro { - public partial class MatchmakingIntroScreen : OsuScreen + /// + /// A brief intro animation that introduces matchmaking to the user. + /// + public partial class IntroScreen : OsuScreen { public override bool DisallowExternalBeatmapRulesetChanges => false; @@ -51,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens protected override BackgroundScreen CreateBackground() => new MatchmakingIntroBackgroundScreen(colourProvider); - public MatchmakingIntroScreen() + public IntroScreen() { ValidForResume = false; } @@ -191,7 +195,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens Schedule(() => { if (this.IsCurrentScreen()) - this.Push(new MatchmakingQueueScreen()); + this.Push(new ScreenQueue()); }); } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs index 813e8efa0d..bd75514b30 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs @@ -20,9 +20,9 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class BeatmapSelectionGrid : CompositeDrawable + public partial class SelectionGrid : CompositeDrawable { public const double ARRANGE_DELAY = 200; @@ -37,10 +37,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly Dictionary panelLookup = new Dictionary(); + private readonly Dictionary panelLookup = new Dictionary(); private readonly PanelGridContainer panelGridContainer; - private readonly Container rollContainer; + private readonly Container rollContainer; private readonly OsuScrollContainer scroll; private bool allowSelection = true; @@ -51,7 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick private Sample? swooshSample; private double? lastSamplePlayback; - public BeatmapSelectionGrid() + public SelectionGrid() { InternalChildren = new Drawable[] { @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick Spacing = new Vector2(panel_spacing) }, }, - rollContainer = new Container + rollContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick public void AddItem(MultiplayerPlaylistItem item) { - var panel = panelLookup[item.ID] = new BeatmapSelectionPanel(item) + var panel = panelLookup[item.ID] = new SelectionPanel(item) { Size = new Vector2(300, 70), AllowSelection = allowSelection, @@ -176,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick var rng = new Random(); - var remainingPanels = new List(); + var remainingPanels = new List(); foreach (var panel in panelGridContainer.Children.ToArray()) { @@ -216,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { var panel = rollContainer.Children[i]; - var position = positions[i] * (BeatmapPanel.SIZE + new Vector2(panel_spacing)); + var position = positions[i] * (SelectionPanel.SIZE + new Vector2(panel_spacing)); panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); @@ -285,7 +285,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex) numSteps++; - BeatmapSelectionPanel? lastPanel = null; + SelectionPanel? lastPanel = null; for (int i = 0; i < numSteps; i++) { @@ -346,7 +346,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick PresentRolledBeatmap(finalItem); } - private partial class PanelGridContainer : FillFlowContainer + private partial class PanelGridContainer : FillFlowContainer { public bool LayoutDisabled; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs new file mode 100644 index 0000000000..1a51ddac64 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs @@ -0,0 +1,502 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions; +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.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class SelectionPanel : Container + { + public static readonly Vector2 SIZE = new Vector2(300, 70); + + private const float corner_radius = 6; + private const float border_width = 3; + + public readonly MultiplayerPlaylistItem Item; + + private readonly Container scaleContainer; + private readonly BeatmapPanel beatmapPanel; + private readonly AvatarOverlay selectionOverlay; + private readonly Container border; + private readonly Box flash; + + public bool AllowSelection; + + public Action? Action; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + public override bool PropagatePositionalInputSubTree => AllowSelection; + + public SelectionPanel(MultiplayerPlaylistItem item) + { + Item = item; + Size = SIZE; + + InternalChildren = new Drawable[] + { + scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-border_width), + Child = border = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius + border_width, + Alpha = 0, + Child = new Box { RelativeSizeAxes = Axes.Both }, + } + }, + beatmapPanel = new BeatmapPanel + { + RelativeSizeAxes = Axes.Both, + OverlayLayer = + { + Children = new[] + { + flash = new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + } + } + }, + selectionOverlay = new AvatarOverlay + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10 }, + Origin = Anchor.CentreLeft, + }, + } + }, + new HoverClickSounds(), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => + { + var beatmap = b.GetResultSafely()!; + + beatmap.StarRating = Item.StarRating; + + beatmapPanel.Beatmap = beatmap; + })); + } + + public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser); + + public bool RemoveUser(int userId) => selectionOverlay.RemoveUser(userId); + + public bool RemoveUser(APIUser user) => RemoveUser(user.Id); + + protected override bool OnHover(HoverEvent e) + { + flash.FadeTo(0.2f, 50) + .Then() + .FadeTo(0.1f, 300); + + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + + flash.FadeOut(200); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + { + scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); + return true; + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + base.OnMouseUp(e); + + if (e.Button == MouseButton.Left) + { + scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); + } + } + + protected override bool OnClick(ClickEvent e) + { + Action?.Invoke(Item); + + flash.FadeTo(0.5f, 50) + .Then() + .FadeTo(0.1f, 400); + + return true; + } + + public void ShowBorder() => border.Show(); + + public void HideBorder() => border.Hide(); + + public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) + { + scaleContainer + .FadeOut() + .MoveToY(distance) + .Delay(delay) + .FadeIn(duration / 2) + .MoveToY(0, duration, Easing.OutExpo); + } + + public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic) + { + AllowSelection = false; + + scaleContainer.Delay(delay) + .ScaleTo(0, duration, easing) + .FadeOut(duration); + + this.Delay(delay + duration).FadeOut().Expire(); + } + + // TODO: combine following two classes with above implementation for simplicity? + private partial class BeatmapPanel : CompositeDrawable + { + public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both }; + + public APIBeatmap? Beatmap + { + get => beatmap; + set + { + if (beatmap?.OnlineID == value?.OnlineID) + return; + + beatmap = value; + + if (IsLoaded) + updateContent(); + } + } + + private APIBeatmap? beatmap; + + private Container content = null!; + private UpdateableOnlineBeatmapSetCover cover = null!; + + public BeatmapPanel(APIBeatmap? beatmap = null) + { + this.beatmap = beatmap; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Masking = true; + CornerRadius = 6; + + InternalChildren = new Drawable[] + { + cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0, timeBeforeUnload: 10000) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal( + colourProvider.Background4.Opacity(0.7f), + colourProvider.Background4.Opacity(0.4f) + ) + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + }, + OverlayLayer, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateContent(); + FinishTransforms(true); + } + + private void updateContent() + { + foreach (var child in content.Children) + child.FadeOut(300).Expire(); + + cover.OnlineInfo = beatmap?.BeatmapSet; + + if (beatmap != null) + { + var panelContent = new BeatmapPanelContent(beatmap) + { + RelativeSizeAxes = Axes.Both, + }; + + content.Add(panelContent); + + panelContent.FadeInFromZero(300); + } + } + + private partial class BeatmapPanelContent : CompositeDrawable + { + private readonly APIBeatmap beatmap; + + public BeatmapPanelContent(APIBeatmap beatmap) + { + this.beatmap = beatmap; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Horizontal = 12 }, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode), + Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + new TextFlowContainer(s => + { + s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); + }).With(d => + { + d.RelativeSizeAxes = Axes.X; + d.AutoSizeAxes = Axes.Y; + d.AddText("by "); + d.AddText(new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); + }), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Top = 6 }, + Spacing = new Vector2(4), + Children = new Drawable[] + { + new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + }, + }; + } + } + } + + private partial class AvatarOverlay : CompositeDrawable + { + private readonly Dictionary avatars = new Dictionary(); + + private readonly Container avatarContainer; + + private Sample? userAddedSample; + private double? lastSamplePlayback; + + public new Axes AutoSizeAxes + { + get => base.AutoSizeAxes; + set => base.AutoSizeAxes = value; + } + + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + public AvatarOverlay() + { + InternalChild = avatarContainer = new Container(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + avatarContainer.AutoSizeAxes = AutoSizeAxes; + avatarContainer.RelativeSizeAxes = RelativeSizeAxes; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); + } + + public bool AddUser(APIUser user, bool isOwnUser) + { + if (avatars.ContainsKey(user.Id)) + return false; + + var avatar = new SelectionAvatar(user, isOwnUser) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }; + + avatarContainer.Add(avatars[user.Id] = avatar); + + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) + { + userAddedSample?.Play(); + lastSamplePlayback = Time.Current; + } + + updateLayout(); + + avatar.FinishTransforms(); + + return true; + } + + public bool RemoveUser(int id) + { + if (!avatars.Remove(id, out var avatar)) + return false; + + avatar.PopOutAndExpire(); + avatarContainer.ChangeChildDepth(avatar, float.MaxValue); + + updateLayout(); + + return true; + } + + private void updateLayout() + { + const double stagger = 30; + const float spacing = 4; + + double delay = 0; + float x = 0; + + for (int i = avatarContainer.Count - 1; i >= 0; i--) + { + var avatar = avatarContainer[i]; + + if (avatar.Expired) + continue; + + avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); + + x -= avatar.LayoutSize.X + spacing; + + delay += stagger; + } + } + + public partial class SelectionAvatar : CompositeDrawable + { + public bool Expired { get; private set; } + + private readonly Container content; + + public SelectionAvatar(APIUser user, bool isOwnUser) + { + Size = new Vector2(30); + + InternalChildren = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = new MatchmakingAvatar(user, isOwnUser) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + content.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + } + + public void PopOutAndExpire() + { + content.ScaleTo(0, 400, Easing.OutExpo); + + this.FadeOut(100).Expire(); + Expired = true; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs similarity index 87% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index 96cfa67642..cf86deeb3e 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -9,19 +9,19 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class PickScreen : MatchmakingSubScreen + public partial class SubScreenBeatmapSelect : MatchmakingSubScreen { - public override PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle => PlayerPanelList.PanelDisplayStyle.Split; + public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Split; public override Drawable PlayersDisplayArea { get; } - private readonly BeatmapSelectionGrid selectionGrid; + private readonly SelectionGrid selectionGrid; [Resolved] private MultiplayerClient client { get; set; } = null!; - public PickScreen() + public SubScreenBeatmapSelect() { InternalChildren = new Drawable[] { @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 200 }, - Child = selectionGrid = new BeatmapSelectionGrid + Child = selectionGrid = new SelectionGrid { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Gameplay/ScreenGameplay.cs similarity index 77% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/Gameplay/ScreenGameplay.cs index af19aa1252..f6f324eb90 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Gameplay/ScreenGameplay.cs @@ -8,11 +8,11 @@ using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay { - public partial class MatchmakingPlayer : MultiplayerPlayer + public partial class ScreenGameplay : MultiplayerPlayer { - public MatchmakingPlayer(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users) + public ScreenGameplay(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users) : base(room, playlistItem, users) { } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs similarity index 87% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs index e3d314844f..faf32c6604 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs @@ -11,8 +11,12 @@ using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { + /// + /// A circular player avatar used in matchmaking displays. + /// Is part of a but can also be used in isolation for a more ambient/decorative user display. + /// public partial class MatchmakingAvatar : CompositeDrawable { public static readonly Vector2 SIZE = new Vector2(30); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingSubScreen.cs similarity index 88% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingSubScreen.cs index fc41b7db84..0141c424bd 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingSubScreen.cs @@ -4,11 +4,11 @@ using osu.Framework.Graphics; using osu.Framework.Screens; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { public abstract partial class MatchmakingSubScreen : Screen { - public abstract PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle { get; } + public abstract PanelDisplayStyle PlayersDisplayStyle { get; } public abstract Drawable? PlayersDisplayArea { get; } protected MatchmakingSubScreen() diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs index 117893bb8c..ce4b471df4 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs @@ -14,9 +14,13 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Users; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { - public partial class PlayerPanel : UserPanel + /// + /// A panel used throughout matchmaking to represent a user, including local information like their + /// rank and high level statistics in the matchmaking system. + /// + public partial class MatchmakingUserPanel : UserPanel { public static readonly Vector2 SIZE_HORIZONTAL = new Vector2(250, 100); public static readonly Vector2 SIZE_VERTICAL = new Vector2(150, 200); @@ -51,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private bool horizontal; - public PlayerPanel(MultiplayerRoomUser user) + public MatchmakingUserPanel(MultiplayerRoomUser user) : base(user.User!) { RoomUser = user; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelRoomAward.cs similarity index 89% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelRoomAward.cs index 5988a73ef8..5e7c3865c1 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelRoomAward.cs @@ -11,16 +11,16 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osuTK.Graphics; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results { - public partial class RoomStatisticPanel : CompositeDrawable + public partial class PanelRoomAward : CompositeDrawable { private readonly Color4 backgroundColour = Color4.SaddleBrown; private readonly string text; private readonly int userId; - public RoomStatisticPanel(string text, int userId) + public PanelRoomAward(string text, int userId) { this.text = text; this.userId = userId; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelUserStatistic.cs similarity index 87% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelUserStatistic.cs index 3a39fc714d..2051359f32 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelUserStatistic.cs @@ -8,15 +8,15 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Sprites; using osuTK.Graphics; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results { - public partial class UserStatisticPanel : CompositeDrawable + public partial class PanelUserStatistic : CompositeDrawable { private readonly Color4 backgroundColour = Color4.SaddleBrown; private readonly string text; - public UserStatisticPanel(string text) + public PanelUserStatistic(string text) { this.text = text; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs similarity index 95% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs index 83c587e7cd..3e6b437f63 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs @@ -14,23 +14,26 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Utils; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results { - public partial class ResultsScreen : MatchmakingSubScreen + /// + /// Final room results, during + /// + public partial class SubScreenResults : MatchmakingSubScreen { private const float grid_spacing = 5; - public override PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle => PlayerPanelList.PanelDisplayStyle.Grid; + public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Grid; public override Drawable PlayersDisplayArea { get; } [Resolved] private MultiplayerClient client { get; set; } = null!; private readonly OsuSpriteText placementText; - private readonly FillFlowContainer userStatistics; - private readonly FillFlowContainer roomStatistics; + private readonly FillFlowContainer userStatistics; + private readonly FillFlowContainer roomStatistics; - public ResultsScreen() + public SubScreenResults() { InternalChild = new GridContainer { @@ -103,7 +106,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results Text = "Breakdown", Font = OsuFont.Default.With(size: 12) }, - userStatistics = new FillFlowContainer + userStatistics = new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -139,7 +142,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results Text = "Statistics", Font = OsuFont.Default.With(size: 12) }, - roomStatistics = new FillFlowContainer + roomStatistics = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -196,7 +199,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results void addStatistic(string text) { - userStatistics.Add(new UserStatisticPanel(text) + userStatistics.Add(new PanelUserStatistic(text) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre @@ -321,7 +324,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results void addStatistic(int userId, string text) { - roomStatistics.Add(new RoomStatisticPanel(text, userId) + roomStatistics.Add(new PanelRoomAward(text, userId) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs similarity index 82% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs index 8fd56877eb..580d157a8b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs @@ -18,18 +18,22 @@ using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults { - public partial class RoundResultsScreen : MatchmakingSubScreen + /// + /// Per-round results, during + /// + public partial class SubScreenRoundResults : MatchmakingSubScreen { private const int panel_spacing = 5; - public override PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle => PlayerPanelList.PanelDisplayStyle.Hidden; + public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Hidden; public override Drawable? PlayersDisplayArea => null; [Resolved] @@ -153,6 +157,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults } }); + private partial class RoundResultsScorePanel : CompositeDrawable + { + public RoundResultsScorePanel(ScoreInfo score) + { + AutoSizeAxes = Axes.Both; + InternalChild = new InstantSizingScorePanel(score); + } + + public override bool PropagateNonPositionalInputSubTree => false; + public override bool PropagatePositionalInputSubTree => false; + + private partial class InstantSizingScorePanel : ScorePanel + { + public InstantSizingScorePanel(ScoreInfo score, bool isNewLocalScore = false) + : base(score, isNewLocalScore) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + FinishTransforms(true); + } + } + } + private partial class AutoScrollContainer : UserTrackingScrollContainer { private const float initial_offset = -0.5f; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundWarmup/SubScreenRoundWarmup.cs similarity index 52% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundWarmup/SubScreenRoundWarmup.cs index 6f982d89f2..e389cbabfa 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundWarmup/SubScreenRoundWarmup.cs @@ -3,12 +3,16 @@ using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundWarmup { - public partial class IdleScreen : MatchmakingSubScreen + /// + /// Shown during + /// + public partial class SubScreenRoundWarmup : MatchmakingSubScreen { - public override PlayerPanelList.PanelDisplayStyle PlayersDisplayStyle => PlayerPanelList.PanelDisplayStyle.Grid; + public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Grid; public override Drawable PlayersDisplayArea => this; public override void OnEntering(ScreenTransitionEvent e) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs new file mode 100644 index 0000000000..29a1acb2b8 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundWarmup; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class ScreenMatchmaking + { + public partial class ScreenStack : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Framework.Screens.ScreenStack screenStack = null!; + private UserPanelOverlay playersList = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10) + { + Bottom = StageDisplay.HEIGHT, + }, + Children = new Drawable[] + { + screenStack = new Framework.Screens.ScreenStack(), + } + }, + playersList = new UserPanelOverlay + { + DisplayArea = this + }, + new StageDisplay + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + screenStack.ScreenPushed += onScreenPushed; + screenStack.ScreenExited += onScreenExited; + + screenStack.Push(new SubScreenRoundWarmup()); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + onMatchRoomStateChanged(client.Room!.MatchState); + } + + private void onScreenPushed(IScreen lastScreen, IScreen newScreen) + { + if (newScreen is not MatchmakingSubScreen matchmakingSubScreen) + return; + + playersList.DisplayStyle = matchmakingSubScreen.PlayersDisplayStyle; + playersList.DisplayArea = matchmakingSubScreen.PlayersDisplayArea; + } + + private void onScreenExited(IScreen lastScreen, IScreen newScreen) + { + if (newScreen is not MatchmakingSubScreen matchmakingSubScreen) + return; + + playersList.DisplayStyle = matchmakingSubScreen.PlayersDisplayStyle; + playersList.DisplayArea = matchmakingSubScreen.PlayersDisplayArea; + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + switch (matchmakingState.Stage) + { + case MatchmakingStage.WaitingForClientsJoin: + case MatchmakingStage.RoundWarmupTime: + while (screenStack.CurrentScreen is not SubScreenRoundWarmup) + screenStack.Exit(); + break; + + case MatchmakingStage.UserBeatmapSelect: + screenStack.Push(new SubScreenBeatmapSelect()); + break; + + case MatchmakingStage.ServerBeatmapFinalised: + Debug.Assert(screenStack.CurrentScreen is SubScreenBeatmapSelect); + ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem); + break; + + case MatchmakingStage.ResultsDisplaying: + screenStack.Push(new SubScreenRoundResults()); + break; + + case MatchmakingStage.Ended: + screenStack.Push(new SubScreenResults()); + break; + } + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs similarity index 95% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 7dde7a480b..dbe958dcac 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -25,13 +25,16 @@ using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Match.Components; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Users; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { - public partial class MatchmakingScreen : OsuScreen + /// + /// The main matchmaking screen which houses a custom through the life cycle of a single session. + /// + public partial class ScreenMatchmaking : OsuScreen { /// /// Padding between rows of the content. @@ -76,7 +79,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private MatchChatDisplay chat = null!; - public MatchmakingScreen(MultiplayerRoom room) + public ScreenMatchmaking(MultiplayerRoom room) { this.room = room; @@ -128,7 +131,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background6, }, - new MatchmakingScreenStack(), + new ScreenStack(), } } ], @@ -248,7 +251,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking sampleStart?.Play(); - this.Push(new MultiplayerPlayerLoader(() => new MatchmakingPlayer(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray()))); + this.Push(new MultiplayerPlayerLoader(() => new ScreenGameplay(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray()))); }); private void checkForAutomaticDownload() diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs new file mode 100644 index 0000000000..f97bf9fe68 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs @@ -0,0 +1,234 @@ +// 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; +using osu.Framework.Extensions.ObjectExtensions; +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.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Matchmaking; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class StageDisplay + { + internal partial class StageSegment : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + public readonly int? Round; + + private readonly MatchmakingStage stage; + + private readonly LocalisableString displayText; + private Drawable progressBar = null!; + + private DateTimeOffset countdownStartTime; + private DateTimeOffset countdownEndTime; + private SpriteIcon arrow = null!; + + private Sample? countdownTickSample; + private double? lastSamplePlayback; + + public bool Active { get; private set; } + + public float Progress => progressBar.Width; + + public StageSegment(int? round, MatchmakingStage stage, LocalisableString displayText) + { + Round = round; + this.stage = stage; + this.displayText = displayText; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OverlayColourProvider colourProvider) + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + arrow = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Alpha = 0.5f, + Size = new Vector2(16), + Icon = FontAwesome.Solid.ArrowRight, + Margin = new MarginPadding { Horizontal = 10 } + }, + new Container + { + Masking = true, + CornerRadius = 5, + CornerExponent = 10, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = + ColourInfo.GradientVertical( + colourProvider.Dark2, + colourProvider.Dark1 + ), + }, + progressBar = new Box + { + Blending = BlendingParameters.Additive, + EdgeSmoothness = new Vector2(1), + RelativeSizeAxes = Axes.Both, + Width = 0, + Colour = colourProvider.Dark3, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = displayText, + Padding = new MarginPadding(10) + } + } + } + } + }; + + Alpha = 0.5f; + countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + client.CountdownStarted += onCountdownStarted; + client.CountdownStopped += onCountdownStopped; + + if (client.Room != null) + { + onMatchRoomStateChanged(client.Room.MatchState); + foreach (var countdown in client.Room.ActiveCountdowns) + onCountdownStarted(countdown); + } + } + + protected override void Update() + { + base.Update(); + + if (!Active) + return; + + TimeSpan total = countdownEndTime - countdownStartTime; + TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; + + if (total.TotalMilliseconds <= 0) + { + progressBar.Width = 0; + return; + } + + progressBar.Width = (float)Math.Clamp(elapsed.TotalMilliseconds / total.TotalMilliseconds, 0, 1); + + int secondsRemaining = Math.Max(0, (int)Math.Ceiling((total.TotalMilliseconds - elapsed.TotalMilliseconds) / 1000)); + + if (total.TotalMilliseconds - elapsed.TotalMilliseconds <= 3000 + && lastSamplePlayback != secondsRemaining) + { + countdownTickSample?.Play(); + lastSamplePlayback = secondsRemaining; + } + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + bool wasActive = Active; + + Active = false; + + if (state is not MatchmakingRoomState roomState) + return; + + if (Round != null && roomState.CurrentRound != Round) + return; + + Active = stage == roomState.Stage; + + if (wasActive) + progressBar.Width = 1; + + bool isPreparing = + (stage == MatchmakingStage.RoundWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsJoin) || + (stage == MatchmakingStage.GameplayWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsBeatmapDownload) || + (stage == MatchmakingStage.ResultsDisplaying && roomState.Stage == MatchmakingStage.Gameplay); + + if (isPreparing) + { + arrow.FadeTo(1, 500) + .Then() + .FadeTo(0.5f, 500) + .Loop(); + } + }); + + private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (!Active) + return; + + if (countdown is not MatchmakingStageCountdown) + return; + + countdownStartTime = DateTimeOffset.Now; + countdownEndTime = countdownStartTime + countdown.TimeRemaining; + arrow.FadeIn(500, Easing.OutQuint); + + this.FadeIn(200); + }); + + private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (!Active) + return; + + if (countdown is not MatchmakingStageCountdown) + return; + + countdownEndTime = DateTimeOffset.Now; + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + client.CountdownStarted -= onCountdownStarted; + client.CountdownStopped -= onCountdownStopped; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StatusText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StatusText.cs new file mode 100644 index 0000000000..8d94df11e4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StatusText.cs @@ -0,0 +1,129 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class StageDisplay + { + public partial class StatusText : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private OsuSpriteText text = null!; + + private Sample? textChangedSample; + private double? lastSamplePlayback; + + public StatusText() + { + AutoSizeAxes = Axes.X; + Height = 16; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + InternalChild = text = new OsuSpriteText + { + Alpha = 0, + Height = 16, + Font = OsuFont.Style.Caption1, + AlwaysPresent = true, + }; + + textChangedSample = audio.Samples.Get(@"Multiplayer/Matchmaking/stage-message"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + onMatchRoomStateChanged(client.Room!.MatchState); + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + text.Text = getTextForStatus(matchmakingState.Stage); + + if (text.Text == string.Empty || (lastSamplePlayback != null && Time.Current - lastSamplePlayback < OsuGameBase.SAMPLE_DEBOUNCE_TIME)) + return; + + textChangedSample?.Play(); + lastSamplePlayback = Time.Current; + + LocalisableString textForStatus = getTextForStatus(matchmakingState.Stage); + + if (string.IsNullOrEmpty(textForStatus.ToString())) + { + text.FadeOut(); + return; + } + + text.RotateTo(2f) + .RotateTo(0, 500, Easing.OutQuint); + + text.FadeInFromZero(500, Easing.OutQuint); + + using (text.BeginDelayedSequence(500)) + { + text + .FadeTo(0.6f, 400, Easing.In) + .Then() + .FadeTo(1, 400, Easing.Out) + .Loop(); + } + + text.ScaleTo(0.3f) + .ScaleTo(1, 500, Easing.OutQuint); + + text.Text = textForStatus; + }); + + private LocalisableString getTextForStatus(MatchmakingStage status) + { + switch (status) + { + case MatchmakingStage.WaitingForClientsJoin: + return "Players are joining the match..."; + + case MatchmakingStage.WaitingForClientsBeatmapDownload: + return "Players are downloading the beatmap..."; + + case MatchmakingStage.Gameplay: + return "Game is in progress..."; + + case MatchmakingStage.Ended: + return "Thanks for playing! The match will close shortly."; + + default: + return string.Empty; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs similarity index 90% rename from osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs index a5bb72c4b6..db302163a5 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs @@ -16,8 +16,11 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Overlays; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { + /// + /// A "global" footer staple element in matchmaking which shows the current progression of the room, from start to finish. + /// public partial class StageDisplay : CompositeDrawable { public const float HEIGHT = 96; @@ -68,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Direction = FillDirection.Horizontal, }, }, - new StageText + new StatusText { Y = 32, Anchor = Anchor.Centre, @@ -93,23 +96,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking }, }; - flow.Add(new StageBubble(null, MatchmakingStage.WaitingForClientsJoin, "Waiting for other users")); + flow.Add(new StageSegment(null, MatchmakingStage.WaitingForClientsJoin, "Waiting for other users")); for (int i = 1; i <= round_count; i++) { - flow.Add(new StageBubble(i, MatchmakingStage.RoundWarmupTime, "Next Round")); - flow.Add(new StageBubble(i, MatchmakingStage.UserBeatmapSelect, "Beatmap Selection")); - flow.Add(new StageBubble(i, MatchmakingStage.GameplayWarmupTime, "Get Ready")); - flow.Add(new StageBubble(i, MatchmakingStage.ResultsDisplaying, "Results")); + flow.Add(new StageSegment(i, MatchmakingStage.RoundWarmupTime, "Next Round")); + flow.Add(new StageSegment(i, MatchmakingStage.UserBeatmapSelect, "Beatmap Selection")); + flow.Add(new StageSegment(i, MatchmakingStage.GameplayWarmupTime, "Get Ready")); + flow.Add(new StageSegment(i, MatchmakingStage.ResultsDisplaying, "Results")); } - flow.Add(new StageBubble(null, MatchmakingStage.Ended, "Match End")); + flow.Add(new StageSegment(null, MatchmakingStage.Ended, "Match End")); } protected override void Update() { base.Update(); - var bubble = flow.OfType().FirstOrDefault(b => b.Active); + var bubble = flow.OfType().FirstOrDefault(b => b.Active); if (bubble != null) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs index a226ce19be..9ddddda710 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/PlayerPanelList.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs @@ -12,14 +12,18 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { - public partial class PlayerPanelList : CompositeDrawable + /// + /// A component which maintains the layout of the players in a matchmaking room. + /// Can be controlled to display the panels in a certain location and in multiple styles. + /// + public partial class UserPanelOverlay : CompositeDrawable { [Resolved] private MultiplayerClient client { get; set; } = null!; - private Container panels = null!; + private Container panels = null!; private PlayerPanelCellContainer gridLayout = null!; private PlayerPanelCellContainer splitLayoutLeft = null!; private PlayerPanelCellContainer splitLayoutRight = null!; @@ -56,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Direction = FillDirection.Vertical, Spacing = new Vector2(20, 5), }, - panels = new Container + panels = new Container { RelativeSizeAxes = Axes.Both } @@ -106,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => { - panels.Add(new PlayerPanel(user) + panels.Add(new MatchmakingUserPanel(user) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -211,7 +215,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking [Resolved] private MultiplayerClient client { get; set; } = null!; - public void AcquirePanels(PlayerPanel[] panels) + public void AcquirePanels(MatchmakingUserPanel[] panels) { while (Count < panels.Length) { @@ -255,10 +259,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private partial class PlayerPanelCell : Drawable { - private PlayerPanel? panel; + private MatchmakingUserPanel? panel; private bool isAnimating; - public void AcquirePanel(PlayerPanel panel) + public void AcquirePanel(MatchmakingUserPanel panel) { this.panel = panel; isAnimating = true; @@ -276,7 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking if (panel == null) return; - Size = panel.Horizontal ? PlayerPanel.SIZE_HORIZONTAL : PlayerPanel.SIZE_VERTICAL; + Size = panel.Horizontal ? MatchmakingUserPanel.SIZE_HORIZONTAL : MatchmakingUserPanel.SIZE_VERTICAL; Size *= panel.Scale; var targetPos = getFinalPosition(); @@ -298,12 +302,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking => panel.Parent!.ToLocalSpace(ScreenSpaceDrawQuad.Centre) - panel.AnchorPosition; } } + } - public enum PanelDisplayStyle - { - Grid, - Split, - Hidden - } + public enum PanelDisplayStyle + { + Grid, + Split, + Hidden } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/CloudVisualisation.cs similarity index 92% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Queue/CloudVisualisation.cs index 3fab5ab207..33ed21f3db 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/CloudVisualisation.cs @@ -11,12 +11,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Screens.Ranking; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { - public partial class MatchmakingCloud : CompositeDrawable + /// + /// A visualisation at the top level of matchmaking which shows the overall system status. + /// This is intended to be something which users can watch while idle, for fun or otherwise. + /// + public partial class CloudVisualisation : CompositeDrawable { private APIUser[] users = []; private Container usersContainer = null!; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs similarity index 97% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs index 8976b1f3b0..eb84a525a8 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs @@ -16,9 +16,9 @@ using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { - public partial class MatchmakingPoolSelector : CompositeDrawable + public partial class PoolSelector : CompositeDrawable { private const float icon_size = 48; @@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private FillFlowContainer poolFlow = null!; - public MatchmakingPoolSelector() + public PoolSelector() { AutoSizeAxes = Axes.Both; } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs similarity index 79% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 1a426501d7..40ac0e5777 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -10,13 +10,21 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Intro; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { - public partial class MatchmakingController : Component + /// + /// A component which acts as a bridge between the online component (ie ) + /// and the visual representations and flow of queueing for matchmaking. + /// + /// Includes support for deferring to background. + /// + /// + /// This is initialised and cached in the but can be used throughout the system via DI. + public partial class QueueController : Component { - public readonly Bindable CurrentState = new Bindable(); + public readonly Bindable CurrentState = new Bindable(); [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -63,12 +71,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void onRoomUpdated() => Scheduler.Add(() => { if (client.Room == null) - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Idle; + CurrentState.Value = ScreenQueue.MatchmakingScreenState.Idle; }); private void onMatchmakingQueueJoined() => Scheduler.Add(() => { - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Queueing; + CurrentState.Value = ScreenQueue.MatchmakingScreenState.Queueing; if (isBackgrounded) { @@ -79,15 +87,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void onMatchmakingQueueLeft() => Scheduler.Add(() => { - if (CurrentState.Value != MatchmakingQueueScreen.MatchmakingScreenState.InRoom) - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Idle; + if (CurrentState.Value != ScreenQueue.MatchmakingScreenState.InRoom) + CurrentState.Value = ScreenQueue.MatchmakingScreenState.Idle; closeNotifications(); }); private void onMatchmakingRoomInvited() => Scheduler.Add(() => { - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.PendingAccept; + CurrentState.Value = ScreenQueue.MatchmakingScreenState.PendingAccept; if (backgroundNotification != null) { @@ -101,7 +109,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking client.JoinRoom(new Room { RoomID = roomId }, password) .FireAndForget(() => Scheduler.Add(() => { - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.InRoom; + CurrentState.Value = ScreenQueue.MatchmakingScreenState.InRoom; })); }); @@ -118,7 +126,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking CompletionClickAction = () => { client.MatchmakingAcceptInvitation().FireAndForget(); - performer?.PerformFromScreen(s => s.Push(new MatchmakingIntroScreen())); + performer?.PerformFromScreen(s => s.Push(new IntroScreen())); closeNotifications(); return true; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs similarity index 96% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs index d23ff9bf84..d13bc9c2da 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs @@ -27,18 +27,22 @@ using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { - public partial class MatchmakingQueueScreen : OsuScreen + /// + /// The initial screen that users arrive at when preparing for a quick play session. + /// + public partial class ScreenQueue : OsuScreen { public override bool ShowFooter => true; private Container mainContent = null!; private MatchmakingScreenState state; - private MatchmakingCloud cloud = null!; + private CloudVisualisation cloud = null!; [Resolved] private IAPIProvider api { get; set; } = null!; @@ -56,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private IDialogOverlay dialogOverlay { get; set; } = null!; [Resolved] - private MatchmakingController controller { get; set; } = null!; + private QueueController controller { get; set; } = null!; [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -79,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens InternalChildren = new Drawable[] { - cloud = new MatchmakingCloud + cloud = new CloudVisualisation { Y = -100, Anchor = Anchor.Centre, @@ -252,7 +256,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens Spacing = new Vector2(10), Children = new Drawable[] { - new MatchmakingPoolSelector + new PoolSelector { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -411,7 +415,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens }; using (BeginDelayedSequence(2000)) - Schedule(() => this.Push(new MatchmakingScreen(client.Room!))); + Schedule(() => this.Push(new ScreenMatchmaking(client.Room!))); break; default: diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs deleted file mode 100644 index 2e13c59055..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Screens; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens -{ - public partial class MatchmakingScreenStack : CompositeDrawable - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private ScreenStack screenStack = null!; - private PlayerPanelList playersList = null!; - - [BackgroundDependencyLoader] - private void load() - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10) - { - Bottom = StageDisplay.HEIGHT, - }, - Children = new Drawable[] - { - screenStack = new ScreenStack(), - } - }, - playersList = new PlayerPanelList - { - DisplayArea = this - }, - new StageDisplay - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - screenStack.ScreenPushed += onScreenPushed; - screenStack.ScreenExited += onScreenExited; - - screenStack.Push(new IdleScreen()); - - client.MatchRoomStateChanged += onMatchRoomStateChanged; - onMatchRoomStateChanged(client.Room!.MatchState); - } - - private void onScreenPushed(IScreen lastScreen, IScreen newScreen) - { - if (newScreen is not MatchmakingSubScreen matchmakingSubScreen) - return; - - playersList.DisplayStyle = matchmakingSubScreen.PlayersDisplayStyle; - playersList.DisplayArea = matchmakingSubScreen.PlayersDisplayArea; - } - - private void onScreenExited(IScreen lastScreen, IScreen newScreen) - { - if (newScreen is not MatchmakingSubScreen matchmakingSubScreen) - return; - - playersList.DisplayStyle = matchmakingSubScreen.PlayersDisplayStyle; - playersList.DisplayArea = matchmakingSubScreen.PlayersDisplayArea; - } - - private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - if (state is not MatchmakingRoomState matchmakingState) - return; - - switch (matchmakingState.Stage) - { - case MatchmakingStage.WaitingForClientsJoin: - case MatchmakingStage.RoundWarmupTime: - while (screenStack.CurrentScreen is not IdleScreen) - screenStack.Exit(); - break; - - case MatchmakingStage.UserBeatmapSelect: - screenStack.Push(new PickScreen()); - break; - - case MatchmakingStage.ServerBeatmapFinalised: - Debug.Assert(screenStack.CurrentScreen is PickScreen); - ((PickScreen)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem); - break; - - case MatchmakingStage.ResultsDisplaying: - screenStack.Push(new RoundResultsScreen()); - break; - - case MatchmakingStage.Ended: - screenStack.Push(new ResultsScreen()); - break; - } - }); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - client.MatchRoomStateChanged -= onMatchRoomStateChanged; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs deleted file mode 100644 index 807c7d3355..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs +++ /dev/null @@ -1,176 +0,0 @@ -// 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.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick -{ - public partial class BeatmapPanel : CompositeDrawable - { - public static readonly Vector2 SIZE = new Vector2(300, 70); - - public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both }; - - public APIBeatmap? Beatmap - { - get => beatmap; - set - { - if (beatmap?.OnlineID == value?.OnlineID) - return; - - beatmap = value; - - if (IsLoaded) - updateContent(); - } - } - - private APIBeatmap? beatmap; - - private Container content = null!; - private UpdateableOnlineBeatmapSetCover cover = null!; - - public BeatmapPanel(APIBeatmap? beatmap = null) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Masking = true; - CornerRadius = 6; - - InternalChildren = new Drawable[] - { - cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0, timeBeforeUnload: 10000) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal( - colourProvider.Background4.Opacity(0.7f), - colourProvider.Background4.Opacity(0.4f) - ) - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - OverlayLayer, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateContent(); - FinishTransforms(true); - } - - private void updateContent() - { - foreach (var child in content.Children) - child.FadeOut(300).Expire(); - - cover.OnlineInfo = beatmap?.BeatmapSet; - - if (beatmap != null) - { - var panelContent = new BeatmapPanelContent(beatmap) - { - RelativeSizeAxes = Axes.Both, - }; - - content.Add(panelContent); - - panelContent.FadeInFromZero(300); - } - } - - private partial class BeatmapPanelContent : CompositeDrawable - { - private readonly APIBeatmap beatmap; - - public BeatmapPanelContent(APIBeatmap beatmap) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding { Horizontal = 12 }, - Children = new Drawable[] - { - new TruncatingSpriteText - { - Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode), - Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - }, - new TextFlowContainer(s => - { - s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); - }).With(d => - { - d.RelativeSizeAxes = Axes.X; - d.AutoSizeAxes = Axes.Y; - d.AddText("by "); - d.AddText(new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); - }), - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 6 }, - Spacing = new Vector2(4), - Children = new Drawable[] - { - new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - new TruncatingSpriteText - { - Text = beatmap.DifficultyName, - Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - }, - }; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs deleted file mode 100644 index 2a15201d11..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs +++ /dev/null @@ -1,157 +0,0 @@ -// 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.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick -{ - public partial class BeatmapSelectionOverlay : CompositeDrawable - { - private readonly Dictionary avatars = new Dictionary(); - - private readonly Container avatarContainer; - - private Sample? userAddedSample; - private double? lastSamplePlayback; - - public new Axes AutoSizeAxes - { - get => base.AutoSizeAxes; - set => base.AutoSizeAxes = value; - } - - public new MarginPadding Padding - { - get => base.Padding; - set => base.Padding = value; - } - - public BeatmapSelectionOverlay() - { - InternalChild = avatarContainer = new Container(); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - avatarContainer.AutoSizeAxes = AutoSizeAxes; - avatarContainer.RelativeSizeAxes = RelativeSizeAxes; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); - } - - public bool AddUser(APIUser user, bool isOwnUser) - { - if (avatars.ContainsKey(user.Id)) - return false; - - var avatar = new SelectionAvatar(user, isOwnUser) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }; - - avatarContainer.Add(avatars[user.Id] = avatar); - - if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) - { - userAddedSample?.Play(); - lastSamplePlayback = Time.Current; - } - - updateLayout(); - - avatar.FinishTransforms(); - - return true; - } - - public bool RemoveUser(int id) - { - if (!avatars.Remove(id, out var avatar)) - return false; - - avatar.PopOutAndExpire(); - avatarContainer.ChangeChildDepth(avatar, float.MaxValue); - - updateLayout(); - - return true; - } - - private void updateLayout() - { - const double stagger = 30; - const float spacing = 4; - - double delay = 0; - float x = 0; - - for (int i = avatarContainer.Count - 1; i >= 0; i--) - { - var avatar = avatarContainer[i]; - - if (avatar.Expired) - continue; - - avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); - - x -= avatar.LayoutSize.X + spacing; - - delay += stagger; - } - } - - public partial class SelectionAvatar : CompositeDrawable - { - public bool Expired { get; private set; } - - private readonly Container content; - - public SelectionAvatar(APIUser user, bool isOwnUser) - { - Size = new Vector2(30); - - InternalChildren = new Drawable[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Child = new MatchmakingAvatar(user, isOwnUser) - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - content.ScaleTo(0) - .ScaleTo(1, 500, Easing.OutElasticHalf) - .FadeIn(200); - } - - public void PopOutAndExpire() - { - content.ScaleTo(0, 400, Easing.OutExpo); - - this.FadeOut(100).Expire(); - Expired = true; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs deleted file mode 100644 index 090c275bef..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs +++ /dev/null @@ -1,190 +0,0 @@ -// 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.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Database; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osuTK.Input; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick -{ - public partial class BeatmapSelectionPanel : Container - { - private const float corner_radius = 6; - private const float border_width = 3; - - public readonly MultiplayerPlaylistItem Item; - - private readonly Container scaleContainer; - private readonly BeatmapPanel beatmapPanel; - private readonly BeatmapSelectionOverlay selectionOverlay; - private readonly Container border; - private readonly Box flash; - - public bool AllowSelection; - - public Action? Action; - - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - - public override bool PropagatePositionalInputSubTree => AllowSelection; - - public BeatmapSelectionPanel(MultiplayerPlaylistItem item) - { - Item = item; - Size = BeatmapPanel.SIZE; - - InternalChildren = new Drawable[] - { - scaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(-border_width), - Child = border = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius + border_width, - Alpha = 0, - Child = new Box { RelativeSizeAxes = Axes.Both }, - } - }, - beatmapPanel = new BeatmapPanel - { - RelativeSizeAxes = Axes.Both, - OverlayLayer = - { - Children = new[] - { - flash = new Box - { - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - } - } - }, - selectionOverlay = new BeatmapSelectionOverlay - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10 }, - Origin = Anchor.CentreLeft, - }, - } - }, - new HoverClickSounds(), - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => - { - var beatmap = b.GetResultSafely()!; - - beatmap.StarRating = Item.StarRating; - - beatmapPanel.Beatmap = beatmap; - })); - } - - public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser); - - public bool RemoveUser(int userId) => selectionOverlay.RemoveUser(userId); - - public bool RemoveUser(APIUser user) => RemoveUser(user.Id); - - protected override bool OnHover(HoverEvent e) - { - flash.FadeTo(0.2f, 50) - .Then() - .FadeTo(0.1f, 300); - - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - base.OnHoverLost(e); - - flash.FadeOut(200); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (e.Button == MouseButton.Left) - { - scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); - return true; - } - - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - base.OnMouseUp(e); - - if (e.Button == MouseButton.Left) - { - scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); - } - } - - protected override bool OnClick(ClickEvent e) - { - Action?.Invoke(Item); - - flash.FadeTo(0.5f, 50) - .Then() - .FadeTo(0.1f, 400); - - return true; - } - - public void ShowBorder() => border.Show(); - - public void HideBorder() => border.Hide(); - - public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) - { - scaleContainer - .FadeOut() - .MoveToY(distance) - .Delay(delay) - .FadeIn(duration / 2) - .MoveToY(0, duration, Easing.OutExpo); - } - - public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic) - { - AllowSelection = false; - - scaleContainer.Delay(delay) - .ScaleTo(0, duration, easing) - .FadeOut(duration); - - this.Delay(delay + duration).FadeOut().Expire(); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs deleted file mode 100644 index ad30c19c02..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults -{ - internal partial class RoundResultsScorePanel : CompositeDrawable - { - public RoundResultsScorePanel(ScoreInfo score) - { - AutoSizeAxes = Axes.Both; - InternalChild = new InstantSizingScorePanel(score); - } - - public override bool PropagateNonPositionalInputSubTree => false; - public override bool PropagatePositionalInputSubTree => false; - - private partial class InstantSizingScorePanel : ScorePanel - { - public InstantSizingScorePanel(ScoreInfo score, bool isNewLocalScore = false) - : base(score, isNewLocalScore) - { - } - - protected override void LoadComplete() - { - base.LoadComplete(); - FinishTransforms(true); - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs deleted file mode 100644 index da6de711ff..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs +++ /dev/null @@ -1,231 +0,0 @@ -// 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; -using osu.Framework.Extensions.ObjectExtensions; -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.Framework.Localisation; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Matchmaking; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Overlays; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking -{ - internal partial class StageBubble : CompositeDrawable - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - public readonly int? Round; - - private readonly MatchmakingStage stage; - - private readonly LocalisableString displayText; - private Drawable progressBar = null!; - - private DateTimeOffset countdownStartTime; - private DateTimeOffset countdownEndTime; - private SpriteIcon arrow = null!; - - private Sample? countdownTickSample; - private double? lastSamplePlayback; - - public bool Active { get; private set; } - - public float Progress => progressBar.Width; - - public StageBubble(int? round, MatchmakingStage stage, LocalisableString displayText) - { - Round = round; - this.stage = stage; - this.displayText = displayText; - - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio, OverlayColourProvider colourProvider) - { - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new Drawable[] - { - arrow = new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Alpha = 0.5f, - Size = new Vector2(16), - Icon = FontAwesome.Solid.ArrowRight, - Margin = new MarginPadding { Horizontal = 10 } - }, - new Container - { - Masking = true, - CornerRadius = 5, - CornerExponent = 10, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = - ColourInfo.GradientVertical( - colourProvider.Dark2, - colourProvider.Dark1 - ), - }, - progressBar = new Box - { - Blending = BlendingParameters.Additive, - EdgeSmoothness = new Vector2(1), - RelativeSizeAxes = Axes.Both, - Width = 0, - Colour = colourProvider.Dark3, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = displayText, - Padding = new MarginPadding(10) - } - } - } - } - }; - - Alpha = 0.5f; - countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - client.MatchRoomStateChanged += onMatchRoomStateChanged; - client.CountdownStarted += onCountdownStarted; - client.CountdownStopped += onCountdownStopped; - - if (client.Room != null) - { - onMatchRoomStateChanged(client.Room.MatchState); - foreach (var countdown in client.Room.ActiveCountdowns) - onCountdownStarted(countdown); - } - } - - protected override void Update() - { - base.Update(); - - if (!Active) - return; - - TimeSpan total = countdownEndTime - countdownStartTime; - TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; - - if (total.TotalMilliseconds <= 0) - { - progressBar.Width = 0; - return; - } - - progressBar.Width = (float)Math.Clamp(elapsed.TotalMilliseconds / total.TotalMilliseconds, 0, 1); - - int secondsRemaining = Math.Max(0, (int)Math.Ceiling((total.TotalMilliseconds - elapsed.TotalMilliseconds) / 1000)); - - if (total.TotalMilliseconds - elapsed.TotalMilliseconds <= 3000 - && lastSamplePlayback != secondsRemaining) - { - countdownTickSample?.Play(); - lastSamplePlayback = secondsRemaining; - } - } - - private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - bool wasActive = Active; - - Active = false; - - if (state is not MatchmakingRoomState roomState) - return; - - if (Round != null && roomState.CurrentRound != Round) - return; - - Active = stage == roomState.Stage; - - if (wasActive) - progressBar.Width = 1; - - bool isPreparing = - (stage == MatchmakingStage.RoundWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsJoin) || - (stage == MatchmakingStage.GameplayWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsBeatmapDownload) || - (stage == MatchmakingStage.ResultsDisplaying && roomState.Stage == MatchmakingStage.Gameplay); - - if (isPreparing) - { - arrow.FadeTo(1, 500) - .Then() - .FadeTo(0.5f, 500) - .Loop(); - } - }); - - private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() => - { - if (!Active) - return; - - if (countdown is not MatchmakingStageCountdown) - return; - - countdownStartTime = DateTimeOffset.Now; - countdownEndTime = countdownStartTime + countdown.TimeRemaining; - arrow.FadeIn(500, Easing.OutQuint); - - this.FadeIn(200); - }); - - private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() => - { - if (!Active) - return; - - if (countdown is not MatchmakingStageCountdown) - return; - - countdownEndTime = DateTimeOffset.Now; - }); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - { - client.MatchRoomStateChanged -= onMatchRoomStateChanged; - client.CountdownStarted -= onCountdownStarted; - client.CountdownStopped -= onCountdownStopped; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs deleted file mode 100644 index 677906ee9b..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs +++ /dev/null @@ -1,126 +0,0 @@ -// 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.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking -{ - public partial class StageText : CompositeDrawable - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private OsuSpriteText text = null!; - - private Sample? textChangedSample; - private double? lastSamplePlayback; - - public StageText() - { - AutoSizeAxes = Axes.X; - Height = 16; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - InternalChild = text = new OsuSpriteText - { - Alpha = 0, - Height = 16, - Font = OsuFont.Style.Caption1, - AlwaysPresent = true, - }; - - textChangedSample = audio.Samples.Get(@"Multiplayer/Matchmaking/stage-message"); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - client.MatchRoomStateChanged += onMatchRoomStateChanged; - onMatchRoomStateChanged(client.Room!.MatchState); - } - - private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - if (state is not MatchmakingRoomState matchmakingState) - return; - - text.Text = getTextForStatus(matchmakingState.Stage); - - if (text.Text == string.Empty || (lastSamplePlayback != null && Time.Current - lastSamplePlayback < OsuGameBase.SAMPLE_DEBOUNCE_TIME)) - return; - - textChangedSample?.Play(); - lastSamplePlayback = Time.Current; - - LocalisableString textForStatus = getTextForStatus(matchmakingState.Stage); - - if (string.IsNullOrEmpty(textForStatus.ToString())) - { - text.FadeOut(); - return; - } - - text.RotateTo(2f) - .RotateTo(0, 500, Easing.OutQuint); - - text.FadeInFromZero(500, Easing.OutQuint); - - using (text.BeginDelayedSequence(500)) - { - text - .FadeTo(0.6f, 400, Easing.In) - .Then() - .FadeTo(1, 400, Easing.Out) - .Loop(); - } - - text.ScaleTo(0.3f) - .ScaleTo(1, 500, Easing.OutQuint); - - text.Text = textForStatus; - }); - - private LocalisableString getTextForStatus(MatchmakingStage status) - { - switch (status) - { - case MatchmakingStage.WaitingForClientsJoin: - return "Players are joining the match..."; - - case MatchmakingStage.WaitingForClientsBeatmapDownload: - return "Players are downloading the beatmap..."; - - case MatchmakingStage.Gameplay: - return "Game is in progress..."; - - case MatchmakingStage.Ended: - return "Thanks for playing! The match will close shortly."; - - default: - return string.Empty; - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - client.MatchRoomStateChanged -= onMatchRoomStateChanged; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.Cell.cs similarity index 100% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.Cell.cs diff --git a/osu.Game/Screens/Ranking/UserTagControl_AddTagsPopover.cs b/osu.Game/Screens/Ranking/UserTagControl.AddTagsPopover.cs similarity index 100% rename from osu.Game/Screens/Ranking/UserTagControl_AddTagsPopover.cs rename to osu.Game/Screens/Ranking/UserTagControl.AddTagsPopover.cs diff --git a/osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs b/osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs similarity index 100% rename from osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs rename to osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea.Header.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs rename to osu.Game/Screens/SelectV2/BeatmapDetailsArea.Header.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_WedgeSelector.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea.WedgeSelector.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapDetailsArea_WedgeSelector.cs rename to osu.Game/Screens/SelectV2/BeatmapDetailsArea.WedgeSelector.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.Tooltip.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs rename to osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.Tooltip.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.FailRetryDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.FailRetryDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.MetadataDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.MetadataDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.RatingSpreadDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.RatingSpreadDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.SuccessRateDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.SuccessRateDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.TagsLine.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.TagsLine.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.UserRatingDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.UserRatingDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyStatisticsDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyStatisticsDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_FavouriteButton.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_FavouriteButton.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.Statistic.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.Statistic.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.StatisticDifficulty.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.StatisticDifficulty.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.StatisticPlayCount.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.StatisticPlayCount.cs diff --git a/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs b/osu.Game/Screens/SelectV2/FilterControl.DifficultyRangeSlider.cs similarity index 100% rename from osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs rename to osu.Game/Screens/SelectV2/FilterControl.DifficultyRangeSlider.cs diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs similarity index 100% rename from osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs rename to osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs From 702511c918cf3eac88b5577d17244f8569ca6e1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 14:22:55 +0900 Subject: [PATCH 216/267] More matchmaking renames and spacing adjusts --- ...nGrid.cs => TestSceneBeatmapSelectGrid.cs} | 27 +++++++++++++++---- ...anel.cs => TestSceneBeatmapSelectPanel.cs} | 6 ++--- .../Matchmaking/TestScenePlayerPanel.cs | 4 +-- .../Matchmaking/TestSceneUserPanelOverlay.cs | 4 +-- ...{SelectionGrid.cs => BeatmapSelectGrid.cs} | 22 +++++++-------- ...electionPanel.cs => BeatmapSelectPanel.cs} | 4 +-- .../BeatmapSelect/SubScreenBeatmapSelect.cs | 17 ++++++------ .../Matchmaking/Match/MatchmakingAvatar.cs | 2 +- ...MatchmakingUserPanel.cs => PlayerPanel.cs} | 5 ++-- ...rPanelOverlay.cs => PlayerPanelOverlay.cs} | 22 +++++++-------- .../Match/ScreenMatchmaking.ScreenStack.cs | 4 +-- 11 files changed, 67 insertions(+), 50 deletions(-) rename osu.Game.Tests/Visual/Matchmaking/{TestSceneSelectionGrid.cs => TestSceneBeatmapSelectGrid.cs} (84%) rename osu.Game.Tests/Visual/Matchmaking/{TestSceneSelectionPanel.cs => TestSceneBeatmapSelectPanel.cs} (88%) rename osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/{SelectionGrid.cs => BeatmapSelectGrid.cs} (94%) rename osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/{SelectionPanel.cs => BeatmapSelectPanel.cs} (99%) rename osu.Game/Screens/OnlinePlay/Matchmaking/Match/{MatchmakingUserPanel.cs => PlayerPanel.cs} (98%) rename osu.Game/Screens/OnlinePlay/Matchmaking/Match/{UserPanelOverlay.cs => PlayerPanelOverlay.cs} (93%) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs similarity index 84% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 6fba5af070..93a33bdd95 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -10,6 +10,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.OnlinePlay; @@ -17,11 +18,11 @@ using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneSelectionGrid : OnlinePlayTestScene + public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene { private MultiplayerPlaylistItem[] items = null!; - private SelectionGrid grid = null!; + private BeatmapSelectGrid grid = null!; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; @@ -58,7 +59,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("add grid", () => Child = grid = new SelectionGrid + AddStep("add grid", () => Child = grid = new BeatmapSelectGrid { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -82,6 +83,22 @@ namespace osu.Game.Tests.Visual.Matchmaking { // test scene is weird. }); + + AddStep("add selection 1", () => grid.ChildrenOfType().First().AddUser(new APIUser + { + Id = 6411631, + Username = "Maarvin", + }, isOwnUser: true)); + AddStep("add selection 2", () => grid.ChildrenOfType().Skip(5).First().AddUser(new APIUser + { + Id = 2, + Username = "peppy", + })); + AddStep("add selection 3", () => grid.ChildrenOfType().Skip(10).First().AddUser(new APIUser + { + Id = 1040328, + Username = "smoogipoo", + })); } [Test] @@ -154,7 +171,7 @@ namespace osu.Game.Tests.Visual.Matchmaking var (candidateItems, _) = pickRandomItems(count); grid.TransferCandidatePanelsToRollContainer(candidateItems); - grid.Delay(SelectionGrid.ARRANGE_DELAY) + grid.Delay(BeatmapSelectGrid.ARRANGE_DELAY) .Schedule(() => grid.ArrangeItemsForRollAnimation()); }); @@ -162,7 +179,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("display roll order", () => { - var panels = grid.ChildrenOfType().ToArray(); + var panels = grid.ChildrenOfType().ToArray(); for (int i = 0; i < panels.Length; i++) { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs similarity index 88% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 6745802b30..2de4d6d7ea 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -12,7 +12,7 @@ using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneSelectionPanel : MultiplayerTestScene + public partial class TestSceneBeatmapSelectPanel : MultiplayerTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -20,9 +20,9 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestBeatmapPanel() { - SelectionPanel? panel = null; + BeatmapSelectPanel? panel = null; - AddStep("add panel", () => Child = panel = new SelectionPanel(new MultiplayerPlaylistItem()) + AddStep("add panel", () => Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index a7c14cfd94..1ef5e2edc1 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestScenePlayerPanel : MultiplayerTestScene { - private MatchmakingUserPanel panel = null!; + private PlayerPanel panel = null!; public override void SetUpSteps() { @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); - AddStep("add panel", () => Child = panel = new MatchmakingUserPanel(new MultiplayerRoomUser(1) + AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1) { User = new APIUser { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs index 9ed233a507..f48e489370 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneUserPanelOverlay : MultiplayerTestScene { - private UserPanelOverlay list = null!; + private PlayerPanelOverlay list = null!; public override void SetUpSteps() { @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Matchmaking Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Size = new Vector2(0.8f), - Child = list = new UserPanelOverlay() + Child = list = new PlayerPanelOverlay() }); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index bd75514b30..209c6f553a 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -22,7 +22,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class SelectionGrid : CompositeDrawable + public partial class BeatmapSelectGrid : CompositeDrawable { public const double ARRANGE_DELAY = 200; @@ -30,17 +30,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private const double arrange_duration = 1000; private const double roll_duration = 4000; private const double present_beatmap_delay = 1200; - private const float panel_spacing = 20; + private const float panel_spacing = 4; public event Action? ItemSelected; [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly Dictionary panelLookup = new Dictionary(); + private readonly Dictionary panelLookup = new Dictionary(); private readonly PanelGridContainer panelGridContainer; - private readonly Container rollContainer; + private readonly Container rollContainer; private readonly OsuScrollContainer scroll; private bool allowSelection = true; @@ -51,7 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private Sample? swooshSample; private double? lastSamplePlayback; - public SelectionGrid() + public BeatmapSelectGrid() { InternalChildren = new Drawable[] { @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Spacing = new Vector2(panel_spacing) }, }, - rollContainer = new Container + rollContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public void AddItem(MultiplayerPlaylistItem item) { - var panel = panelLookup[item.ID] = new SelectionPanel(item) + var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item) { Size = new Vector2(300, 70), AllowSelection = allowSelection, @@ -176,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect var rng = new Random(); - var remainingPanels = new List(); + var remainingPanels = new List(); foreach (var panel in panelGridContainer.Children.ToArray()) { @@ -216,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { var panel = rollContainer.Children[i]; - var position = positions[i] * (SelectionPanel.SIZE + new Vector2(panel_spacing)); + var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing)); panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); @@ -285,7 +285,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex) numSteps++; - SelectionPanel? lastPanel = null; + BeatmapSelectPanel? lastPanel = null; for (int i = 0; i < numSteps; i++) { @@ -346,7 +346,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect PresentRolledBeatmap(finalItem); } - private partial class PanelGridContainer : FillFlowContainer + private partial class PanelGridContainer : FillFlowContainer { public bool LayoutDisabled; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs similarity index 99% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index 1a51ddac64..ab89dcd65f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -28,7 +28,7 @@ using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class SelectionPanel : Container + public partial class BeatmapSelectPanel : Container { public static readonly Vector2 SIZE = new Vector2(300, 70); @@ -52,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public override bool PropagatePositionalInputSubTree => AllowSelection; - public SelectionPanel(MultiplayerPlaylistItem item) + public BeatmapSelectPanel(MultiplayerPlaylistItem item) { Item = item; Size = SIZE; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index cf86deeb3e..4b34125517 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Split; public override Drawable PlayersDisplayArea { get; } - private readonly SelectionGrid selectionGrid; + private readonly BeatmapSelectGrid beatmapSelectGrid; [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 200 }, - Child = selectionGrid = new SelectionGrid + Child = beatmapSelectGrid = new BeatmapSelectGrid { RelativeSizeAxes = Axes.Both, }, @@ -37,8 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5 }, - Child = PlayersDisplayArea = Empty().With(d => + Child = PlayersDisplayArea = new Container().With(d => { d.RelativeSizeAxes = Axes.Both; }) @@ -55,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect foreach (var item in client.Room!.Playlist) onItemAdded(item); - selectionGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); + beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); client.MatchmakingItemSelected += onItemSelected; client.MatchmakingItemDeselected += onItemDeselected; @@ -66,22 +65,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect if (item.Expired) return; - selectionGrid.AddItem(item); + beatmapSelectGrid.AddItem(item); }); private void onItemSelected(int userId, long itemId) { var user = client.Room!.Users.First(it => it.UserID == userId).User!; - selectionGrid.SetUserSelection(user, itemId, true); + beatmapSelectGrid.SetUserSelection(user, itemId, true); } private void onItemDeselected(int userId, long itemId) { var user = client.Room!.Users.First(it => it.UserID == userId).User!; - selectionGrid.SetUserSelection(user, itemId, false); + beatmapSelectGrid.SetUserSelection(user, itemId, false); } - public void RollFinalBeatmap(long[] candidateItems, long finalItem) => selectionGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); + public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs index faf32c6604..53db2114c7 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { /// /// A circular player avatar used in matchmaking displays. - /// Is part of a but can also be used in isolation for a more ambient/decorative user display. + /// Is part of a but can also be used in isolation for a more ambient/decorative user display. /// public partial class MatchmakingAvatar : CompositeDrawable { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs similarity index 98% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index ce4b471df4..f18a33c830 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match /// A panel used throughout matchmaking to represent a user, including local information like their /// rank and high level statistics in the matchmaking system. /// - public partial class MatchmakingUserPanel : UserPanel + public partial class PlayerPanel : UserPanel { public static readonly Vector2 SIZE_HORIZONTAL = new Vector2(250, 100); public static readonly Vector2 SIZE_VERTICAL = new Vector2(150, 200); @@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private bool horizontal; - public MatchmakingUserPanel(MultiplayerRoomUser user) + public PlayerPanel(MultiplayerRoomUser user) : base(user.User!) { RoomUser = user; @@ -66,6 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { Masking = true; CornerRadius = 10; + CornerExponent = 10; Add(scaleContainer = new Container { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs similarity index 93% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index 9ddddda710..8ce080f633 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -18,12 +18,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match /// A component which maintains the layout of the players in a matchmaking room. /// Can be controlled to display the panels in a certain location and in multiple styles. /// - public partial class UserPanelOverlay : CompositeDrawable + public partial class PlayerPanelOverlay : CompositeDrawable { [Resolved] private MultiplayerClient client { get; set; } = null!; - private Container panels = null!; + private Container panels = null!; private PlayerPanelCellContainer gridLayout = null!; private PlayerPanelCellContainer splitLayoutLeft = null!; private PlayerPanelCellContainer splitLayoutRight = null!; @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match gridLayout = new PlayerPanelCellContainer { RelativeSizeAxes = Axes.Both, - Spacing = new Vector2(20, 5), + Spacing = new Vector2(20), }, splitLayoutLeft = new PlayerPanelCellContainer { @@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Vertical, - Spacing = new Vector2(20, 5), + Spacing = new Vector2(5), }, splitLayoutRight = new PlayerPanelCellContainer { @@ -58,9 +58,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Vertical, - Spacing = new Vector2(20, 5), + Spacing = new Vector2(5), }, - panels = new Container + panels = new Container { RelativeSizeAxes = Axes.Both } @@ -110,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => { - panels.Add(new MatchmakingUserPanel(user) + panels.Add(new PlayerPanel(user) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -215,7 +215,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match [Resolved] private MultiplayerClient client { get; set; } = null!; - public void AcquirePanels(MatchmakingUserPanel[] panels) + public void AcquirePanels(PlayerPanel[] panels) { while (Count < panels.Length) { @@ -259,10 +259,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private partial class PlayerPanelCell : Drawable { - private MatchmakingUserPanel? panel; + private PlayerPanel? panel; private bool isAnimating; - public void AcquirePanel(MatchmakingUserPanel panel) + public void AcquirePanel(PlayerPanel panel) { this.panel = panel; isAnimating = true; @@ -280,7 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (panel == null) return; - Size = panel.Horizontal ? MatchmakingUserPanel.SIZE_HORIZONTAL : MatchmakingUserPanel.SIZE_VERTICAL; + Size = panel.Horizontal ? PlayerPanel.SIZE_HORIZONTAL : PlayerPanel.SIZE_VERTICAL; Size *= panel.Scale; var targetPos = getFinalPosition(); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs index 29a1acb2b8..c1f436e0c9 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private MultiplayerClient client { get; set; } = null!; private Framework.Screens.ScreenStack screenStack = null!; - private UserPanelOverlay playersList = null!; + private PlayerPanelOverlay playersList = null!; [BackgroundDependencyLoader] private void load() @@ -45,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match screenStack = new Framework.Screens.ScreenStack(), } }, - playersList = new UserPanelOverlay + playersList = new PlayerPanelOverlay { DisplayArea = this }, From 7879e091c0bb8e1515271479b315fb47eaafd8f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 14:39:20 +0900 Subject: [PATCH 217/267] Simplify avatar handling in beatmap selection panels --- .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index ab89dcd65f..13bf0dea45 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -95,13 +95,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } }, - selectionOverlay = new AvatarOverlay - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10 }, - Origin = Anchor.CentreLeft, - }, + selectionOverlay = new AvatarOverlay() } }, new HoverClickSounds(), @@ -358,36 +352,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private partial class AvatarOverlay : CompositeDrawable { - private readonly Dictionary avatars = new Dictionary(); - - private readonly Container avatarContainer; + private readonly Container avatars; private Sample? userAddedSample; private double? lastSamplePlayback; - public new Axes AutoSizeAxes - { - get => base.AutoSizeAxes; - set => base.AutoSizeAxes = value; - } - - public new MarginPadding Padding - { - get => base.Padding; - set => base.Padding = value; - } - public AvatarOverlay() { - InternalChild = avatarContainer = new Container(); + InternalChild = avatars = new Container(); + + Padding = new MarginPadding(5); + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; } protected override void LoadComplete() { base.LoadComplete(); - avatarContainer.AutoSizeAxes = AutoSizeAxes; - avatarContainer.RelativeSizeAxes = RelativeSizeAxes; + avatars.RelativeSizeAxes = Axes.X; + avatars.AutoSizeAxes = Axes.Y; } [BackgroundDependencyLoader] @@ -398,7 +383,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public bool AddUser(APIUser user, bool isOwnUser) { - if (avatars.ContainsKey(user.Id)) + if (avatars.Any(a => a.User.Id == user.Id)) return false; var avatar = new SelectionAvatar(user, isOwnUser) @@ -407,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Origin = Anchor.CentreRight, }; - avatarContainer.Add(avatars[user.Id] = avatar); + avatars.Add(avatar); if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) { @@ -424,11 +409,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public bool RemoveUser(int id) { - if (!avatars.Remove(id, out var avatar)) + if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) return false; avatar.PopOutAndExpire(); - avatarContainer.ChangeChildDepth(avatar, float.MaxValue); + avatars.ChangeChildDepth(avatar, float.MaxValue); updateLayout(); @@ -443,9 +428,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect double delay = 0; float x = 0; - for (int i = avatarContainer.Count - 1; i >= 0; i--) + for (int i = avatars.Count - 1; i >= 0; i--) { - var avatar = avatarContainer[i]; + var avatar = avatars[i]; if (avatar.Expired) continue; @@ -460,12 +445,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public partial class SelectionAvatar : CompositeDrawable { + public APIUser User { get; } + public bool Expired { get; private set; } private readonly Container content; public SelectionAvatar(APIUser user, bool isOwnUser) { + User = user; Size = new Vector2(30); InternalChildren = new Drawable[] From d690477776a53f80fbb840ee05b9f6d3ead50c6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 14:52:23 +0900 Subject: [PATCH 218/267] Add context menu to show beatmap details --- .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index 13bf0dea45..a2f10e05f4 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -11,7 +11,9 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -20,6 +22,7 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -196,7 +199,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } // TODO: combine following two classes with above implementation for simplicity? - private partial class BeatmapPanel : CompositeDrawable + private partial class BeatmapPanel : CompositeDrawable, IHasContextMenu { public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both }; @@ -283,6 +286,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } + [Resolved] + private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + + public MenuItem[] ContextMenuItems + { + get + { + // this is very weird, but the beatmap may be null while loading because reasons. + if (beatmap == null) + return []; + + return new MenuItem[] + { + new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => + { + beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet!.OnlineID); + }), + }; + } + } + private partial class BeatmapPanelContent : CompositeDrawable { private readonly APIBeatmap beatmap; @@ -400,7 +424,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect lastSamplePlayback = Time.Current; } - updateLayout(); + updateAvatarLayout(); avatar.FinishTransforms(); @@ -415,12 +439,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect avatar.PopOutAndExpire(); avatars.ChangeChildDepth(avatar, float.MaxValue); - updateLayout(); + updateAvatarLayout(); return true; } - private void updateLayout() + private void updateAvatarLayout() { const double stagger = 30; const float spacing = 4; From e9063dcf57dc93fdc0bbbd06f843c6e5a9245b80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 15:27:10 +0900 Subject: [PATCH 219/267] Simplify flash layer --- .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 155 +++++++----------- 1 file changed, 63 insertions(+), 92 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index a2f10e05f4..053c89da3e 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -44,15 +44,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private readonly BeatmapPanel beatmapPanel; private readonly AvatarOverlay selectionOverlay; private readonly Container border; - private readonly Box flash; + + private readonly Drawable lighting; public bool AllowSelection; public Action? Action; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - public override bool PropagatePositionalInputSubTree => AllowSelection; public BeatmapSelectPanel(MultiplayerPlaylistItem item) @@ -60,76 +58,64 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Item = item; Size = SIZE; - InternalChildren = new Drawable[] + InternalChild = scaleContainer = new Container { - scaleContainer = new Container + Masking = true, + CornerRadius = 6, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + new HoverClickSounds(), + new Container { - new Container + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-border_width), + Child = border = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(-border_width), - Child = border = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius + border_width, - Alpha = 0, - Child = new Box { RelativeSizeAxes = Axes.Both }, - } - }, - beatmapPanel = new BeatmapPanel - { - RelativeSizeAxes = Axes.Both, - OverlayLayer = - { - Children = new[] - { - flash = new Box - { - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - } - } - }, - selectionOverlay = new AvatarOverlay() + Masking = true, + CornerRadius = corner_radius + border_width, + Alpha = 0, + Child = new Box { RelativeSizeAxes = Axes.Both }, + } + }, + beatmapPanel = new BeatmapPanel { RelativeSizeAxes = Axes.Both }, + lighting = new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, } - }, - new HoverClickSounds(), + } }; } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load(BeatmapLookupCache lookupCache) { - base.LoadComplete(); - - beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => + lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => { var beatmap = b.GetResultSafely()!; - beatmap.StarRating = Item.StarRating; - beatmapPanel.Beatmap = beatmap; })); } public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser); - - public bool RemoveUser(int userId) => selectionOverlay.RemoveUser(userId); - - public bool RemoveUser(APIUser user) => RemoveUser(user.Id); + public bool RemoveUser(APIUser user) => selectionOverlay.RemoveUser(user.Id); protected override bool OnHover(HoverEvent e) { - flash.FadeTo(0.2f, 50) - .Then() - .FadeTo(0.1f, 300); + lighting.FadeTo(0.2f, 50) + .Then() + .FadeTo(0.1f, 300); return true; } @@ -138,7 +124,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { base.OnHoverLost(e); - flash.FadeOut(200); + lighting.FadeOut(200); } protected override bool OnMouseDown(MouseDownEvent e) @@ -166,9 +152,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { Action?.Invoke(Item); - flash.FadeTo(0.5f, 50) - .Then() - .FadeTo(0.1f, 400); + lighting.FadeTo(0.5f, 50) + .Then() + .FadeTo(0.1f, 400); return true; } @@ -201,11 +187,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect // TODO: combine following two classes with above implementation for simplicity? private partial class BeatmapPanel : CompositeDrawable, IHasContextMenu { - public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both }; - public APIBeatmap? Beatmap { - get => beatmap; set { if (beatmap?.OnlineID == value?.OnlineID) @@ -233,6 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { Masking = true; CornerRadius = 6; + CornerExponent = 10; InternalChildren = new Drawable[] { @@ -254,7 +238,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { RelativeSizeAxes = Axes.Both, }, - OverlayLayer, }; } @@ -383,20 +366,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public AvatarOverlay() { - InternalChild = avatars = new Container(); + AutoSizeAxes = Axes.Both; + + InternalChild = avatars = new Container + { + AutoSizeAxes = Axes.X, + Height = SelectionAvatar.AVATAR_SIZE, + }; Padding = new MarginPadding(5); - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - avatars.RelativeSizeAxes = Axes.X; - avatars.AutoSizeAxes = Axes.Y; } [BackgroundDependencyLoader] @@ -410,11 +388,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect if (avatars.Any(a => a.User.Id == user.Id)) return false; - var avatar = new SelectionAvatar(user, isOwnUser) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }; + var avatar = new SelectionAvatar(user, isOwnUser); avatars.Add(avatar); @@ -469,26 +443,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public partial class SelectionAvatar : CompositeDrawable { + public const float AVATAR_SIZE = 30; + public APIUser User { get; } public bool Expired { get; private set; } - private readonly Container content; + private readonly MatchmakingAvatar avatar; public SelectionAvatar(APIUser user, bool isOwnUser) { User = user; - Size = new Vector2(30); + Size = new Vector2(AVATAR_SIZE); - InternalChildren = new Drawable[] + InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) { - content = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Child = new MatchmakingAvatar(user, isOwnUser) - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }; } @@ -496,14 +467,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { base.LoadComplete(); - content.ScaleTo(0) - .ScaleTo(1, 500, Easing.OutElasticHalf) - .FadeIn(200); + avatar.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); } public void PopOutAndExpire() { - content.ScaleTo(0, 400, Easing.OutExpo); + avatar.ScaleTo(0, 400, Easing.OutExpo); this.FadeOut(100).Expire(); Expired = true; From 3af4edf0512290d5f74f10f575a3341f6b586963 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 15:23:06 +0900 Subject: [PATCH 220/267] Remove pointless `TestSceneMatchmakingScreenStack` Should always be using `TestSceneMatchmakingScreen` right? --- .../TestSceneMatchmakingScreenStack.cs | 113 ------------------ 1 file changed, 113 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs deleted file mode 100644 index ba7e27b753..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs +++ /dev/null @@ -1,113 +0,0 @@ -// 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.Extensions; -using osu.Framework.Graphics; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Match; -using osu.Game.Tests.Visual.Multiplayer; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneMatchmakingScreenStack : MultiplayerTestScene - { - private const int user_count = 8; - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("join room", () => - { - var room = CreateDefaultRoom(MatchType.Matchmaking); - room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - })).ToArray(); - - JoinRoom(room); - }); - - WaitForJoined(); - - AddStep("add carousel", () => - { - Child = new ScreenMatchmaking.ScreenStack - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }; - }); - - AddStep("join users", () => - { - var users = Enumerable.Range(1, user_count).Select(i => new MultiplayerRoomUser(i) - { - User = new APIUser - { - Username = $"Player {i}" - } - }).ToArray(); - - foreach (var user in users) - MultiplayerClient.AddUser(user); - }); - } - - [Test] - public void TestChangeStage() - { - for (int round = 1; round <= 2; round++) - { - AddLabel($"Round {round}"); - - int r = round; - changeStage(MatchmakingStage.RoundWarmupTime, state => state.CurrentRound = r); - changeStage(MatchmakingStage.UserBeatmapSelect); - changeStage(MatchmakingStage.ServerBeatmapFinalised, state => - { - MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 8).Select(i => new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - }).ToArray(); - - state.CandidateItems = beatmaps.Select(b => b.ID).ToArray(); - state.CandidateItem = beatmaps[0].ID; - }, waitTime: 35); - - changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload); - changeStage(MatchmakingStage.GameplayWarmupTime); - changeStage(MatchmakingStage.Gameplay); - changeStage(MatchmakingStage.ResultsDisplaying); - } - - changeStage(MatchmakingStage.Ended, state => - { - int localUserId = API.LocalUser.Value.OnlineID; - - state.Users[localUserId].Placement = 1; - state.Users[localUserId].Rounds[1].Placement = 1; - state.Users[localUserId].Rounds[1].TotalScore = 1; - state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1; - }); - } - - private void changeStage(MatchmakingStage stage, Action? prepare = null, int waitTime = 5) - { - AddStep($"stage: {stage}", () => MultiplayerClient.MatchmakingChangeStage(stage, prepare).WaitSafely()); - AddWaitStep("wait", waitTime); - } - } -} From 9dc5605d9500d6a10095cd513189c203231ab173 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 16:15:37 +0900 Subject: [PATCH 221/267] Scale active stage larger --- .../Matchmaking/Match/StageDisplay.StageSegment.cs | 10 +++++++++- .../OnlinePlay/Matchmaking/Match/StageDisplay.cs | 7 +++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs index f97bf9fe68..301cac1437 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs @@ -42,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private Sample? countdownTickSample; private double? lastSamplePlayback; + private Container mainContent = null!; + public bool Active { get; private set; } public float Progress => progressBar.Width; @@ -49,10 +51,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public StageSegment(int? round, MatchmakingStage stage, LocalisableString displayText) { Round = round; + this.stage = stage; this.displayText = displayText; AutoSizeAxes = Axes.Both; + + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; } [BackgroundDependencyLoader] @@ -74,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Icon = FontAwesome.Solid.ArrowRight, Margin = new MarginPadding { Horizontal = 10 } }, - new Container + mainContent = new Container { Masking = true, CornerRadius = 5, @@ -178,6 +184,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (wasActive) progressBar.Width = 1; + mainContent.ScaleTo(Active ? 1.3f : 1, 500, Easing.OutQuint); + bool isPreparing = (stage == MatchmakingStage.RoundWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsJoin) || (stage == MatchmakingStage.GameplayWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsBeatmapDownload) || diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs index db302163a5..419824549b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs @@ -57,17 +57,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { scroll = new StageScrollContainer { - ScrollbarOverlapsContent = false, ScrollbarVisible = false, ClampExtension = 0, RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Height = 36, + Height = HEIGHT, Child = flow = new FillFlowContainer { Padding = new MarginPadding { Horizontal = 2000 }, AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Direction = FillDirection.Horizontal, }, }, From c66cc5ebb149b59280447fe1e60ec8c3f20a7134 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 26 Sep 2025 09:28:04 +0200 Subject: [PATCH 222/267] Handle disabled text in composition tool button --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 8 -------- osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs | 5 ++++- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 27ea7863bf..c138808890 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -262,14 +262,6 @@ namespace osu.Game.Rulesets.Edit .Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t))) .ToList(); - foreach (var item in toolboxCollection.Items) - { - item.Selected.DisabledChanged += isDisabled => - { - item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).Tool.TooltipText; - }; - } - togglesCollection.AddRange(CreateTernaryButtons().ToArray()); sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates); diff --git a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs index 641d60dbd3..65a0fb983a 100644 --- a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs +++ b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs @@ -16,7 +16,10 @@ namespace osu.Game.Rulesets.Edit { Tool = tool; - TooltipText = tool.TooltipText; + Selected.BindDisabledChanged(isDisabled => + { + TooltipText = isDisabled ? "Add at least one timing point first!" : Tool.TooltipText; + }, true); } } } From 76c304391348cb76d82b96c14e985d66b41f719d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 16:48:56 +0900 Subject: [PATCH 223/267] Improve selection animation border isolation I'm not final on the design, just wanted to split it out into an actual border element rather than an "underlay". --- .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 2 +- .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 107 ++++++++++++------ 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 209c6f553a..a784f644ab 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -330,7 +330,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { rollContainer.ChangeChildDepth(panel, float.MinValue); - panel.ShowBorder(); + panel.ShowChosenBorder(); panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) .ScaleTo(1.5f, 1000, Easing.OutExpo); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index 053c89da3e..fdb3954535 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; @@ -27,6 +28,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect @@ -35,21 +37,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { public static readonly Vector2 SIZE = new Vector2(300, 70); - private const float corner_radius = 6; - private const float border_width = 3; + public bool AllowSelection { get; set; } public readonly MultiplayerPlaylistItem Item; - private readonly Container scaleContainer; - private readonly BeatmapPanel beatmapPanel; - private readonly AvatarOverlay selectionOverlay; - private readonly Container border; + public Action? Action { private get; init; } - private readonly Drawable lighting; + private const float corner_radius = 6; + private const float border_width = 3; - public bool AllowSelection; + private Container scaleContainer = null!; + private BeatmapPanel beatmapPanel = null!; + private AvatarOverlay selectionOverlay = null!; + private Drawable lighting = null!; - public Action? Action; + private Container border = null!; public override bool PropagatePositionalInputSubTree => AllowSelection; @@ -57,49 +59,71 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { Item = item; Size = SIZE; + } + [BackgroundDependencyLoader] + private void load(BeatmapLookupCache lookupCache, OverlayColourProvider colourProvider) + { InternalChild = scaleContainer = new Container { - Masking = true, - CornerRadius = 6, RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Children = new[] { - new HoverClickSounds(), new Container { + Masking = true, + CornerRadius = corner_radius, + CornerExponent = 10, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(-border_width), - Child = border = new Container + Children = new[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius + border_width, - Alpha = 0, - Child = new Box { RelativeSizeAxes = Axes.Both }, + new HoverClickSounds(), + beatmapPanel = new BeatmapPanel { RelativeSizeAxes = Axes.Both }, + lighting = new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } } }, - beatmapPanel = new BeatmapPanel { RelativeSizeAxes = Axes.Both }, - lighting = new Box + border = new Container { - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, Alpha = 0, + Masking = true, + CornerRadius = corner_radius, + Blending = BlendingParameters.Additive, + CornerExponent = 10, + RelativeSizeAxes = Axes.Both, + BorderThickness = border_width, + BorderColour = colourProvider.Light1, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 40, + Roundness = 300, + Colour = colourProvider.Light3.Opacity(0.1f), + }, + Children = new Drawable[] + { + new Box + { + AlwaysPresent = true, + Alpha = 0, + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + } }, - selectionOverlay = new AvatarOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - } } }; - } - - [BackgroundDependencyLoader] - private void load(BeatmapLookupCache lookupCache) - { lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => { var beatmap = b.GetResultSafely()!; @@ -159,9 +183,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect return true; } - public void ShowBorder() => border.Show(); + public void ShowChosenBorder() + { + border.FadeTo(1, 1000, Easing.OutQuint); + } - public void HideBorder() => border.Hide(); + public void ShowBorder() + { + border.FadeTo(1, 80, Easing.OutQuint) + .Then() + .FadeTo(0.7f, 800, Easing.OutQuint); + } + + public void HideBorder() + { + border.FadeOut(500, Easing.OutQuint); + } public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) { From badeb24d566e263e56c41a55f1feda1bf14653ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 16:53:22 +0900 Subject: [PATCH 224/267] Change beatmap in selection panels to always be non-null --- .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 65 ++++++------------- 1 file changed, 21 insertions(+), 44 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index fdb3954535..01ffe86139 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -47,11 +47,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private const float border_width = 3; private Container scaleContainer = null!; - private BeatmapPanel beatmapPanel = null!; private AvatarOverlay selectionOverlay = null!; private Drawable lighting = null!; private Container border = null!; + private Container mainContent = null!; public override bool PropagatePositionalInputSubTree => AllowSelection; @@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Origin = Anchor.Centre, Children = new[] { - new Container + mainContent = new Container { Masking = true, CornerRadius = corner_radius, @@ -80,7 +80,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Children = new[] { new HoverClickSounds(), - beatmapPanel = new BeatmapPanel { RelativeSizeAxes = Axes.Both }, lighting = new Box { Blending = BlendingParameters.Additive, @@ -128,7 +127,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { var beatmap = b.GetResultSafely()!; beatmap.StarRating = Item.StarRating; - beatmapPanel.Beatmap = beatmap; + + mainContent.Add(new BeatmapPanel(beatmap) + { + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both + }); })); } @@ -224,26 +228,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect // TODO: combine following two classes with above implementation for simplicity? private partial class BeatmapPanel : CompositeDrawable, IHasContextMenu { - public APIBeatmap? Beatmap - { - set - { - if (beatmap?.OnlineID == value?.OnlineID) - return; - - beatmap = value; - - if (IsLoaded) - updateContent(); - } - } - - private APIBeatmap? beatmap; + private readonly APIBeatmap beatmap; private Container content = null!; private UpdateableOnlineBeatmapSetCover cover = null!; - public BeatmapPanel(APIBeatmap? beatmap = null) + public BeatmapPanel(APIBeatmap beatmap) { this.beatmap = beatmap; } @@ -291,41 +281,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect foreach (var child in content.Children) child.FadeOut(300).Expire(); - cover.OnlineInfo = beatmap?.BeatmapSet; + cover.OnlineInfo = beatmap.BeatmapSet; - if (beatmap != null) + var panelContent = new BeatmapPanelContent(beatmap) { - var panelContent = new BeatmapPanelContent(beatmap) - { - RelativeSizeAxes = Axes.Both, - }; + RelativeSizeAxes = Axes.Both, + }; - content.Add(panelContent); + content.Add(panelContent); - panelContent.FadeInFromZero(300); - } + panelContent.FadeInFromZero(300); } [Resolved] private BeatmapSetOverlay? beatmapSetOverlay { get; set; } - public MenuItem[] ContextMenuItems + public MenuItem[] ContextMenuItems => new MenuItem[] { - get + new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => { - // this is very weird, but the beatmap may be null while loading because reasons. - if (beatmap == null) - return []; - - return new MenuItem[] - { - new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => - { - beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet!.OnlineID); - }), - }; - } - } + beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet!.OnlineID); + }), + }; private partial class BeatmapPanelContent : CompositeDrawable { From da80b61f3888869a01d5c2ecb68f69511fdca5ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 18:08:47 +0900 Subject: [PATCH 225/267] Allow visibly disabling the "go to beatmap" button Easiest way to make this work without rewriting the layout logic. I think it makes sense to have the button still exist there but not be usable on certain screens. --- .../Cards/Buttons/GoToBeatmapButton.cs | 32 ++++++++++++++++--- .../Cards/CollapsibleButtonContainer.cs | 22 +++++++------ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs index d2c077d010..3d732b6683 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs @@ -16,10 +16,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private readonly Bindable state = new Bindable(); private readonly APIBeatmapSet beatmapSet; + private readonly bool allowNavigationToBeatmap; - public GoToBeatmapButton(APIBeatmapSet beatmapSet) + public GoToBeatmapButton(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap) { this.beatmapSet = beatmapSet; + this.allowNavigationToBeatmap = allowNavigationToBeatmap; } [BackgroundDependencyLoader(true)] @@ -27,7 +29,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons { Action = () => game?.PresentBeatmap(beatmapSet); Icon.Icon = FontAwesome.Solid.AngleDoubleRight; - TooltipText = "Go to beatmap"; } protected override void LoadComplete() @@ -40,8 +41,31 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private void updateState() { - Enabled.Value = state.Value == DownloadState.LocallyAvailable; - this.FadeTo(Enabled.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + bool available = state.Value == DownloadState.LocallyAvailable; + Enabled.Value = allowNavigationToBeatmap && available; + + float alpha; + + if (available && allowNavigationToBeatmap) + { + TooltipText = "Go to beatmap"; + Enabled.Value = true; + alpha = 1f; + } + else if (available) + { + TooltipText = string.Empty; + Enabled.Value = false; + alpha = 0.3f; + } + else + { + TooltipText = string.Empty; + Enabled.Value = false; + alpha = 0; + } + + this.FadeTo(alpha, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index 56d405ce3c..8283d97817 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards set { buttonsExpandedWidth = value; - buttonArea.Width = value; if (IsLoaded) updateState(); } @@ -67,7 +66,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public CollapsibleButtonContainer(APIBeatmapSet beatmapSet) + public CollapsibleButtonContainer(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap = true) { downloadTracker = new BeatmapDownloadTracker(beatmapSet); @@ -116,14 +115,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Both, Height = 0.5f, }, - new GoToBeatmapButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State }, - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - } } } }, @@ -152,6 +143,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards } } }; + + buttons.Add(new GoToBeatmapButton(beatmapSet, allowNavigationToBeatmap) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State }, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + }); } protected override void LoadComplete() @@ -165,6 +165,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() { + buttonArea.Width = buttonsExpandedWidth; + float buttonAreaWidth = ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth; float mainAreaWidth = Width - buttonAreaWidth; From 0a17a3c4edc6e879f05c696fb8b1968379cb5682 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 18:09:05 +0900 Subject: [PATCH 226/267] Use `BeatmapCard` for matchmaking beatmap display --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 4 +- .../BeatmapSelect/BeatmapCardMatchmaking.cs | 354 ++++++++++++++++++ .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 1 - .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 176 +-------- 4 files changed, 365 insertions(+), 170 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 54f8d656fe..4c0466fa04 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards public const float TRANSITION_DURATION = 340; public const float CORNER_RADIUS = 8; - protected const float WIDTH = 345; + public const float WIDTH = 345; public IBindable Expanded { get; } @@ -77,7 +77,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards containingInputManager = GetContainingInputManager(); - Action = () => + Action ??= () => { if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs new file mode 100644 index 0000000000..c9df4610f9 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs @@ -0,0 +1,354 @@ +// 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 System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Beatmaps.Drawables.Cards.Statistics; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapSet; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class BeatmapCardMatchmaking : BeatmapCard + { + private readonly APIBeatmap beatmap; + + protected override Drawable IdleContent => idleBottomContent; + protected override Drawable DownloadInProgressContent => downloadProgressBar; + + public const float HEIGHT = 80; + + [Cached] + private readonly BeatmapCardContent content; + + private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; + + private FillFlowContainer statisticsContainer = null!; + + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public BeatmapCardMatchmaking(APIBeatmap beatmap) + : base(beatmap.BeatmapSet!, false) + { + this.beatmap = beatmap; + content = new BeatmapCardContent(HEIGHT); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Width = WIDTH; + Height = HEIGHT; + + FillFlowContainer leftIconArea = null!; + FillFlowContainer titleBadgeArea = null!; + GridContainer artistContainer = null!; + + Child = content.With(c => + { + c.MainContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet) + { + Name = @"Left (icon) area", + Size = new Vector2(HEIGHT), + Padding = new MarginPadding { Right = CORNER_RADIUS }, + Child = leftIconArea = new FillFlowContainer + { + Margin = new MarginPadding(4), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) + } + }, + buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false) + { + X = HEIGHT - CORNER_RADIUS, + Width = WIDTH - HEIGHT + CORNER_RADIUS, + FavouriteState = { BindTarget = FavouriteState }, + ButtonsCollapsedWidth = 0, + ButtonsExpandedWidth = 24, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), + Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + titleBadgeArea = new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + } + } + }, + artistContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new TruncatingSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + Empty() + }, + } + }, + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 1 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(BeatmapSet.Author); + }), + } + }, + new Container + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2), + AlwaysPresent = true, + Children = new Drawable[] + { + statisticsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(8, 0), + Alpha = 0, + AlwaysPresent = true, + ChildrenEnumerable = createStatistics() + }, + new Container + { + Masking = true, + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f), + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Padding = new MarginPadding(2), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] + { + new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(0.875f), + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + }, + } + }, + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 5, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = DownloadTracker.State }, + Progress = { BindTarget = DownloadTracker.Progress } + } + } + } + } + } + } + }; + c.Expanded.BindTarget = Expanded; + }); + + if (BeatmapSet.HasVideo) + leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); + + if (BeatmapSet.HasStoryboard) + leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); + + if (BeatmapSet.FeaturedInSpotlight) + { + titleBadgeArea.Add(new SpotlightBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }); + } + + if (BeatmapSet.HasExplicitContent) + { + titleBadgeArea.Add(new ExplicitContentBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }); + } + + if (BeatmapSet.TrackId != null) + { + artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }; + } + } + + private LocalisableString createArtistText() + { + var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); + return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); + } + + private IEnumerable createStatistics() + { + var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet); + if (hypesStatistic != null) + yield return hypesStatistic; + + var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet); + if (nominationsStatistic != null) + yield return nominationsStatistic; + + yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState }; + yield return new PlayCountStatistic(BeatmapSet); + + var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet); + if (dateStatistic != null) + yield return dateStatistic; + } + + protected override void UpdateState() + { + base.UpdateState(); + + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + thumbnail.Dimmed.Value = showDetails; + + statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); + } + + public override MenuItem[] ContextMenuItems + { + get + { + var items = base.ContextMenuItems.ToList(); + + foreach (var button in buttonContainer.Buttons) + { + if (button.Enabled.Value) + items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick())); + } + + return items.ToArray(); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index a784f644ab..4d19890993 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -110,7 +110,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item) { - Size = new Vector2(300, 70), AllowSelection = allowSelection, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index 01ffe86139..3266e39905 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -9,21 +9,12 @@ using osu.Framework.Audio.Sample; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -35,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { public partial class BeatmapSelectPanel : Container { - public static readonly Vector2 SIZE = new Vector2(300, 70); + public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT); public bool AllowSelection { get; set; } @@ -43,7 +34,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public Action? Action { private get; init; } - private const float corner_radius = 6; private const float border_width = 3; private Container scaleContainer = null!; @@ -74,12 +64,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect mainContent = new Container { Masking = true, - CornerRadius = corner_radius, + CornerRadius = BeatmapCard.CORNER_RADIUS, CornerExponent = 10, RelativeSizeAxes = Axes.Both, Children = new[] { - new HoverClickSounds(), lighting = new Box { Blending = BlendingParameters.Additive, @@ -97,9 +86,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { Alpha = 0, Masking = true, - CornerRadius = corner_radius, - Blending = BlendingParameters.Additive, + CornerRadius = BeatmapCard.CORNER_RADIUS, CornerExponent = 10, + Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, BorderThickness = border_width, BorderColour = colourProvider.Light1, @@ -128,10 +117,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect var beatmap = b.GetResultSafely()!; beatmap.StarRating = Item.StarRating; - mainContent.Add(new BeatmapPanel(beatmap) + mainContent.Add(new BeatmapCardMatchmaking(beatmap) { Depth = float.MaxValue, - RelativeSizeAxes = Axes.Both + Action = () => Action?.Invoke(Item), }); })); } @@ -178,13 +167,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect protected override bool OnClick(ClickEvent e) { - Action?.Invoke(Item); - lighting.FadeTo(0.5f, 50) .Then() .FadeTo(0.1f, 400); - return true; + // pass through to let the beatmap card handle actual click. + return false; } public void ShowChosenBorder() @@ -225,152 +213,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect this.Delay(delay + duration).FadeOut().Expire(); } - // TODO: combine following two classes with above implementation for simplicity? - private partial class BeatmapPanel : CompositeDrawable, IHasContextMenu - { - private readonly APIBeatmap beatmap; - - private Container content = null!; - private UpdateableOnlineBeatmapSetCover cover = null!; - - public BeatmapPanel(APIBeatmap beatmap) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Masking = true; - CornerRadius = 6; - CornerExponent = 10; - - InternalChildren = new Drawable[] - { - cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0, timeBeforeUnload: 10000) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal( - colourProvider.Background4.Opacity(0.7f), - colourProvider.Background4.Opacity(0.4f) - ) - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateContent(); - FinishTransforms(true); - } - - private void updateContent() - { - foreach (var child in content.Children) - child.FadeOut(300).Expire(); - - cover.OnlineInfo = beatmap.BeatmapSet; - - var panelContent = new BeatmapPanelContent(beatmap) - { - RelativeSizeAxes = Axes.Both, - }; - - content.Add(panelContent); - - panelContent.FadeInFromZero(300); - } - - [Resolved] - private BeatmapSetOverlay? beatmapSetOverlay { get; set; } - - public MenuItem[] ContextMenuItems => new MenuItem[] - { - new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => - { - beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet!.OnlineID); - }), - }; - - private partial class BeatmapPanelContent : CompositeDrawable - { - private readonly APIBeatmap beatmap; - - public BeatmapPanelContent(APIBeatmap beatmap) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding { Horizontal = 12 }, - Children = new Drawable[] - { - new TruncatingSpriteText - { - Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode), - Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - }, - new TextFlowContainer(s => - { - s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); - }).With(d => - { - d.RelativeSizeAxes = Axes.X; - d.AutoSizeAxes = Axes.Y; - d.AddText("by "); - d.AddText(new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); - }), - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 6 }, - Spacing = new Vector2(4), - Children = new Drawable[] - { - new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - new TruncatingSpriteText - { - Text = beatmap.DifficultyName, - Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - }, - }; - } - } - } - private partial class AvatarOverlay : CompositeDrawable { private readonly Container avatars; From b7435062072d53f4634bc8d95f3a6ad5b9bd86a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 18:26:09 +0900 Subject: [PATCH 227/267] Keep panel backgrounds loaded --- .../Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs | 4 ++-- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs | 4 ++-- .../Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs | 4 ++-- .../Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs index deb56bb281..a57f3e7ce7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs @@ -21,7 +21,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo) + public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false) { InternalChildren = new Drawable[] { @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.Both, }, - cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), 500, 500) + cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000) { RelativeSizeAxes = Axes.Both, Colour = Colour4.Transparent diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index 1f6f638618..4a7054588e 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -35,11 +35,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo) + public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false) { InternalChildren = new Drawable[] { - new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List, keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000) { RelativeSizeAxes = Axes.Both, OnlineInfo = onlineInfo diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index 8283d97817..8262e787d8 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -66,7 +66,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public CollapsibleButtonContainer(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap = true) + public CollapsibleButtonContainer(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap = true, bool keepBackgroundLoaded = false) { downloadTracker = new BeatmapDownloadTracker(beatmapSet); @@ -126,7 +126,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Masking = true, Children = new Drawable[] { - new BeatmapCardContentBackground(beatmapSet) + new BeatmapCardContentBackground(beatmapSet, keepBackgroundLoaded) { RelativeSizeAxes = Axes.Both, Dimmed = { BindTarget = ShowDetails } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs index c9df4610f9..8fbf8491d6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet) + thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet, keepLoaded: true) { Name = @"Left (icon) area", Size = new Vector2(HEIGHT), @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Spacing = new Vector2(1) } }, - buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false) + buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true) { X = HEIGHT - CORNER_RADIUS, Width = WIDTH - HEIGHT + CORNER_RADIUS, From 0f8d8780d389b8428937e47618be67a2e250d785 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Sep 2025 18:09:14 +0900 Subject: [PATCH 228/267] Adjust stage display animation to linger for longer --- osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs index 419824549b..e383df71c9 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs @@ -225,8 +225,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match round = value.Value; - this.ScaleTo(6, 500, Easing.OutQuart) - .MoveToY(-300, 500, Easing.OutQuart) + this.ScaleTo(6, 1000, Easing.OutPow10) + .MoveToY(-300, 1000, Easing.OutPow10) .Then() .MoveToY(0, 500, Easing.InQuart) .ScaleTo(1, 500, Easing.InQuart); From 55ef22139041172d1e146ce7e98d77918aa7b507 Mon Sep 17 00:00:00 2001 From: tadatomix Date: Sun, 28 Sep 2025 22:49:45 +0300 Subject: [PATCH 229/267] Add a separate panel for `RankAchieved` group --- .../Screens/SelectV2/PanelGroupRankDisplay.cs | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs diff --git a/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs b/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs new file mode 100644 index 0000000000..c174ccaf97 --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs @@ -0,0 +1,225 @@ +// 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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +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.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Scoring; +using osuTK; +using osuTK.Graphics; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelGroupRankDisplay : Panel + { + public const float HEIGHT = PanelGroup.HEIGHT; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Drawable iconContainer = null!; + private Box backgroundBorder = null!; + private Box contentBackground = null!; + private OsuSpriteText starRatingText = null!; + private CircularContainer countPill = null!; + private OsuSpriteText countText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = PanelGroup.HEIGHT; + + Icon = iconContainer = new Container + { + AlwaysPresent = true, + RelativeSizeAxes = Axes.Y, + Alpha = 0f, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + }, + }; + + Background = backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Highlight1, + }; + + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5) + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1, colourProvider.Highlight1.Opacity(0f)), + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + UseFullGlyphHeight = false, + Font = OsuFont.Style.Heading2, + } + } + }, + countPill = new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + countText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + UseFullGlyphHeight = false, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => onExpanded(), true); + } + + private Color4 rankColour; + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + var group = (RankDisplayGroupDefinition)Item.Model; + ScoreRank rank = group.Rank; + + rankColour = OsuColour.ForRank(rank); + + AccentColour = rankColour; + backgroundBorder.Colour = rankColour; + contentBackground.Colour = rankColour.Darken(1f); + glow.Colour = ColourInfo.GradientHorizontal(rankColour, rankColour.Opacity(0f)); + + switch (rank) + { + case ScoreRank.SH: + case ScoreRank.XH: + starRatingText.Colour = ColourInfo.GradientVertical(Color4.White, Color4Extensions.FromHex("afdff0")); + iconContainer.Colour = colourProvider.Background5; + break; + + case ScoreRank.X: + case ScoreRank.S: + starRatingText.Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex(@"ffe7a8"), Color4Extensions.FromHex(@"ffb800")); + iconContainer.Colour = colourProvider.Background5; + break; + + case ScoreRank.F: + starRatingText.Colour = Color4Extensions.FromHex(@"CC3333"); + iconContainer.Colour = colourProvider.Content1; + break; + + default: + starRatingText.Colour = Color4.White; + iconContainer.Colour = colourProvider.Background5; + break; + } + + starRatingText.Text = group.Title; + + ColourInfo colour = ColourInfo.GradientHorizontal(rankColour.Darken(0.6f), rankColour.Darken(0.8f)); + + triangles.Colour = colour; + + countText.Text = Item.NestedItemCount.ToLocalisableString(@"N0"); + + onExpanded(); + } + + private void onExpanded() + { + const float duration = 500; + + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); + } + + protected override void Update() + { + base.Update(); + + // Move the count pill in the opposite direction to keep it pinned to the screen regardless of the X position of TopLevelContent. + countPill.X = -TopLevelContent.X; + } + + public override MenuItem[] ContextMenuItems + { + get + { + if (Item == null) + return Array.Empty(); + + return new MenuItem[] + { + new OsuMenuItem(Expanded.Value ? WebCommonStrings.ButtonsCollapse.ToSentence() : WebCommonStrings.ButtonsExpand.ToSentence(), MenuItemType.Highlighted, () => TriggerClick()) + }; + } + } + } +} From cf20bbbf80cae223fb8b1b44be6b8248946d5d5b Mon Sep 17 00:00:00 2001 From: tadatomix Date: Sun, 28 Sep 2025 22:55:22 +0300 Subject: [PATCH 230/267] Add a new `Rank Achieved` panel to BeatmapCarousel.cs --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 52d5989c8f..679fec76f2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -788,9 +788,11 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool setPanelPool = new DrawablePool(100); private readonly DrawablePool groupPanelPool = new DrawablePool(100); private readonly DrawablePool starsGroupPanelPool = new DrawablePool(11); + private readonly DrawablePool ranksGroupPanelPool = new DrawablePool(11); private void setupPools() { + AddInternal(ranksGroupPanelPool); AddInternal(starsGroupPanelPool); AddInternal(groupPanelPool); AddInternal(beatmapPanelPool); @@ -829,6 +831,9 @@ namespace osu.Game.Screens.SelectV2 case StarDifficultyGroupDefinition: return starsGroupPanelPool.Get(); + case RankDisplayGroupDefinition: + return ranksGroupPanelPool.Get(); + case GroupDefinition: return groupPanelPool.Get(); @@ -1085,6 +1090,11 @@ namespace osu.Game.Screens.SelectV2 /// public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); + /// + /// Defines a grouping header for a set of carousel items grouped by achieved rank. + /// + public record RankDisplayGroupDefinition(int Order, string Title, ScoreRank Rank) : GroupDefinition(Order, Title); + /// /// Used to represent a portion of a under a . /// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it. From 33791318fe22316b933ef821c54c5d9c8a323853 Mon Sep 17 00:00:00 2001 From: tadatomix Date: Sun, 28 Sep 2025 22:56:32 +0300 Subject: [PATCH 231/267] Call a new panel style, when `Rank Achieved` grouping is picked --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 69f5596578..f2159d63f5 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -433,7 +433,7 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary topRankMapping) { if (topRankMapping.TryGetValue(beatmap.ID, out var rank)) - return new GroupDefinition(-(int)rank, rank.GetDescription()).Yield(); + return new RankDisplayGroupDefinition(-(int)rank, rank.GetDescription(), rank).Yield(); return new GroupDefinition(int.MaxValue, "Unplayed").Yield(); } From 6ac4f482ee2151998f89b4fa7e7f16666e15583d Mon Sep 17 00:00:00 2001 From: tadatomix Date: Sun, 28 Sep 2025 23:10:32 +0300 Subject: [PATCH 232/267] Add a new test for `Rank Achieved` panels --- .../SongSelectV2/TestScenePanelGroup.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs index 3b9a07437a..e6a58136fa 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -3,12 +3,14 @@ using System; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Cursor; +using osu.Game.Scoring; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Visual.UserInterface; using osuTK; @@ -86,6 +88,64 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } } + [Test] + public void TestRanks() + { + for (int i = -1; i <= 7; i++) + { + ScoreRank rank = (ScoreRank)i; + + AddStep($"display rank {rank}", () => + { + ContentContainer.Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine)) + }, + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new[] + { + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(0, $"{rank.GetDescription()}", rank)) + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(1, $"{rank.GetDescription()}", rank)), + KeyboardSelected = { Value = true }, + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(2, $"{rank.GetDescription()}", rank)), + Expanded = { Value = true }, + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(3, $"{rank.GetDescription()}", rank)), + Expanded = { Value = true }, + KeyboardSelected = { Value = true }, + }, + }, + } + } + }; + }); + } + } + protected override Drawable CreateContent() { return new OsuContextMenuContainer From 295adf9f28c9d9cc381fd0d2cc417d428b896e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Sep 2025 08:31:54 +0200 Subject: [PATCH 233/267] Add actual test coverage for relevant failure --- .../Extensions/NumberFormattingExtensionsTest.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs index b02bf01019..3a96459b73 100644 --- a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs +++ b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs @@ -46,9 +46,12 @@ namespace osu.Game.Tests.Extensions [Test] [SetCulture("fr-FR")] - public void TestCultureInsensitivity() + [TestCase(0.4, true, 2, ExpectedResult = "40%")] + [TestCase(1e-6, false, 6, ExpectedResult = "0.000001")] + [TestCase(0.48333, true, 4, ExpectedResult = "48.33%")] + public string TestCultureInsensitivity(double input, bool percent, int decimalDigits) { - Assert.That(0.4.ToStandardFormattedString(maxDecimalDigits: 2, asPercentage: true), Is.EqualTo("40%")); + return input.ToStandardFormattedString(decimalDigits, percent); } } } From d1cf248b9a253095d92e1640075b15592ac2e7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Sep 2025 08:33:18 +0200 Subject: [PATCH 234/267] Remove redundant double string invariance --- osu.Game/Extensions/NumberFormattingExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs index fe2ce37a0f..ff35dbc2a0 100644 --- a/osu.Game/Extensions/NumberFormattingExtensions.cs +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -36,7 +36,7 @@ namespace osu.Game.Extensions string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; - return FormattableString.Invariant($"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}", CultureInfo.InvariantCulture)}"); + return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}", CultureInfo.InvariantCulture)}"; } /// From 70f683e7fa4a5a59aa15ffe8dea435e4eebdb14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Sep 2025 08:38:06 +0200 Subject: [PATCH 235/267] Use slightly nicer way of invarianting rate adjust setting value --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 1950f8b66e..49bdd93bc6 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Localisation; @@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.Mods get { if (!SpeedChange.IsDefault) - yield return ("Speed change", $"{SpeedChange.Value.ToString("N2", CultureInfo.InvariantCulture)}x"); + yield return ("Speed change", FormattableString.Invariant($@"{SpeedChange.Value:N2}x")); } } From 9dc79e6f0d9924e42b0bc2c420e2abea0ec9bc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Sep 2025 09:31:50 +0200 Subject: [PATCH 236/267] Avoid passing the same thing three times --- .../Visual/SongSelectV2/TestScenePanelGroup.cs | 9 ++++----- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 3 ++- .../Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs index e6a58136fa..f678ec372a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -3,7 +3,6 @@ using System; using NUnit.Framework; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -120,21 +119,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelGroupRankDisplay { - Item = new CarouselItem(new RankDisplayGroupDefinition(0, $"{rank.GetDescription()}", rank)) + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)) }, new PanelGroupRankDisplay { - Item = new CarouselItem(new RankDisplayGroupDefinition(1, $"{rank.GetDescription()}", rank)), + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), KeyboardSelected = { Value = true }, }, new PanelGroupRankDisplay { - Item = new CarouselItem(new RankDisplayGroupDefinition(2, $"{rank.GetDescription()}", rank)), + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), Expanded = { Value = true }, }, new PanelGroupRankDisplay { - Item = new CarouselItem(new RankDisplayGroupDefinition(3, $"{rank.GetDescription()}", rank)), + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), Expanded = { Value = true }, KeyboardSelected = { Value = true }, }, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 679fec76f2..135187dc08 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -1093,7 +1094,7 @@ namespace osu.Game.Screens.SelectV2 /// /// Defines a grouping header for a set of carousel items grouped by achieved rank. /// - public record RankDisplayGroupDefinition(int Order, string Title, ScoreRank Rank) : GroupDefinition(Order, Title); + public record RankDisplayGroupDefinition(ScoreRank Rank) : GroupDefinition(-(int)Rank, Rank.GetDescription()); /// /// Used to represent a portion of a under a . diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index f2159d63f5..37ea7b7497 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -433,7 +433,7 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary topRankMapping) { if (topRankMapping.TryGetValue(beatmap.ID, out var rank)) - return new RankDisplayGroupDefinition(-(int)rank, rank.GetDescription(), rank).Yield(); + return new RankDisplayGroupDefinition(rank).Yield(); return new GroupDefinition(int.MaxValue, "Unplayed").Yield(); } From fd412618dba7399b1778b0902b73ffbae21a2399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Sep 2025 09:34:40 +0200 Subject: [PATCH 237/267] Adjust initial pool size for group rank displays 11 is excessive. There can ever be at most 9 of these panels, ever, because there are at most 9 possible letter grades at this time (F, D, C, B, A, S, S+, SS, SS+). --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 135187dc08..d2b18b2f33 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -789,7 +789,7 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool setPanelPool = new DrawablePool(100); private readonly DrawablePool groupPanelPool = new DrawablePool(100); private readonly DrawablePool starsGroupPanelPool = new DrawablePool(11); - private readonly DrawablePool ranksGroupPanelPool = new DrawablePool(11); + private readonly DrawablePool ranksGroupPanelPool = new DrawablePool(9); private void setupPools() { From 47b6b70cae56767ddcec0b7aab2dd58a64e91a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Sep 2025 09:44:05 +0200 Subject: [PATCH 238/267] Avoid duplicating rank name colour constants --- osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs b/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs index c174ccaf97..95e8b5f43b 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Scoring; using osuTK; @@ -158,18 +159,18 @@ namespace osu.Game.Screens.SelectV2 { case ScoreRank.SH: case ScoreRank.XH: - starRatingText.Colour = ColourInfo.GradientVertical(Color4.White, Color4Extensions.FromHex("afdff0")); + starRatingText.Colour = DrawableRank.GetRankNameColour(rank); iconContainer.Colour = colourProvider.Background5; break; case ScoreRank.X: case ScoreRank.S: - starRatingText.Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex(@"ffe7a8"), Color4Extensions.FromHex(@"ffb800")); + starRatingText.Colour = DrawableRank.GetRankNameColour(rank); iconContainer.Colour = colourProvider.Background5; break; case ScoreRank.F: - starRatingText.Colour = Color4Extensions.FromHex(@"CC3333"); + starRatingText.Colour = DrawableRank.GetRankNameColour(rank); iconContainer.Colour = colourProvider.Content1; break; From 82beeec730093dfec8c0ae358a63e4d71eb56680 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Sep 2025 16:55:54 +0900 Subject: [PATCH 239/267] Add failing test --- .../Matchmaking/TestSceneUserPanelOverlay.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs index 9ed233a507..28d45d5f38 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; @@ -102,6 +103,26 @@ namespace osu.Game.Tests.Visual.Matchmaking }, 8); } + [Test] + public void RemovePanels() + { + AddStep("join another user", () => + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(1) + { + User = new APIUser + { + Username = "User 1" + } + }); + }); + + AddUntilStep("two panels displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); + + AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); + AddUntilStep("one panel displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + } + [Test] public void ChangeRankings() { From 573d639238a69ae193f322afe961bd7b2a3f9ea8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Sep 2025 16:56:48 +0900 Subject: [PATCH 240/267] Fix nullref when users leave quick-play rooms --- .../Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs index 9ddddda710..a938dadae0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs @@ -277,7 +277,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { base.Update(); - if (panel == null) + if (panel?.Parent == null) return; Size = panel.Horizontal ? MatchmakingUserPanel.SIZE_HORIZONTAL : MatchmakingUserPanel.SIZE_VERTICAL; @@ -299,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match isAnimating &= !Precision.AlmostEquals(panel.Position, targetPos, 0.5f); Vector2 getFinalPosition() - => panel.Parent!.ToLocalSpace(ScreenSpaceDrawQuad.Centre) - panel.AnchorPosition; + => panel.Parent.ToLocalSpace(ScreenSpaceDrawQuad.Centre) - panel.AnchorPosition; } } } From 3f5b71fdc3859dd1d415c9da75c2552cc47e0fd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Sep 2025 17:31:45 +0900 Subject: [PATCH 241/267] Always explicitly assign the action for beatmap cards --- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 36 ++++++++++--------- .../Drawables/Cards/BeatmapCardExtra.cs | 2 ++ .../Drawables/Cards/BeatmapCardNano.cs | 2 ++ .../Drawables/Cards/BeatmapCardNormal.cs | 2 ++ 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 4c0466fa04..8cd0ac965a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -77,25 +77,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards containingInputManager = GetContainingInputManager(); - Action ??= () => - { - if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true) - { - switch (DownloadTracker.State.Value) - { - case DownloadState.NotDownloaded: - if (!BeatmapSet.Availability.DownloadDisabled) - beatmaps?.Download(BeatmapSet, preferNoVideo.Value); - break; + if (Action == null) + throw new InvalidOperationException($"An action should be assigned to this {nameof(BeatmapCard)}. To use the default, assign {nameof(DefaultAction)}."); + } - case DownloadState.LocallyAvailable: - game?.PresentBeatmap(BeatmapSet); - break; - } + protected void DefaultAction() + { + if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true) + { + switch (DownloadTracker.State.Value) + { + case DownloadState.NotDownloaded: + if (!BeatmapSet.Availability.DownloadDisabled) beatmaps?.Download(BeatmapSet, preferNoVideo.Value); + break; + + case DownloadState.LocallyAvailable: + game?.PresentBeatmap(BeatmapSet); + break; } - else - beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); - }; + } + else + beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 9428984115..75fdc7d7e8 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -46,6 +46,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, allowExpansion) { content = new BeatmapCardContent(height); + + Action = DefaultAction; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs index 62108fe6f5..c23a03aabe 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -54,6 +54,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, false) { content = new BeatmapCardContent(height); + + Action = DefaultAction; } [BackgroundDependencyLoader] diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 505a6fcdae..ac9ee94f56 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -47,6 +47,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, allowExpansion) { content = new BeatmapCardContent(HEIGHT); + + Action = DefaultAction; } [BackgroundDependencyLoader] From 40cbe58220d434c4fe3c099b607af5076002a2c4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 29 Sep 2025 14:23:19 +0200 Subject: [PATCH 242/267] Revert inline method for code abstraction --- .../Components/PathControlPointVisualiser.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index b6b1185816..99002c2ef4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -440,26 +440,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Vector2 oldPosition = hitObject.Position; double oldStartTime = hitObject.StartTime; - SnapResult snapControlPoint(Vector2 newScreenSpacePosition, bool trySnapToDistanceGrid) - { - var result = positionSnapProvider?.TrySnapToNearbyObjects(newScreenSpacePosition, oldStartTime); - if (trySnapToDistanceGrid) - result ??= positionSnapProvider?.TrySnapToDistanceGrid(newScreenSpacePosition, limitedDistanceSnap.Value ? oldStartTime : null); - if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newScreenSpacePosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) - result = gridSnapResult; - result ??= new SnapResult(newScreenSpacePosition, oldStartTime); - return result; - } - if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0])) { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - var snapResult = snapControlPoint(newHeadPosition, true); - Vector2 movementDelta = Parent!.ToLocalSpace(snapResult.ScreenSpacePosition) - hitObject.Position; + + var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newHeadPosition, oldStartTime); + + Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position; hitObject.Position += movementDelta; - hitObject.StartTime = snapResult.Time ?? hitObject.StartTime; + hitObject.StartTime = result.Time ?? hitObject.StartTime; for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { @@ -475,8 +470,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components else { Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition); - var snapResult = snapControlPoint(newControlPointPosition, false); - Vector2 movementDelta = Parent!.ToLocalSpace(snapResult.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; + + var result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newControlPointPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newControlPointPosition, oldStartTime); + + Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; for (int i = 0; i < controlPoints.Count; ++i) { From d76dce76ec5a7d7329ec7f98d73464fd8f708952 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 29 Sep 2025 14:44:12 +0200 Subject: [PATCH 243/267] dont snap inherited bspline type control points to nearby objects --- .../Sliders/Components/PathControlPointVisualiser.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 99002c2ef4..bff6701826 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -471,7 +471,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition); - var result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime); + // Snapping inherited B-spline control points to nearby objects would be unintuitive, because snapping them does not equate to snapping the interpolated slider path. + bool shouldSnapToNearbyObjects = dragPathTypes[draggedControlPointIndex] is not null || + dragPathTypes[..draggedControlPointIndex].LastOrDefault(t => t is not null)?.Type != SplineType.BSpline; + + SnapResult result = null; + if (shouldSnapToNearbyObjects) + result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime); if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newControlPointPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(newControlPointPosition, oldStartTime); From 18549ea7dccd8e525fe2f0752fc7c647c1ffad1a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 29 Sep 2025 15:07:03 +0200 Subject: [PATCH 244/267] Remove all linq calls from getScreenSpaceControlPointNodes --- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index a7016bdae0..0df657a9a0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -637,12 +637,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (DrawableObject.SliderBody == null) yield break; - PathType? currentPathType = DrawableObject.HitObject.Path.ControlPoints.FirstOrDefault()?.Type; + PathType? currentPathType = null; - // Skip the first control point because it is already covered by the slider head // Skip the last control point because its always either not on the slider path or exactly on the slider end - foreach (var controlPoint in DrawableObject.HitObject.Path.ControlPoints.Skip(1).SkipLast(1)) + for (int i = 0; i < DrawableObject.HitObject.Path.ControlPoints.Count - 1; i++) { + var controlPoint = DrawableObject.HitObject.Path.ControlPoints[i]; + + // Skip the first control point because it is already covered by the slider head + if (i == 0) + { + currentPathType = controlPoint.Type; + continue; + } + if (controlPoint.Type is null && currentPathType != PathType.LINEAR) continue; From 2c39e1e9dbcae17cad677f0a00e28b72df4f5e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Sep 2025 15:30:27 +0200 Subject: [PATCH 245/267] Ensure submission progress sample is stopped when transitioning into a final state Probably closes https://github.com/ppy/osu/issues/35138. I'm not sure. I only got the issue to reproduce once, on dev, using a very large archive that was uploading really slowly, and then never again. The working theory is that basically handling of `progressSampleChannel` is quite dodgy and it could possibly, in circumstances unknown, be allowed to play forevermore after transitioning to failed / canceled state. Success state does not get this treatment because it has special logic to set progress to 1. --- .../TestSceneSubmissionStageProgress.cs | 19 +++++++++++++++++++ .../Submission/SubmissionStageProgress.cs | 2 ++ 2 files changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs index ee22cbda71..2dc9077a14 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs @@ -68,6 +68,25 @@ namespace osu.Game.Tests.Visual.Editing progress.SetInProgress(incrementingProgress += RNG.NextSingle(0.08f)); }, 0, true); }); + AddStep("increase progress slowly then fail", () => + { + incrementingProgress = 0; + + ScheduledDelegate? task = null; + + task = Scheduler.AddDelayed(() => + { + if (incrementingProgress >= 1) + { + progress.SetFailed("nope"); + // ReSharper disable once AccessToModifiedClosure + task?.Cancel(); + return; + } + + progress.SetInProgress(incrementingProgress += RNG.NextSingle(0.001f)); + }, 0, true); + }); AddUntilStep("wait for completed", () => incrementingProgress >= 1); AddStep("completed", () => progress.SetCompleted()); diff --git a/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs index 8af4e3fe52..e7f8ff933d 100644 --- a/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs +++ b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs @@ -263,6 +263,7 @@ namespace osu.Game.Screens.Edit.Submission iconContainer.Colour = colours.Red1; iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); errorSample?.Play(); + progressSampleChannel?.Stop(); break; case StageStatusType.Canceled: @@ -274,6 +275,7 @@ namespace osu.Game.Screens.Edit.Submission iconContainer.Colour = colours.Gray8; iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); cancelSample?.Play(); + progressSampleChannel?.Stop(); break; } } From 41698d58483a03a82136f730cef438b4d46c8201 Mon Sep 17 00:00:00 2001 From: AeroKoder Date: Mon, 29 Sep 2025 09:27:01 -0700 Subject: [PATCH 246/267] Updated `moveSelectionInBounds` in `OsuSelectionScaleHandler` to match the one in `OsuSelectionHandler` --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 3072e5d11b..d5f3137769 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void moveSelectionInBounds() { - Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys); + Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys, true); Vector2 delta = Vector2.Zero; From d31df5bbc767e5d63c7c7eb917454a833b87de3b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Sep 2025 12:02:30 +0900 Subject: [PATCH 247/267] Make quick play chat not hold focus --- .../Match/MatchmakingChatDisplay.cs | 70 +++++++++++++++++++ .../Matchmaking/Match/ScreenMatchmaking.cs | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs new file mode 100644 index 0000000000..4ff6a3cdf6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs @@ -0,0 +1,70 @@ +// 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.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osu.Game.Online.Rooms; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.OnlinePlay.Match.Components; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class MatchmakingChatDisplay : MatchChatDisplay, IKeyBindingHandler + { + protected new ChatTextBox TextBox => base.TextBox!; + + public MatchmakingChatDisplay(Room room, bool leaveChannelOnDispose = true) + : base(room, leaveChannelOnDispose) + { + } + + [BackgroundDependencyLoader] + private void load(RealmKeyBindingStore keyBindingStore) + { + resetPlaceholderText(); + + TextBox.HoldFocus = false; + TextBox.ReleaseFocusOnCommit = true; + TextBox.Focus = () => TextBox.PlaceholderText = ChatStrings.InputPlaceholder; + TextBox.FocusLost = resetPlaceholderText; + + void resetPlaceholderText() => TextBox.PlaceholderText = Localisation.ChatStrings.InGameInputPlaceholder(keyBindingStore.GetBindingsStringFor(GlobalAction.ToggleChatFocus)); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Back: + if (TextBox.HasFocus) + { + Schedule(() => TextBox.KillFocus()); + return true; + } + + break; + + case GlobalAction.ToggleChatFocus: + if (TextBox.HasFocus) + { + Schedule(() => TextBox.KillFocus()); + } + else + { + Schedule(() => TextBox.TakeFocus()); + } + + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index dbe958dcac..4ad0a0bb5f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -144,7 +144,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Width = 700, Height = 130, Padding = new MarginPadding { Bottom = row_padding }, - Child = chat = new MatchChatDisplay(new Room(room)) + Child = chat = new MatchmakingChatDisplay(new Room(room)) { RelativeSizeAxes = Axes.Both, } From 057406c910893a310223a2d3cdb7aaf7cda5b702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Sep 2025 08:32:53 +0200 Subject: [PATCH 248/267] Simplify logic further --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 0df657a9a0..b46dd44ce5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -644,19 +644,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { var controlPoint = DrawableObject.HitObject.Path.ControlPoints[i]; + if (controlPoint.Type is not null) + currentPathType = controlPoint.Type; + // Skip the first control point because it is already covered by the slider head if (i == 0) - { - currentPathType = controlPoint.Type; continue; - } if (controlPoint.Type is null && currentPathType != PathType.LINEAR) continue; - if (controlPoint.Type is not null) - currentPathType = controlPoint.Type; - var screenSpacePosition = DrawableObject.SliderBody.ToScreenSpace(DrawableObject.SliderBody.PathOffset + controlPoint.Position); yield return screenSpacePosition; } From 256483165f9538a25d66d6e7a4c88e4f08cbbca7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Sep 2025 18:10:34 +0900 Subject: [PATCH 249/267] Update framework --- 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 498d6f267e..64bdd985f6 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 4ce5be23bc..d945420306 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 3ebb72a20af1cf0ad65b7ca26beae54cc4844863 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Sep 2025 15:54:20 +0900 Subject: [PATCH 250/267] Add avatar action request/event models --- .../Events/MatchmakingAvatarAction.cs | 12 ++++++++ .../Events/MatchmakingAvatarActionEvent.cs | 29 +++++++++++++++++++ .../Events/MatchmakingAvatarActionRequest.cs | 23 +++++++++++++++ .../Online/Multiplayer/MatchServerEvent.cs | 2 ++ .../Online/Multiplayer/MatchUserRequest.cs | 2 ++ osu.Game/Online/SignalRWorkaroundTypes.cs | 5 +++- 6 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs create mode 100644 osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionEvent.cs create mode 100644 osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionRequest.cs diff --git a/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs new file mode 100644 index 0000000000..84ccff2587 --- /dev/null +++ b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Matchmaking.Events +{ + /// + /// An action performed on a user's avatar in a matchmaking room. + /// + public enum MatchmakingAvatarAction + { + } +} diff --git a/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionEvent.cs b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionEvent.cs new file mode 100644 index 0000000000..187d234855 --- /dev/null +++ b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionEvent.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 System; +using MessagePack; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Online.Matchmaking.Events +{ + /// + /// An action performed by a user in a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingAvatarActionEvent : MatchServerEvent + { + /// + /// The user performing the action. + /// + [Key(0)] + public int UserId { get; set; } + + /// + /// The action. + /// + [Key(1)] + public MatchmakingAvatarAction Action { get; set; } + } +} diff --git a/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionRequest.cs b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionRequest.cs new file mode 100644 index 0000000000..abee95f0e2 --- /dev/null +++ b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionRequest.cs @@ -0,0 +1,23 @@ +// 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 MessagePack; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Online.Matchmaking.Events +{ + /// + /// Requests to perform an action on a user's avatar in a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingAvatarActionRequest : MatchUserRequest + { + /// + /// The action. + /// + [Key(0)] + public MatchmakingAvatarAction Action { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MatchServerEvent.cs b/osu.Game/Online/Multiplayer/MatchServerEvent.cs index 376ff4d261..529a299438 100644 --- a/osu.Game/Online/Multiplayer/MatchServerEvent.cs +++ b/osu.Game/Online/Multiplayer/MatchServerEvent.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer.Countdown; namespace osu.Game.Online.Multiplayer @@ -15,6 +16,7 @@ namespace osu.Game.Online.Multiplayer // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. [Union(0, typeof(CountdownStartedEvent))] [Union(1, typeof(CountdownStoppedEvent))] + [Union(2, typeof(MatchmakingAvatarActionEvent))] public abstract class MatchServerEvent { } diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index 8515256581..02704ea161 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; @@ -17,6 +18,7 @@ namespace osu.Game.Online.Multiplayer [Union(0, typeof(ChangeTeamRequest))] [Union(1, typeof(StartMatchCountdownRequest))] [Union(2, typeof(StopCountdownRequest))] + [Union(3, typeof(MatchmakingAvatarActionRequest))] public abstract class MatchUserRequest { } diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 04d4b8d7af..e509891486 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Game.Online.Matchmaking; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; @@ -53,7 +54,9 @@ namespace osu.Game.Online (typeof(MatchmakingQueueStatus.MatchFound), typeof(MatchmakingQueueStatus)), (typeof(MatchmakingQueueStatus.JoiningMatch), typeof(MatchmakingQueueStatus)), (typeof(MatchmakingRoomState), typeof(MatchRoomState)), - (typeof(MatchmakingStageCountdown), typeof(MultiplayerCountdown)) + (typeof(MatchmakingStageCountdown), typeof(MultiplayerCountdown)), + (typeof(MatchmakingAvatarActionRequest), typeof(MatchUserRequest)), + (typeof(MatchmakingAvatarActionEvent), typeof(MatchServerEvent)), }; } } From e636a09e0fe49780c462671ffabdd5549fdcf42c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Sep 2025 16:01:58 +0900 Subject: [PATCH 251/267] Add ability to jump in quick play --- .../Matchmaking/TestScenePlayerPanel.cs | 7 +++ .../Events/MatchmakingAvatarAction.cs | 1 + .../Online/Multiplayer/MultiplayerClient.cs | 5 +- .../Matchmaking/Match/PlayerPanel.cs | 60 ++++++++++++++++--- .../Matchmaking/Match/ScreenMatchmaking.cs | 18 ++++++ .../Multiplayer/TestMultiplayerClient.cs | 16 ++++- 6 files changed, 96 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index 1ef5e2edc1..09c0f5fdbf 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; @@ -91,5 +92,11 @@ namespace osu.Game.Tests.Visual.Matchmaking } }).WaitSafely()); } + + [Test] + public void TestJump() + { + AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely()); + } } } diff --git a/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs index 84ccff2587..cab007327c 100644 --- a/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs +++ b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs @@ -8,5 +8,6 @@ namespace osu.Game.Online.Matchmaking.Events /// public enum MatchmakingAvatarAction { + Jump } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 09dd3a00ae..a58d433e7d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -117,6 +117,8 @@ namespace osu.Game.Online.Multiplayer public event Action? CountdownStopped; + public event Action? MatchEvent; + public event Action? UserStateChanged; public event Action? MatchmakingQueueJoined; @@ -704,7 +706,7 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - public Task MatchEvent(MatchServerEvent e) + Task IMultiplayerClient.MatchEvent(MatchServerEvent e) { handleRoomRequest(() => { @@ -737,6 +739,7 @@ namespace osu.Game.Online.Multiplayer break; } + MatchEvent?.Invoke(e); RoomUpdated?.Invoke(); }); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index f18a33c830..2f543d9950 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Users; @@ -24,6 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { public static readonly Vector2 SIZE_HORIZONTAL = new Vector2(250, 100); public static readonly Vector2 SIZE_VERTICAL = new Vector2(150, 200); + private static readonly Vector2 avatar_size = new Vector2(80); public readonly MultiplayerRoomUser RoomUser; @@ -36,6 +38,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private OsuSpriteText rankText = null!; private OsuSpriteText scoreText = null!; + private Drawable avatarPositionTarget = null!; + private Drawable avatarJumpTarget = null!; private MatchmakingAvatar avatar = null!; private OsuSpriteText username = null!; @@ -78,13 +82,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { - avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) + avatarPositionTarget = new Container { - Anchor = Anchor.TopLeft, Origin = Anchor.Centre, - Size = new Vector2(80), + Size = avatar_size, + Child = avatarJumpTarget = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Child = avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = Vector2.One + } + } }, rankText = new OsuSpriteText { @@ -123,6 +139,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match updateLayout(true); client.MatchRoomStateChanged += onRoomStateChanged; + client.MatchEvent += onMatchEvent; + onRoomStateChanged(client.Room!.MatchState); avatar.ScaleTo(0) @@ -155,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { double duration = instant ? 0 : 1000; - avatar.MoveTo(avatarPosition, duration, Easing.OutPow10); + avatarPositionTarget.MoveTo(avatarPosition, duration, Easing.OutPow10); this.ResizeTo(horizontal ? SIZE_HORIZONTAL : SIZE_VERTICAL, duration, Easing.OutPow10); rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10); @@ -176,16 +194,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match mainContent.ScaleTo(1, 750, Easing.OutPow10); mainContent.MoveTo(Vector2.Zero, 1250, Easing.OutPow10); - avatar.MoveTo(avatarPosition, 1250, Easing.OutPow10); + avatarPositionTarget.MoveTo(avatarPosition, 1250, Easing.OutPow10); base.OnHoverLost(e); } protected override bool OnMouseMove(MouseMoveEvent e) { - var offset = (avatar.ToLocalSpace(e.ScreenSpaceMousePosition) - avatar.DrawSize / 2) * 0.02f; + var offset = (avatarPositionTarget.ToLocalSpace(e.ScreenSpaceMousePosition) - avatarPositionTarget.DrawSize / 2) * 0.02f; mainContent.MoveTo(offset * 0.5f, 1000, Easing.OutPow10); - avatar.MoveTo(avatarPosition + offset, 400, Easing.OutPow10); + avatarPositionTarget.MoveTo(avatarPosition + offset, 400, Easing.OutPow10); return base.OnMouseMove(e); } @@ -201,12 +219,38 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match scoreText.Text = $"{userScore.Points} pts"; }); + private void onMatchEvent(MatchServerEvent e) + { + switch (e) + { + case MatchmakingAvatarActionEvent action: + if (action.UserId != RoomUser.UserID) + break; + + switch (action.Action) + { + case MatchmakingAvatarAction.Jump: + avatarJumpTarget.MoveToY(-10, 200, Easing.Out) + .Then().MoveToY(0, 200, Easing.In); + avatarJumpTarget.ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out) + .Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In) + .Then().ScaleTo(Vector2.One, 800, Easing.OutElastic); + break; + } + + break; + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (client.IsNotNull()) + { client.MatchRoomStateChanged -= onRoomStateChanged; + client.MatchEvent -= onMatchEvent; + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 4ad0a0bb5f..e4031c5e98 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -13,12 +13,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Cursor; using osu.Game.Online; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -28,6 +30,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Users; +using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { @@ -290,6 +293,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match })); } + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.Space: + if (e.Repeat) + return true; + + client.SendMatchRequest(new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).FireAndForget(); + return true; + } + + return false; + } + private bool exitConfirmed; public override bool OnExiting(ScreenExitEvent e) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 5a69c6fcba..bd16c36eec 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -15,6 +15,7 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Matchmaking; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; @@ -386,7 +387,10 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - public override async Task SendMatchRequest(MatchUserRequest request) + public override Task SendMatchRequest(MatchUserRequest request) + => SendUserMatchRequest(api.LocalUser.Value.OnlineID, request); + + public async Task SendUserMatchRequest(int userId, MatchUserRequest request) { request = clone(request); @@ -404,7 +408,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (targetTeam != null) { userState.TeamID = targetTeam.ID; - await ((IMultiplayerClient)this).MatchUserStateChanged(clone(LocalUser.UserID), clone(userState)).ConfigureAwait(false); + await ((IMultiplayerClient)this).MatchUserStateChanged(userId, clone(userState)).ConfigureAwait(false); } break; @@ -416,6 +420,14 @@ namespace osu.Game.Tests.Visual.Multiplayer case StopCountdownRequest stopCountdown: await StopCountdown(ServerRoom.ActiveCountdowns.First(c => c.ID == stopCountdown.ID)).ConfigureAwait(false); break; + + case MatchmakingAvatarActionRequest avatarAction: + await ((IMultiplayerClient)this).MatchEvent(new MatchmakingAvatarActionEvent + { + UserId = userId, + Action = avatarAction.Action + }).ConfigureAwait(false); + break; } } From a3e09a1c31f5158e615fbacfa43de5c7c868693e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Sep 2025 11:32:15 +0200 Subject: [PATCH 252/267] Fix song select carousel sometimes teleporting on beatmap set deletion Closes https://github.com/ppy/osu/issues/35010. The issue here does not reproduce consistently, and is more or less random in presentation. That said, using a large enough realm database more or less ensures that the issue will present itself (in testing on a large realm db, the failure rate is around ~50%). This actually regressed in https://github.com/ppy/osu/pull/34842. The core failure in this case is here: https://github.com/ppy/osu/blob/fd412618dba7399b1778b0902b73ffbae21a2399/osu.Game/Screens/SelectV2/BeatmapCarousel.cs#L161 The `CheckModelEquality()` call above is comparing two `BeatmapInfo`s, but a84c364e44d1e1f89c209da4f29e0ab524b3e2ab changed the `BeatmapInfo`-comparing path of `CheckModelEquality()` to use `GroupedBeatmap` instead. Due to this, `CheckModelEquality()` falls back to reference equality comparison for `BeatmapInfo`s. When that reference comparison fails, the carousel stops detecting that the current selection was deleted from under it correctly, and therefore the proximity-based selection logic never runs. Due to the human-obvious mechanism of failure and relatively easy manual reproduction I've decided not to try and add tests for this, as they are likely to take a long time to write due to the mechanism of failure being incorrect use of reference equality specifically. That said, I can try on request. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d2b18b2f33..a5e187fed2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -816,6 +816,11 @@ namespace osu.Game.Screens.SelectV2 if (x is GroupedBeatmap groupedBeatmapX && y is GroupedBeatmap groupedBeatmapY) return groupedBeatmapX.Equals(groupedBeatmapY); + // `BeatmapInfo` is no longer used directly in carousel items, but in rare circumstances still is used for model equality comparisons + // (see `beatmapSetsChanged()` deletion handling logic, which aims to find a beatmap close to the just-deleted one, disregarding grouping concerns) + if (x is BeatmapInfo beatmapInfoX && y is BeatmapInfo beatmapInfoY) + return beatmapInfoX.Equals(beatmapInfoY); + if (x is GroupDefinition groupX && y is GroupDefinition groupY) return groupX.Equals(groupY); From 6e39e714e1892ab7852539aa8963e2887e0f9ecb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Sep 2025 19:22:16 +0900 Subject: [PATCH 253/267] Refactor to simplify sample handling --- .../Containers/OsuClickableContainer.cs | 12 +++- .../Matchmaking/Queue/PoolSelector.cs | 63 +++++++------------ .../Matchmaking/Queue/ScreenQueue.cs | 16 ++--- 3 files changed, 40 insertions(+), 51 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index fceee90d06..dbc354ae07 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -18,6 +18,8 @@ namespace osu.Game.Graphics.Containers private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; + private HoverSounds samples = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => // base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation). base.ReceivePositionalInputAt(screenSpacePos) @@ -33,6 +35,14 @@ namespace osu.Game.Graphics.Containers this.sampleSet = sampleSet; } + public void TriggerClickWithSound() + { + TriggerClick(); + + // TriggerClick doesn't recursively fire the event so we need to manually do this. + (samples as HoverClickSounds)?.PlayClickSample(); + } + public virtual LocalisableString TooltipText { get; set; } [BackgroundDependencyLoader] @@ -46,7 +56,7 @@ namespace osu.Game.Graphics.Containers AddRangeInternal(new Drawable[] { - CreateHoverSounds(sampleSet), + samples = CreateHoverSounds(sampleSet), content, }); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs index d71390cfa8..71f976329f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs @@ -1,7 +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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -28,7 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue public readonly Bindable SelectedPool = new Bindable(); private FillFlowContainer poolFlow = null!; - private HoverClickSounds clickSounds = null!; public PoolSelector() { @@ -38,20 +37,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + InternalChild = poolFlow = new FillFlowContainer { - poolFlow = new FillFlowContainer - { - AutoSizeAxes = Axes.X, - Height = icon_size * 1.2f, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - }, - clickSounds = new HoverClickSounds(HoverSampleSet.TabSelect) - { - // Click samples are played manually - Alpha = 0 - } + AutoSizeAxes = Axes.X, + Height = icon_size * 1.2f, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), }; } @@ -77,37 +68,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Key != Key.Left && e.Key != Key.Right) - return false; - - clickSounds.PlayClickSample(); - - if (SelectedPool.Value == null) - { - SelectedPool.Value = AvailablePools.Value[0]; - return true; - } - - int currentPoolIndex = Array.IndexOf(AvailablePools.Value, SelectedPool.Value); + var currentSelection = poolFlow.SingleOrDefault(b => b.IsSelected); switch (e.Key) { case Key.Left: - SelectedPool.Value = currentPoolIndex == 0 - ? AvailablePools.Value[^1] - : AvailablePools.Value[(currentPoolIndex - 1) % AvailablePools.Value.Length]; - break; + { + var next = poolFlow.Reverse().SkipWhile(b => b != currentSelection).Skip(1).FirstOrDefault(); + (next ?? poolFlow.Last()).TriggerClickWithSound(); + return true; + } case Key.Right: - SelectedPool.Value = AvailablePools.Value[(currentPoolIndex + 1) % AvailablePools.Value.Length]; - break; + { + var next = poolFlow.SkipWhile(b => b != currentSelection).Skip(1).FirstOrDefault(); + (next ?? poolFlow.First()).TriggerClickWithSound(); + return true; + } } - return true; + return false; } private partial class SelectorButton : OsuAnimatedButton { + public bool IsSelected => SelectedPool.Value?.Equals(pool) == true; + public readonly Bindable SelectedPool = new Bindable(); [Resolved] @@ -119,6 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue private Box flashLayer = null!; public SelectorButton(MatchmakingPool pool) + : base(HoverSampleSet.ButtonSidebar) { this.pool = pool; @@ -171,23 +158,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue protected override bool OnHover(HoverEvent e) { - if (!isSelected) + if (!IsSelected) flashLayer.FadeTo(0.05f, 200, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - if (!isSelected) + if (!IsSelected) flashLayer.FadeTo(0f, 200, Easing.OutQuint); base.OnHoverLost(e); } - private bool isSelected => SelectedPool.Value?.Equals(pool) == true; - private void onSelectionChanged(ValueChangedEvent selection) { - if (isSelected) + if (IsSelected) { this.ScaleTo(1.2f, 200, Easing.OutQuint); iconSprite.FadeColour(Color4.Gold, 100, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs index 7f291f27c0..501a46d4c4 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs @@ -462,8 +462,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue private partial class SelectionButton : ShearedButton, IKeyBindingHandler { - private HoverClickSounds clickSounds = null!; - public SelectionButton(float? width = null, float height = DEFAULT_HEIGHT) : base(width, height) { @@ -471,22 +469,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue public bool OnPressed(KeyBindingPressEvent e) { - if (e.Action != GlobalAction.Select) - return false; - - if (e.Repeat) + if (e.Action == GlobalAction.Select && !e.Repeat) + { + TriggerClickWithSound(); return true; + } - clickSounds.PlayClickSample(); - Action(); - return true; + return false; } public void OnReleased(KeyBindingReleaseEvent e) { } - - protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => clickSounds = (HoverClickSounds)base.CreateHoverSounds(sampleSet); } } } From 2edd49d2c041b2ba6fd759dd1f18dff501500263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Sep 2025 12:57:10 +0200 Subject: [PATCH 254/267] Add half-height-of-selected-panel adjustment to carousel scroll target Intended to address https://github.com/ppy/osu/issues/35147, maybe? The old carousel would target the vertical center of the active panel when scrolling: https://github.com/ppy/osu/blob/b9e1b6969e78dfa798bb4afed8afae55e9e4adb1/osu.Game/Screens/Select/BeatmapCarousel.cs#L948 This was not in place in the new carousel, weirdly, which was targeting the top-left corner of the selected panel. --- osu.Game/Graphics/Carousel/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 0df183bb71..8f001007b9 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -761,10 +761,10 @@ namespace osu.Game.Graphics.Carousel updateItemYPosition(item, ref lastVisible, ref yPos); if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!)) - currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition, i); + currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition + item.DrawHeight / 2, i); if (CheckModelEquality(item.Model, currentSelection.Model!)) - currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition, i); + currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition + item.DrawHeight / 2, i); } // Update the total height of all items (to make the scroll container scrollable through the full height even though From ac2df49c35431eceeb23feb794fb25aa05538683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Sep 2025 13:19:03 +0200 Subject: [PATCH 255/267] Demonstrate failure in test --- ...eneSongSelectCurrentSelectionInvalidated.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs index 7c604eb37b..0ec61f59da 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs @@ -55,22 +55,26 @@ namespace osu.Game.Tests.Visual.SongSelectV2 waitForFiltering(6); BeatmapInfo? initiallySelected = null; - AddAssert("selected is taiko", () => (initiallySelected = selectedBeatmap)?.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddAssert("carousel beatmap is taiko", () => (initiallySelected = selectedBeatmap)?.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddUntilStep("global beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1)); ChangeRuleset(0); waitForFiltering(7); - AddAssert("selected is osu", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(0)); - AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); + AddAssert("carousel beatmap is osu", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(0)); + AddUntilStep("global beatmap is osu", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(0)); + AddAssert("carousel beatmap is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); ChangeRuleset(1); waitForFiltering(8); - AddAssert("selected is taiko", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(1)); - AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); + AddAssert("carousel beatmap is taiko", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddUntilStep("global beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddAssert("carousel beatmap is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); ChangeRuleset(2); waitForFiltering(9); - AddAssert("selected is catch", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(2)); - AddAssert("selected is different set", () => selectedBeatmap?.BeatmapSet, () => Is.Not.EqualTo(initiallySelected!.BeatmapSet)); + AddAssert("carousel beatmap is catch", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(2)); + AddUntilStep("global beatmap is catch", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(2)); + AddAssert("carousel beatmap is different set", () => selectedBeatmap?.BeatmapSet, () => Is.Not.EqualTo(initiallySelected!.BeatmapSet)); } /// From f66c8d10e0dd5606880243811d91c9ff57acdcbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Sep 2025 13:22:45 +0200 Subject: [PATCH 256/267] Fix song select not changing global beatmap correctly when switching rulesets Closes https://github.com/ppy/osu/issues/35113. Regressed in dfed564bda7408ec4fd1c82ab15f90f410cdd444 - setting `Carousel.CurrentSelection` was not all that `requestRecommendedSelection()` was doing there... A potential point of discussion is whether this global beatmap switch should be debounced or instant. I'm not sure I have a particularly well-formed opinion on that. One argument in favour of not debouncing is that if you look closely at the left side of the screen while the debounce is in progress, you can still sort of see the broken behaviour happen - it just doesn't stay there forever. Thankfully `ensureGlobalBeatmapValid()` being called in every scenario on screen suspension prevented this bug from being any worse than it is right now. --- osu.Game/Screens/SelectV2/SongSelect.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 9947ffc6bc..f5588bcda4 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -600,7 +600,9 @@ namespace osu.Game.Screens.SelectV2 if (validBeatmaps.Any()) { - carousel.CurrentBeatmap = difficultyRecommender?.GetRecommendedBeatmap(validBeatmaps) ?? validBeatmaps.First(); + var beatmap = difficultyRecommender?.GetRecommendedBeatmap(validBeatmaps) ?? validBeatmaps.First(); + carousel.CurrentBeatmap = beatmap; + debounceQueueSelection(beatmap); return true; } } From 33ddb84633b42aea7e7581dae018508d75acaed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Sep 2025 13:58:10 +0200 Subject: [PATCH 257/267] Forcibly refetch online beatmap content on re-entering song select Closes https://github.com/ppy/osu/issues/34546. --- osu.Game/Screens/SelectV2/SongSelect.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 9947ffc6bc..9a69b9f0b8 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -715,7 +715,7 @@ namespace osu.Game.Screens.SelectV2 ensurePlayingSelected(); updateBackgroundDim(); - fetchOnlineInfo(); + fetchOnlineInfo(force: true); } private void onLeavingScreen() @@ -1056,11 +1056,11 @@ namespace osu.Game.Screens.SelectV2 private CancellationTokenSource? onlineLookupCancellation; private Task? currentOnlineLookup; - private void fetchOnlineInfo() + private void fetchOnlineInfo(bool force = false) { var beatmapSetInfo = Beatmap.Value.BeatmapSetInfo; - if (lastLookupResult.Value?.Result?.OnlineID == beatmapSetInfo.OnlineID) + if (lastLookupResult.Value?.Result?.OnlineID == beatmapSetInfo.OnlineID && !force) return; onlineLookupCancellation?.Cancel(); From 4b2f3efcbd9c203ce5f3c1aefe3664ca0976e73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Sep 2025 15:16:58 +0200 Subject: [PATCH 258/267] Add tests covering desired UX --- .../TestSceneBeatmapCarouselArtistGrouping.cs | 34 +++++++++++++++++++ ...tSceneBeatmapCarouselDifficultyGrouping.cs | 26 ++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index c34077889d..1178f89da6 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -249,5 +249,39 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckDisplayedBeatmapSetsCount(10); CheckDisplayedBeatmapsCount(30); } + + [Test] + public void TestGroupDoesNotExpandAgainOnRefilterIfManuallyCollapsed() + { + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + + CheckDisplayedGroupsCount(1); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); + + CheckHasSelection(); + + ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); + + CheckDisplayedGroupsCount(5); + CheckDisplayedBeatmapSetsCount(10); + CheckDisplayedBeatmapsCount(30); + + ToggleGroupCollapse(); + + ApplyToFilterAndWaitForFilter("apply no-op filter", c => c.AllowConvertedBeatmaps = !c.AllowConvertedBeatmaps); + AddAssert("group didn't re-expand", () => Carousel.ExpandedGroup, () => Is.Null); + + ToggleGroupCollapse(); + AddAssert("beatmap set re-expanded correctly", () => Carousel.ExpandedBeatmapSet?.BeatmapSet, () => Is.EqualTo(BeatmapSets[2])); + + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[1].Metadata.Title); + + CheckDisplayedGroupsCount(1); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); + + CheckHasSelection(); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 58ecfcbf3b..2cffe60ec1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -337,5 +337,31 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("expanded group is still first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); } + + [Test] + public void TestExpandedGroupDoesNotExpandAgainOnRefilterIfManuallyCollapsed() + { + SelectPrevSet(); + + WaitForBeatmapSelection(2, 9); + AddAssert("expanded group is last", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(6)); + + SelectNextPanel(); + Select(); + + WaitForBeatmapSelection(2, 9); + AddAssert("expanded group is first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); + + ToggleGroupCollapse(); + + // doesn't actually filter anything away, but triggers a filter. + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = "Some"); + AddAssert("group didn't re-expand", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.Null); + + ToggleGroupCollapse(); + + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = "Som"); + AddAssert("expanded group is first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); + } } } From bc4d5d07d7b70a793c2d3fc32c3cef3f645d3676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Sep 2025 15:17:03 +0200 Subject: [PATCH 259/267] Do not forcibly re-expand carousel groups on refilters if the user manually collapsed them RFC. Closes https://github.com/ppy/osu/issues/35091. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d2b18b2f33..490d5095ec 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -343,6 +343,12 @@ namespace osu.Game.Screens.SelectV2 } } + /// + /// Tracks whether the user has manually requested to collapse an open group. + /// In this case, refilters should not forcibly expand groups until the user expands a group again themselves. + /// + private bool userCollapsedGroup; + protected override void HandleItemActivated(CarouselItem item) { try @@ -355,11 +361,19 @@ namespace osu.Game.Screens.SelectV2 { setExpansionStateOfGroup(ExpandedGroup, false); ExpandedGroup = null; + userCollapsedGroup = true; return; } setExpandedGroup(group); + if (userCollapsedGroup) + { + if (grouping.BeatmapSetsGroupedTogether && CurrentGroupedBeatmap != null) + setExpandedSet(new GroupedBeatmapSet(CurrentGroupedBeatmap.Group, CurrentGroupedBeatmap.Beatmap.BeatmapSet!)); + userCollapsedGroup = false; + } + // If the active selection is within this group, it should get keyboard focus immediately. if (CurrentSelectionItem?.IsVisible == true && CurrentSelection is GroupedBeatmap gb) RequestSelection(gb); @@ -398,6 +412,9 @@ namespace osu.Game.Screens.SelectV2 throw new InvalidOperationException("Groups should never become selected"); case GroupedBeatmap groupedBeatmap: + if (userCollapsedGroup) + break; + setExpandedGroup(groupedBeatmap.Group); if (grouping.BeatmapSetsGroupedTogether) From 114e7f5c612fd1da0baa527bf1b0c4379426db87 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 30 Sep 2025 10:35:38 -0700 Subject: [PATCH 260/267] Rename `GetRankName` to `GetRankLetter` --- osu.Game/Online/Leaderboards/DrawableRank.cs | 8 ++++---- osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index d11e200b7c..ab4d777580 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -53,9 +53,9 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.Centre, Spacing = new Vector2(-3, 0), Padding = new MarginPadding { Top = 5 }, - Colour = GetRankNameColour(rank), + Colour = GetRankLetterColour(rank), Font = OsuFont.Numeric.With(size: 25), - Text = GetRankName(rank), + Text = GetRankLetter(rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, @@ -65,12 +65,12 @@ namespace osu.Game.Online.Leaderboards }; } - public static string GetRankName(ScoreRank rank) => rank.GetDescription().Replace("Silver ", ""); + public static string GetRankLetter(ScoreRank rank) => rank.GetDescription().Replace("Silver ", ""); /// /// Retrieves the grade text colour. /// - public static ColourInfo GetRankNameColour(ScoreRank rank) + public static ColourInfo GetRankLetterColour(ScoreRank rank) { switch (rank) { diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs index 76e59b32b8..ce8bd941c2 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Origin = Anchor.Centre, GlowColour = OsuColour.ForRank(rank), Spacing = new Vector2(-15, 0), - Text = DrawableRank.GetRankName(rank), + Text = DrawableRank.GetRankLetter(rank), Font = OsuFont.Numeric.With(size: 76), UseFullGlyphHeight = false }, @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(-15, 0), - Text = DrawableRank.GetRankName(rank), + Text = DrawableRank.GetRankLetter(rank), Font = OsuFont.Numeric.With(size: 76), UseFullGlyphHeight = false, Shadow = false diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 80414d3f44..5013150f05 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -392,9 +392,9 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(Score.Rank), + Colour = DrawableRank.GetRankLetterColour(Score.Rank), Font = OsuFont.Numeric.With(size: 14), - Text = DrawableRank.GetRankName(Score.Rank), + Text = DrawableRank.GetRankLetter(Score.Rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, From 7d81ff81157d4d4935d6f0649edc7e163eb70a51 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 30 Sep 2025 10:52:55 -0700 Subject: [PATCH 261/267] Fix jank `GetRankLetter()` method --- osu.Game/Online/Leaderboards/DrawableRank.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index ab4d777580..f4f4165c7f 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -1,7 +1,6 @@ // 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.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -65,7 +64,24 @@ namespace osu.Game.Online.Leaderboards }; } - public static string GetRankLetter(ScoreRank rank) => rank.GetDescription().Replace("Silver ", ""); + /// + /// Returns letters to be shown in places where ranks are shown on a badge or similar to the user. + /// + public static string GetRankLetter(ScoreRank rank) + { + switch (rank) + { + case ScoreRank.SH: + return @"S"; + + case ScoreRank.X: + case ScoreRank.XH: + return @"SS"; + + default: + return rank.ToString(); + } + } /// /// Retrieves the grade text colour. From ee638492bf39c983725a9a38d0fd0dd4a56eb195 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 30 Sep 2025 10:53:28 -0700 Subject: [PATCH 262/267] Remove now unused description attributes from `ScoreRank` --- osu.Game/Scoring/ScoreRank.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs index 59a51c0944..7e44e46471 100644 --- a/osu.Game/Scoring/ScoreRank.cs +++ b/osu.Game/Scoring/ScoreRank.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; @@ -10,40 +9,31 @@ namespace osu.Game.Scoring public enum ScoreRank { // TODO: Localisable? - [Description(@"F")] F = -1, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankD))] - [Description(@"D")] D, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankC))] - [Description(@"C")] C, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankB))] - [Description(@"B")] B, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankA))] - [Description(@"A")] A, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankS))] - [Description(@"S")] S, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankSH))] - [Description(@"Silver S")] // ReSharper disable once InconsistentNaming SH, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankX))] - [Description(@"SS")] X, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankXH))] - [Description(@"Silver SS")] // ReSharper disable once InconsistentNaming XH, } From 6bf589af8813f0e9fc01f3676936a83384c6f4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 1 Oct 2025 08:44:56 +0200 Subject: [PATCH 263/267] Use slightly safer method of converting to string --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index e17901651c..aa590ccdc1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -1079,13 +1079,13 @@ namespace osu.Game.Screens.SelectV2 /// public LocalisableString Title { get; } - private readonly LocalisableString uncasedTitle; + private readonly string uncasedTitle; public GroupDefinition(int order, LocalisableString title) { Order = order; Title = title; - uncasedTitle = title.ToLower().ToString(); + uncasedTitle = title.ToLower().GetLocalised(LocalisationParameters.DEFAULT); } public virtual bool Equals(GroupDefinition? other) => uncasedTitle == other?.uncasedTitle; From b7d36cffd4f72aa6c2b5a2717114f22808b1ebfe Mon Sep 17 00:00:00 2001 From: Jinkku <49614252+Jinkku@users.noreply.github.com> Date: Wed, 1 Oct 2025 07:30:06 -0400 Subject: [PATCH 264/267] Refactor spritesheet-based icons to be single-file based (#34976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach Co-authored-by: Dean Herbert --- osu.Game/Graphics/OsuIcon.cs | 106 ++++++++++++----------------------- osu.Game/OsuGameBase.cs | 2 - osu.Game/osu.Game.csproj | 2 +- 3 files changed, 36 insertions(+), 74 deletions(-) diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 3a8dfac826..0cf2acadda 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -16,77 +16,19 @@ namespace osu.Game.Graphics { public static class OsuIcon { - #region Legacy spritesheet-based icons - - private static IconUsage get(int icon) => new IconUsage((char)icon, @"osuFont"); - - // ruleset icons in circles - public static IconUsage RulesetOsu => get(0xe000); - public static IconUsage RulesetMania => get(0xe001); - public static IconUsage RulesetCatch => get(0xe002); - public static IconUsage RulesetTaiko => get(0xe003); - - // ruleset icons without circles - public static IconUsage FilledCircle => get(0xe004); - public static IconUsage Logo => get(0xe006); - public static IconUsage ChevronDownCircle => get(0xe007); - public static IconUsage EditCircle => get(0xe033); - public static IconUsage LeftCircle => get(0xe034); - public static IconUsage RightCircle => get(0xe035); - public static IconUsage Charts => get(0xe036); - public static IconUsage Solo => get(0xe037); - public static IconUsage Multi => get(0xe038); - public static IconUsage Gear => get(0xe039); - - // misc icons - public static IconUsage Bat => get(0xe008); - public static IconUsage Bubble => get(0xe009); - public static IconUsage BubblePop => get(0xe02e); - public static IconUsage Dice => get(0xe011); - public static IconUsage HeartBreak => get(0xe030); - public static IconUsage Hot => get(0xe031); - public static IconUsage ListSearch => get(0xe032); - - //osu! playstyles - public static IconUsage PlayStyleTablet => get(0xe02a); - public static IconUsage PlayStyleMouse => get(0xe029); - public static IconUsage PlayStyleKeyboard => get(0xe02b); - public static IconUsage PlayStyleTouch => get(0xe02c); - - // osu! difficulties - public static IconUsage EasyOsu => get(0xe015); - public static IconUsage NormalOsu => get(0xe016); - public static IconUsage HardOsu => get(0xe017); - public static IconUsage InsaneOsu => get(0xe018); - public static IconUsage ExpertOsu => get(0xe019); - - // taiko difficulties - public static IconUsage EasyTaiko => get(0xe01a); - public static IconUsage NormalTaiko => get(0xe01b); - public static IconUsage HardTaiko => get(0xe01c); - public static IconUsage InsaneTaiko => get(0xe01d); - public static IconUsage ExpertTaiko => get(0xe01e); - - // fruits difficulties - public static IconUsage EasyFruits => get(0xe01f); - public static IconUsage NormalFruits => get(0xe020); - public static IconUsage HardFruits => get(0xe021); - public static IconUsage InsaneFruits => get(0xe022); - public static IconUsage ExpertFruits => get(0xe023); - - // mania difficulties - public static IconUsage EasyMania => get(0xe024); - public static IconUsage NormalMania => get(0xe025); - public static IconUsage HardMania => get(0xe026); - public static IconUsage InsaneMania => get(0xe027); - public static IconUsage ExpertMania => get(0xe028); - - #endregion - - #region New single-file-based icons - public const string FONT_NAME = @"Icons"; + // ruleset icons + public static IconUsage RulesetOsu => get(OsuIconMapping.RulesetOsu); + public static IconUsage RulesetMania => get(OsuIconMapping.RulesetMania); + public static IconUsage RulesetCatch => get(OsuIconMapping.RulesetCatch); + public static IconUsage RulesetTaiko => get(OsuIconMapping.RulesetTaiko); + + public static IconUsage Logo => get(OsuIconMapping.Logo); + public static IconUsage EditCircle => get(OsuIconMapping.EditCircle); + public static IconUsage LeftCircle => get(OsuIconMapping.LeftCircle); + public static IconUsage RightCircle => get(OsuIconMapping.RightCircle); + public static IconUsage Audio => get(OsuIconMapping.Audio); public static IconUsage Beatmap => get(OsuIconMapping.Beatmap); public static IconUsage Calendar => get(OsuIconMapping.Calendar); @@ -246,6 +188,30 @@ namespace osu.Game.Graphics private enum OsuIconMapping { + [Description(@"Logo")] + Logo, + + [Description(@"RulesetOsu")] + RulesetOsu, + + [Description(@"RulesetMania")] + RulesetMania, + + [Description(@"RulesetCatch")] + RulesetCatch, + + [Description(@"RulesetTaiko")] + RulesetTaiko, + + [Description(@"EditCircle")] + EditCircle, + + [Description(@"LeftCircle")] + LeftCircle, + + [Description(@"RightCircle")] + RightCircle, + [Description(@"audio")] Audio, @@ -737,7 +703,5 @@ namespace osu.Game.Graphics textures.Dispose(); } } - - #endregion } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index df1eac4461..222427cb60 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -467,8 +467,6 @@ namespace osu.Game protected virtual void InitialiseFonts() { - AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Torus/Torus-Regular"); AddFont(Resources, @"Fonts/Torus/Torus-Light"); AddFont(Resources, @"Fonts/Torus/Torus-SemiBold"); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c343c831e0..6457d1cccd 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From ccf31511723bbb9d563a38d9c509c168cd11fa6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 1 Oct 2025 13:49:54 +0200 Subject: [PATCH 265/267] Use consistent ordering of update button on carousel beatmap panels Closes https://github.com/ppy/osu/issues/34810. The reason why I touched it in this direction and not the other is only because the standalone panel positioning of the button was touched last in 92ed9646277d14b469a7d8f18ad3ad78f613a8fb, thus I changed the set panel to match that. --- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 1a6e886cb7..a52d3fa216 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -134,12 +134,6 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 4f }, Children = new Drawable[] { - updateButton = new PanelUpdateBeatmapButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f, Top = -2f }, - }, statusPill = new BeatmapSetOnlineStatusPill { Origin = Anchor.CentreLeft, @@ -148,6 +142,12 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Right = 5f }, Animated = false, }, + updateButton = new PanelUpdateBeatmapButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, + }, difficultiesDisplay = new DifficultySpectrumDisplay { Anchor = Anchor.CentreLeft, From 0230a27de61ef6ac727e6bc1d505b88cceb5b673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 1 Oct 2025 14:17:16 +0200 Subject: [PATCH 266/267] Add failing test case --- .../TestSceneSongSelectNavigation.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 9a1f1dc515..f3e9d5b3ab 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -217,6 +218,26 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("leaderboard matches gameplay beatmap", () => Game.ChildrenOfType().Single().CurrentCriteria?.Beatmap, () => Is.EqualTo(beatmap().BeatmapInfo)); } + [Test] + public void TestEnterKeyProgressesToGameplayEvenIfCarouselFilteredOut() + { + PushAndConfirm(() => new SoloSongSelect()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("filter out active beatmap", () => this.ChildrenOfType().First().Text = "abacadabadaeba"); + AddUntilStep("wait for filter", () => this.ChildrenOfType().Single().IsFiltering, () => Is.False); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("player entered", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + } + private Func playToResults() { var player = playToCompletion(); From 9b78187d295b6913abbfa3a7500c7e5afdc27156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 1 Oct 2025 14:31:48 +0200 Subject: [PATCH 267/267] Fix pressing Enter not starting current global beatmap if carousel is fully filtered out Closes https://github.com/ppy/osu/issues/34693. --- osu.Game/Graphics/Carousel/Carousel.cs | 13 ++++++++----- osu.Game/Screens/SelectV2/SongSelect.cs | 8 ++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 8f001007b9..b605efc69d 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -453,8 +453,7 @@ namespace osu.Game.Graphics.Carousel // matching with exact modifier consideration (so Ctrl+Enter would be ignored). case Key.Enter: case Key.KeypadEnter: - activateSelection(); - return true; + return activateSelection(); } return base.OnKeyDown(e); @@ -465,8 +464,7 @@ namespace osu.Game.Graphics.Carousel switch (e.Action) { case GlobalAction.Select: - activateSelection(); - return true; + return activateSelection(); // the selection traversal handlers below are scheduled to avoid an issue // wherein if the update frame rate is low, keeping one of the actions below pressed leads to selection moving back to the start / end. @@ -560,10 +558,15 @@ namespace osu.Game.Graphics.Carousel { } - private void activateSelection() + private bool activateSelection() { if (currentKeyboardSelection.CarouselItem != null) + { Activate(currentKeyboardSelection.CarouselItem); + return true; + } + + return false; } private void traverseKeyboardSelection(int direction) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 8b36e2c358..64262ed6ab 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -987,6 +987,14 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { + case GlobalAction.Select: + // in most circumstances this is handled already by the carousel itself, but there are cases where it will not be. + // one of which is filtering out all visible beatmaps and attempting to start gameplay. + // in that case, users still expect a `Select` press to advance to gameplay anyway, using the ambient selected beatmap if there is one, + // which matches the behaviour resulting from clicking the osu! cookie in that scenario. + SelectAndRun(Beatmap.Value.BeatmapInfo, OnStart); + return true; + case GlobalAction.IncreaseModSpeed: return modSpeedHotkeyHandler.ChangeSpeed(0.05, flattenedMods);

H2#@eSj-DJ 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 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png deleted file mode 100644 index b9079ad5d5f863ccbbbeac6ab2677ac0d2645698..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1452 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 n?OHhGXep*ZTPPG6 z96*o~Ka@eKiOiH35FtuXA%TyY06%;%fA9$jL;O%6p$U)@HP(nCkWvBxO@t7JLo&jBQyWN|r{ch6hwv-du0@2lV4-PIkJVTV8TdcDlE zHUN9TS%53R8Q=i0C7*c=FajO`wgC6o^^r7LL4u9|Pr%CzJ_65g1Lgn=fF-~R;343N zmWTD|1{y4@XBK=F@FpXB5Cch%17-mmY$vRfwfX_Q9=G--K&;Y=US3}IadB~OF)=Y- zo}Ql09v&V}%I8;BR<@RxmkkRG3tNMOgX^80oogyzgKlyIa0~d{mm&*9&%otvKoq@S zP*C8Po}L~Q931Q?YD%NgxHCLFJlECLHCIzpGpG3TAYcG}dsl^+MY4?OWk4vumz|yM zU0q$B;Ns%qbPztUxw*O3*w{E&R#rAiZr=iQ0IzjTWR*m5yIWgZqaq_CgAOAlO;1lR z#>U3BFD)(Yuv@Z(Ep`kRmx*cW?MJs1zWwjHZ{u$5*{2tO& zM2Vh)OW1yhqE9w7Gz@rqdw&7z)rN(IT^$=6o7?L-bS-%9GJYq!^$y@giRG3}K^4QRZOk=;(+P-TJ(|ymkdq z^A=`iX7+1Y12YT;GMoAXUQlxbFF*+X5*ixnl$e+pEJAc(U|_Pjxp|RI(GvB5&j6-K z$Kb(0(y~@@ad9}A27w{%&17L>%T6cLsH&>EBpM(^MMZ;T`W@g;7R}TA=Ire3nwGT! z0s_3jSbSG;x6e__q6m3|TU9hgp;bplMz*-y0d51)$3zd9hQVOi(YhuaI=*D;LCIcV z0!5dSlHx6D)!N$HaWWkRqDC^8(x4Dzl*a{1)*MLj@$r75`H=+GG*eS_oRlGpu0WOSCPWCiWQbOqp6&0EOr8s14>8`oDx;l%j zO;Q-T!D&wRgzPcd*Vjk4-JoJse;&B|5J1T9q|E^z@&731&jX&54P3P{MeR%WRnrj{ z@Rbc*mGQ(zxqVts89rcpsgIu}zhlq_b3r5{)YsS7cgTsjZKDhfEJH*V3=9lBPo}Ri zjiF;DkjVvdb91GvN>bt~>I_cStja+W3RxCUI}9E+eI0NS2%lqxrD>$5rV5L8%->|X z0V(kjC2O*7eSQ5LN`XVQqqerTF;VMQ zR8&yw`CCv>nnzDh4;AdIX0l9l{LcSUQc^Om*oHkvX=!PKg0Qqz`3@69uId<39jg}X zS?yJW0Eu>OS7J;}O)WsF{sUxy&o?$U3^Hqsj*iZ99)M(QNtrluvWzeW#;5#)V5u9s zM=)SVz9D1-IX`6$hHWC;wkxd3X4=ZSy1HZs2M7EAqf-bmvj)XxE2+W00*()!scu;p zXlv>&W5XJV=xFw>(WLZkX&P@t!xQP~-xb+I6BY-;^L{YrMsnkt+wp>M_8{EN~O1jQh5XIM=AM?&4=F#5X-efHT2G4jbi!w4Y zhLp`4z_8B3J1$wvIC4EiEm{d$oQzNi2!TmYZrnEU5qs)^eyUS^oOd6!H1_ z`BByqz7>)(Fa2M9!(H+oGBg=jEL*B$aYLa_Z3P|`o z)32V)HPqho$eCgySO5S307*qoM6N<$ Ef@aS_IsgCw diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit0.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit0.png deleted file mode 100644 index 3c3ebbfd0b56ddbc5ea559c041aa55c2901b2cf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12904 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 aaDdn7Y6EhC_Bj9m0O)ENYF4VlBmWQRa)M_7 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit100.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit100.png deleted file mode 100644 index 9ecc302910e924d3da00e7a27f3204d2b189a336..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30853 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 R_W%F@YD(IQRS27i{{g7rL}dT~ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png deleted file mode 100644 index a5a3545abf2510e7eec395bf47b73d9082e1fd8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3572 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 u5 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 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/reversearrow.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/reversearrow.png deleted file mode 100644 index 7ebdec37d356490d018f711ae525abb3250be3fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4853 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 Y0OO;-v8NaCZ2$lO07*qoM6N<$f&jDokN^Mx diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png deleted file mode 100644 index 82bec3babebd59263dc486e5900a3a01ea4aaed7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3712 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 JPDHLkV1kmK?iT<6 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png deleted file mode 100644 index b4cf81f26e5cab5a068ce282ee22b15b92d0df12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3337 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 TZ*tsI00000NkvXXu0mjfuoP_o diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png deleted file mode 100644 index a23f5379b223d61079e055162fdd93f107f0ec02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1910 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 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png deleted file mode 100644 index f68d32957ff8dc2e6e26e2b739eab85385548faf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 865 zcmV-n1D^beP) z8ION{U-N9C`MxinYv|$`M+}Q$F)W6~uoxD@Vpt6OzhDOaIXgR>o2*u=8V;)D@TF?C z+T17S{{H?ym;%w+TADXmhT?$-O-NWvN0;UR_GXqT)+8VcT7_OfX+F1jnFIZS?x8!# zh8}URz_+nygyLs1rfHeIXpZ(*xcNFpGu`( zwcG8nVHo$9mzUrA{eGSAow*&jOrP%XnP4jnsY0KQkB@6tS64o6^aq1MaO`wCezV#1 zPfkt<_8IyJm7o-H)Y71MrP!@^ySuwPg+f7WY;1^ZHX9uC`MfBVO5*6~=mXZgWiM7_ zg2J#BUQj3&i!V*nT&N{Y(}Zo?M9#738`=bqh+giU0>f>loV9|82+ zaU80#CVuw0IXW_vYCm)NDP(>JPj7d--Su=j9k?q&C<*&31~y-*}fwjTm!p*P&Jvpnyz z)m?^=o3|X6i423GymcH7s?X++lz!!LDcrGp4_y83T(4ux)LkG8*W#nj518@;Wz|pB rcm*5_XP0^3eKYf&7eE$5NN3S}(it^R$P@B}JRwiW6Y_*SAy4SP5!!0C zTE9FJKtC>>NlHB>x7U?fC3eMrUE}-6&6|p1Lzp5^Zgp4;-?Tka0Ir& z1rTBaEJ%Zoz;`eQ3$UbTh&3rgX9|7}x(Kd<2wyLO^E@RI`3er0F^e8jrdymkWwHaJ zU>EG6-At#`*8_n7WhgqG&Jb^YGairM;r=~O+gUez%_x%?%@8~chr`cux!mVouO}vx ziI~l1Vmh6Q;czJ0?Y5Q8W=ZG?xDRe~_LPX7{ta*sJS`LoBN8x8Qyg!)u8YB7AXw}% zxJ%jddP>*89q=fXO1TEF6vaC~s48xE{ zqtTROY?u4ELr$I4>-A5|~39QXWMIpY}VCkbsmq$cd@;G3M5vml{BBvrEa%t<@5R1rBdky#=iw0 zIjg12+aCmBcDQpy_7jQ3UL+DBYmj&PwOZ{J`d!cgAHWx$%}VBs{OkvExyH8F_z-XN zhAjBd4tYuwo)#^QA>$xAWkltmAR9EuCtu}5>6DRg%ppIcnq=Aa2%R;=oZ@xF169W7 zDr`CzH^QVBIE8JK`;M1drwJD)wp9Li|3LU5zyS0^RhH-3lmq|(002ovPDHLkV1mea BUy%R+ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png deleted file mode 100644 index fc750abc7e80287192efb65633e58b6b19e47125..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4904 zcmV+@6W8pCP)>HPEA?bv`x0yR5sa^IhC4IP-&%2W!qCT)6~GN6jP8z zltl$(hYNRk=iGM=@9+a<{p0;+&Rp*D`+dJ>|D1Ea=v3#IPV=h$PLh+zA^z^}r3W$! zGSIW_KsG`4u0GIsjfJ#}={1b#=re=(4i|0#YNprjY=)m@0_q3k3FJZV@v;Kd0W|n>QZAbRG^nNiO?^UR0E#l_jgTq9~z?!8NzghdU+poJL2g+hsf zr%s(3>2NrVMMXvR`T6-ZbeqY?$H!~YqD2vtCQb6+wr$(ljEsy-_`CopALtxOAAjEj zYuGe?=SEiIO=||zdXeGb;n7b$^;GQbx8L61U@)iw0|q>G>#eu0fbXu5AobnyNm!iC z4z)E9C>-cIe}DhP6)RT!WB&a4{vIA4N~hB)tJSLZ?Ahb^^2;xA!^W3iepw$D7N$%l zlbSejV#3_Hb7Ph-U;alyK|vN-05e)gekv`XA88NQ97OAmNJ~pgUb=LtH!!Mj-IkUX zjSNns|XW)x3H0^tau1n;SBX zn?MHf>esKInlWRBY5Vr=6Ysg_p1_kQPX@rpK|sYoRTMCIU1YF7xdbv8mYSNH^vENR z1SBLRC|uKMG%Balsp{(L)P@ZkPK)5?=uYJ3;9;i4!hqsIgHtwc+&E|Y^yx<2I5HzI zWE2-FuvbA>_Bc1})?TD*91RaRCO`cV;31^JRgM9}_V?m^S0O^aH#Y*}zzT%68g zvABfB&uwmQR^WS%l$4au;q4xv<76FG@VTw4nq($@$l0SFeDJ|3@X&S)+U<7L(9obR zT)3dX9M#ydW6_M1o10sk-?_WHtEi|bwPVK)pC5nxal+1>J4ff`miEGX(4U%`nq05AY1g8_2nq&;fsEnr zQ2Y3bpW%0`#RCTps3}vXd=GDT0Uf4*hJnW_qjhJRG*S3QMnje?S(4!E>)VDwyc!!D zReE|lGMG=%(|Yvi(Sbkw@IxZ_uK~=i)i+zSJD4+W8cANrELsIJhMFoVDN)~j_nrFn z*I(7<&70NW!Gm4DBQ0Vo$^*JUDGBp%OII0GBFb^v7<|huw|JS&<~BdV4OLZDsWofX zoQJoVoN_3L1ikalJI!a#oSE?ItFNli(9kx*vDs`cM(}=S3_n8+6&4oS_U+r(yldAk z=P$qff>B+qUcFkOfKk)7lu8+O)|5 zzIY7Y{t0x3g2*6X-n4b=R=?S^XGaeiGNcWYcpoHjmlc3kTPiCnFJFQSHeYt-%9SQs z8pH|t~*}3o*C3~41znaK}Kr%Bk?P!W^q8gaph!G?3&%c4t3bKg) zB0(YZEr4n)vu4e5`7Q>ZnwlEa;sv3_ik@mw1|Aps04W-@riwlVzGD+uw3;xglR`O~ zT@z^B4rI^<9(bgc@+4Y7E_lKK39#2FM#W>fwhU z_C*NCxcKLve{T8x_ur5Al@^tzbaF6UlNv!k0}NWu0R-qnZ@lrwfvgb;1OTcvLDDaw&AO2 zbN~JK>m)wK`w_8h*|H^%Hrz~t8Vs=QI!OG%oH9_0APDrulN3><7Hz5zE@l!V*v0(( z{8PJk?`}fL@`V}#O${Q(VzAx>N+gPZ;DHB*vDry54^h&(fB*iY6g-^NoC>?^uDcRP zjT)unNQ^>-#9v2REYxVx(N|11(?ikG^Im-M#RE_)I@vd|ℜ1r5i{-0$U=)+!&zI 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 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini deleted file mode 100644 index 06dfa6b7be..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini +++ /dev/null @@ -1,6 +0,0 @@ -[General] -// no version specified means v1 - -[Fonts] -HitCircleOverlap: 3 -ScoreOverlap: 3 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint10.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint10.png deleted file mode 100644 index 3e2fe66a1c0a91e6f21d9bf670d4515ef8cdb3b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2349 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 Y0Sy+ultD$RxBvhE07*qoM6N<$g5gp;DgXcg diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png deleted file mode 100644 index 3811e5050f4cc0404a5713b1b875ab33ac32243a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26350 zcmY&A<&;CK4H1&tA%{vyNK?$BR*{kvD)$^lB9rJ`+Z*p3 z3+>%mO(>BXZxuQcC7lpbslJbycc0(y4}b7*UDxY+zMikw>v_Gd>ptMO!iz-EApihC z^6~cY2LMnY|51hk07yp71^@uSZgTZ?1%M09_PDHf007Y9mITMGj@cNuJ3KZLaE*xJ zM$&zv!#73xM}|iv?6?=X2mm&sJ|3RR9EljkY>i#cxc4 z5JZ3)DRsZ|Wl#4FTEqp?%(Uam!?fz^yv=dLhAKG^;NE2HWD!S;NUJXMAT5Wmswts9kSd6(IBOTb7!L>6vK{`mS)w`LMLR*RBS8=6}jHQ9&l- z^P7nJ_Zj`N6y$<(bs(k@GuF~K-+utG8ocE`GROQErqM;j9UQYa8E=kt5pDd5RQT=R zP&W5h&cs|#kO3e-P7tvO?V*69OmZR)4Ski$wAXp#%2mPCFps?9m%l}ZKxRjGGSlU- z|8Z#|)kRbQme%f3Ym5<(tugGZsw%=Sp;cQjL!V$(Byqy69$!){5Z{l<#CS0#HDw zis}iTZvh@#p(K?N<8drffGTTMAS#xwJe)OQP^nAoOdvl_*Pav+dM(H=!9IJxDebaN zV7WJ^VP*A;MxH5);HNOq__{S>VCHtl>J>i-JH?}br2vLRvT*HQL@aW5PsO8sYIYeL zu#73!h816_boZ4PWR_XEe#Db!(Kg>~Rr{Rn3ecJ>$+CS6)#eMImWaBr?Ag!KgEStBDY!&a@Z^=$}4F70fJZzb@@YxpKg4 zshMzzFcB8H0g|jG29_?t>?_G)+@jJ~-;^2IFOcr$yY8a17HJi8&}YWd_^Acwi!O}l ze{0KrTqJ#TQ5p|xMa5uTY!|1Aqqddmya82{SMYLI;dS1q@h7dxF0Mm9(l;iHjmC{Q zJJpLCmN#Gltc=&hoDv+oB1=@_%3c6GpE5_uZn`R?5!RUEYmXU<86C4^n720m6|o~kB!lX4v)~${Q_Y4l6Hc8&C=WLe{MbI?KCl)x_pMJ{ z#Fm9ZrL%VAgSA@Po%wsxA0M2wbG&c6SHVp8TW*{sI`dN6^rde|NCAH?`MrAxe+ue< z{9Bgt6Ow4`A(VtW?FxTCbjw;~pz~D||ND-$N80CksmTj1fjB!c)APIub$ftz<1-8X zqFMEa((=?OY0>mF{W-VQa+Ym2X8={fgWU}qmgsD;jzs6~t5a^w6^|&N%tg)|M4O)Q za4ZjSIybtjNePC*R!5XJUr~PSB;*HSzss`tFEsD{7UQOpc<)DNLql-IN@A(fZSflH zWFjl1>BF7RFBxth zBgoW}ezc)Ksqe&gR}Xx6aSLln>Tkg$80qfo!e6!L?=_(Cu8<`wT{YONWJT&y)qhs2 zJKm4OWnQpB?Aq(iDqqqA$u6E&nPQwG2p!!$n<*&(#I;@v_oQ^D2N7<^ZE^jTp*f6Ut(b#uUD>f2+W`&%WSDOZ!XpPRTc1vP^3#RTb#N`G#KH%P3xDf-bve3HY)&5mAkrv%=w}u!XRKKiAvXO!NhI+!GZGrTK`80Di?Mz}?Adb_tfGH14kT9_V52Di4ld4g#VXvW2PACJiCTR^xsc#` z@{YTw$n6(RvT2y?h#k%X$9dTPaHIYL_agf=QdDlAngspXf?n&xCtSgQGe6QQ6PqRQ z-iH#(4{1}<93&}MvPx1=U;pGKTZ6MxK&)azd*_38HTQ9CS8mG)VCO&v!{Jdzkd{Qp z67$cCPe@37^If_$QK`D5xn)iv~dF-`~x#IR$xV5&@b?T>!9puqcgd&ODFY$eD?CS8)HOO6LW}}tM|mXS}FT|?@XWItSt@xW$tc;moeYqdEfU+ zfwdYrd(pnZVGVr+aARQg>8k2*W52&seW&==r&v&gOA5K*BQ7#gW@5;@t(New`iH2K zxxtA_MeD!jL7a-aoPx&Pyxb9I+nhxisw2{QV^@j37e(phSYqe(HnmQ&b=nD1&+qNp zY*(o^aw8HU∾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 B%76d> diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png deleted file mode 100644 index d84eab2f15eb844ef0c37f3a0fbda07f7f4b4d16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46103 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