From 3666e4c3320090bb4efc1bbb54ab252f2a390b57 Mon Sep 17 00:00:00 2001 From: Kian Masri Date: Tue, 10 Dec 2024 09:50:48 -0700 Subject: [PATCH 001/308] new: rank the alternate mode --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 9bf5d33d4a..269da46fca 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => @"Don't use the same key twice in a row!"; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray(); + public override bool Ranked => true; protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action; } From 6cb46106fee9c4013a8f6e2a5a3d831b36e8b815 Mon Sep 17 00:00:00 2001 From: Kian Masri Date: Tue, 10 Dec 2024 10:04:36 -0700 Subject: [PATCH 002/308] new: also the single tap mod, it's the same thing --- osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs index 91731b25cf..0d9c7d4afe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => @"SG"; public override LocalisableString Description => @"You must only use one key!"; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray(); + public override bool Ranked => true; protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action; } From 245ade004a7b36733ff0fbd3d7c11d4cfe43752b Mon Sep 17 00:00:00 2001 From: Kian Masri Date: Wed, 11 Dec 2024 09:47:17 -0700 Subject: [PATCH 003/308] new: rank Taiko single tap --- osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs index a5cffca06f..5e959387ec 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override string Acronym => @"SG"; public override LocalisableString Description => @"One key for dons, one key for kats."; + public override bool Ranked => true; public override double ScoreMultiplier => 1.0; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(TaikoModCinema) }; public override ModType Type => ModType.Conversion; From 66eff14d2b800dcfd3d18c41dab7023e02146d49 Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Mon, 16 Dec 2024 01:17:35 -0330 Subject: [PATCH 004/308] Initial proof of concept for tablet output scaling --- .../Settings/TestSceneTabletSettings.cs | 2 ++ .../Settings/Sections/Input/TabletSettings.cs | 35 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 5ca08e0bba..74578205bf 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -134,6 +134,8 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaOffset { get; } = new Bindable(); public Bindable AreaSize { get; } = new Bindable(); + public Bindable OutputSize { get; } = new Bindable(); + public Bindable Rotation { get; } = new Bindable(); public IBindable Tablet => tablet; diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 00ffbc1120..22e0bc99b9 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Framework.Threading; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -35,6 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly Bindable areaOffset = new Bindable(); private readonly Bindable areaSize = new Bindable(); + private readonly Bindable outputSize = new Bindable(); private readonly IBindable tablet = new Bindable(); private readonly BindableNumber offsetX = new BindableNumber { MinValue = 0 }; @@ -45,6 +47,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; + private Bindable scalingMode = null!; + private Bindable scalingSizeX = null!; + private Bindable scalingSizeY = null!; + [Resolved] private GameHost host { get; set; } @@ -76,8 +82,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input } [BackgroundDependencyLoader] - private void load(OsuColour colours, LocalisationManager localisation) + private void load(OsuColour colours, LocalisationManager localisation, OsuConfigManager osuConfig) { + scalingMode = osuConfig.GetBindable(OsuSetting.Scaling); + scalingSizeX = osuConfig.GetBindable(OsuSetting.ScalingSizeX); + scalingSizeY = osuConfig.GetBindable(OsuSetting.ScalingSizeY); + Children = new Drawable[] { new SettingsCheckbox @@ -244,6 +254,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input sizeY.Value = val.NewValue.Y; }), true); + outputSize.BindTo(tabletHandler.OutputSize); + sizeX.BindValueChanged(val => { areaSize.Value = new Vector2(val.NewValue, areaSize.Value.Y); @@ -267,6 +279,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue)); }); + updateScaling(); + scalingSizeX.BindValueChanged(size => updateScaling()); + scalingSizeY.BindValueChanged(size => updateScaling()); + scalingMode.BindValueChanged(mode => updateScaling()); + tablet.BindTo(tabletHandler.Tablet); tablet.BindValueChanged(val => Schedule(() => { @@ -352,6 +369,22 @@ namespace osu.Game.Overlays.Settings.Sections.Input aspectLock.Value = true; } + private void updateScaling() + { + switch (scalingMode.Value) + { + case ScalingMode.Everything: + outputSize.Value = new Vector2( + host.Window.ClientSize.Width * scalingSizeX.Value, + host.Window.ClientSize.Height * scalingSizeY.Value); + break; + + default: + outputSize.Value = new Vector2(host.Window.ClientSize.Width, host.Window.ClientSize.Height); + break; + } + } + private void updateAspectRatio() => aspectRatio.Value = currentAspectRatio; private float currentAspectRatio => sizeX.Value / sizeY.Value; From 4dd0672aa5a5dd0865497ceae68723e4a2f83b46 Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Mon, 16 Dec 2024 21:04:08 -0330 Subject: [PATCH 005/308] Address screen scale positioning --- .../Settings/Sections/Input/TabletSettings.cs | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 22e0bc99b9..db670a0939 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -36,7 +36,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly Bindable areaOffset = new Bindable(); private readonly Bindable areaSize = new Bindable(); - private readonly Bindable outputSize = new Bindable(); + private readonly Bindable outputAreaSize = new Bindable(); + private readonly Bindable outputAreaPosition = new Bindable(); private readonly IBindable tablet = new Bindable(); private readonly BindableNumber offsetX = new BindableNumber { MinValue = 0 }; @@ -50,6 +51,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private Bindable scalingMode = null!; private Bindable scalingSizeX = null!; private Bindable scalingSizeY = null!; + private Bindable scalingPositionX = new Bindable(); + private Bindable scalingPositionY = new Bindable(); [Resolved] private GameHost host { get; set; } @@ -87,6 +90,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input scalingMode = osuConfig.GetBindable(OsuSetting.Scaling); scalingSizeX = osuConfig.GetBindable(OsuSetting.ScalingSizeX); scalingSizeY = osuConfig.GetBindable(OsuSetting.ScalingSizeY); + scalingPositionX = osuConfig.GetBindable(OsuSetting.ScalingPositionX); + scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY); Children = new Drawable[] { @@ -254,7 +259,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input sizeY.Value = val.NewValue.Y; }), true); - outputSize.BindTo(tabletHandler.OutputSize); + outputAreaSize.BindTo(tabletHandler.OutputAreaSize); + outputAreaPosition.BindTo(tabletHandler.OutputAreaPosition); sizeX.BindValueChanged(val => { @@ -280,9 +286,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input }); updateScaling(); - scalingSizeX.BindValueChanged(size => updateScaling()); - scalingSizeY.BindValueChanged(size => updateScaling()); - scalingMode.BindValueChanged(mode => updateScaling()); + scalingMode.BindValueChanged(_ => updateScaling()); + scalingSizeX.BindValueChanged(_ => updateScaling()); + scalingSizeY.BindValueChanged(_ => updateScaling()); + scalingPositionX.BindValueChanged(_ => updateScaling()); + scalingPositionY.BindValueChanged(_ => updateScaling()); tablet.BindTo(tabletHandler.Tablet); tablet.BindValueChanged(val => Schedule(() => @@ -371,17 +379,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateScaling() { - switch (scalingMode.Value) + if (scalingMode.Value == ScalingMode.Everything) { - case ScalingMode.Everything: - outputSize.Value = new Vector2( - host.Window.ClientSize.Width * scalingSizeX.Value, - host.Window.ClientSize.Height * scalingSizeY.Value); - break; - - default: - outputSize.Value = new Vector2(host.Window.ClientSize.Width, host.Window.ClientSize.Height); - break; + outputAreaSize.Value = new Vector2(scalingSizeX.Value, scalingSizeY.Value); + outputAreaPosition.Value = new Vector2(scalingPositionX.Value, scalingPositionY.Value); + } + else + { + outputAreaSize.Value = new Vector2(1, 1); + outputAreaPosition.Value = new Vector2(0.5f, 0.5f); } } From 93ed0483b6a6ca70b988ed1d0623640d7a3b3559 Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Mon, 16 Dec 2024 22:59:04 -0330 Subject: [PATCH 006/308] Fix TestSceneTabletSettings --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 74578205bf..f44a08e03a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -134,7 +134,9 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaOffset { get; } = new Bindable(); public Bindable AreaSize { get; } = new Bindable(); - public Bindable OutputSize { get; } = new Bindable(); + public Bindable OutputAreaSize { get; } = new Bindable(); + + public Bindable OutputAreaPosition { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); From fd504e5641c0fbd0fb46dd9d97ccf52e11c150e9 Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Mon, 16 Dec 2024 23:17:22 -0330 Subject: [PATCH 007/308] Minor cleanup --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index f44a08e03a..b94069aa7b 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -135,7 +135,6 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaSize { get; } = new Bindable(); public Bindable OutputAreaSize { get; } = new Bindable(); - public Bindable OutputAreaPosition { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); From 0b3b6468a594676b4d7da662b0aaca1768b43eec Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Sat, 16 Aug 2025 23:02:28 -0230 Subject: [PATCH 008/308] Reflect tablet output area changes in osu-framework --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 2 +- .../Overlays/Settings/Sections/Input/TabletSettings.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 1fbc395aa5..dc9ef7439e 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaSize { get; } = new Bindable(); public Bindable OutputAreaSize { get; } = new Bindable(); - public Bindable OutputAreaPosition { get; } = new Bindable(); + public Bindable OutputAreaOffset { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 8fd5bb7c8f..35304f7c34 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly Bindable areaOffset = new Bindable(); private readonly Bindable areaSize = new Bindable(); private readonly Bindable outputAreaSize = new Bindable(); - private readonly Bindable outputAreaPosition = new Bindable(); + private readonly Bindable outputAreaOffset = new Bindable(); private readonly IBindable tablet = new Bindable(); private readonly BindableNumber offsetX = new BindableNumber { MinValue = 0, Precision = 1 }; @@ -265,7 +265,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }), true); outputAreaSize.BindTo(tabletHandler.OutputAreaSize); - outputAreaPosition.BindTo(tabletHandler.OutputAreaPosition); + outputAreaOffset.BindTo(tabletHandler.OutputAreaOffset); sizeX.BindValueChanged(val => { @@ -389,12 +389,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (scalingMode.Value == ScalingMode.Everything) { outputAreaSize.Value = new Vector2(scalingSizeX.Value, scalingSizeY.Value); - outputAreaPosition.Value = new Vector2(scalingPositionX.Value, scalingPositionY.Value); + outputAreaOffset.Value = new Vector2(scalingPositionX.Value, scalingPositionY.Value); } else { outputAreaSize.Value = new Vector2(1, 1); - outputAreaPosition.Value = new Vector2(0.5f, 0.5f); + outputAreaOffset.Value = new Vector2(0.5f, 0.5f); } } From ac21f8b9606466f6f0c1b4dda32524b59ea68617 Mon Sep 17 00:00:00 2001 From: Du Yijie Date: Wed, 27 Aug 2025 10:06:26 +0800 Subject: [PATCH 009/308] Implement "legacy" pp counter There is no pp counter in osu!(stable). However, a "legacy" pp counter allows skinners to more easily fit a pp counter into their skin's theme. --- .../Archives/modified-classic-20250827.osk | Bin 0 -> 1688 bytes .../Resources/special-skin/score-0@2x.png | Bin 0 -> 3283 bytes .../Resources/special-skin/score-1@2x.png | Bin 0 -> 1986 bytes .../Resources/special-skin/score-2@2x.png | Bin 0 -> 3302 bytes .../Resources/special-skin/score-3@2x.png | Bin 0 -> 3273 bytes .../Resources/special-skin/score-4@2x.png | Bin 0 -> 3170 bytes .../Resources/special-skin/score-5@2x.png | Bin 0 -> 3377 bytes .../Resources/special-skin/score-6@2x.png | Bin 0 -> 3305 bytes .../Resources/special-skin/score-7@2x.png | Bin 0 -> 3170 bytes .../Resources/special-skin/score-8@2x.png | Bin 0 -> 3714 bytes .../Resources/special-skin/score-9@2x.png | Bin 0 -> 3193 bytes .../Resources/special-skin/score-comma@2x.png | Bin 0 -> 1149 bytes .../Resources/special-skin/score-dot@2x.png | Bin 0 -> 930 bytes .../special-skin/score-percent@2x.png | Bin 0 -> 5397 bytes .../Resources/special-skin/score-pp@2x.png | Bin 0 -> 4777 bytes .../Resources/special-skin/score-x@2x.png | Bin 0 -> 2801 bytes .../Skins/SkinDeserialisationTest.cs | 2 + .../TestScenePerformancePointsCounter.cs | 3 +- .../LegacyPerformancePointsCounter.cs | 44 ++++++++++++++++++ osu.Game/Skinning/LegacySpriteText.cs | 10 +++- 20 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-classic-20250827.osk create mode 100644 osu.Game.Tests/Resources/special-skin/score-0@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-1@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-2@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-3@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-4@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-5@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-6@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-7@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-8@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-9@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-comma@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-dot@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-percent@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-pp@2x.png create mode 100644 osu.Game.Tests/Resources/special-skin/score-x@2x.png create mode 100644 osu.Game/Skinning/LegacyPerformancePointsCounter.cs diff --git a/osu.Game.Tests/Resources/Archives/modified-classic-20250827.osk b/osu.Game.Tests/Resources/Archives/modified-classic-20250827.osk new file mode 100644 index 0000000000000000000000000000000000000000..605ea60f4c74a2f72cf77d766ae5c5654a785726 GIT binary patch literal 1688 zcmb7EeK-?%9RJNs@tl`t#F<>AD=#bIG)>M+qM4R$u5xRJE%UOa8Huu`G@2DgswrhC zR#ZD#)ZMu>ByU~biaojTc)8?dcj4%s+^NoWf86J<-yh%4_xF51pU?M;$3oQ+0D$G7 zoMaMct-&hoRs$d#0e~R@07qpoy-A)Ptf&JlCXE@xMuoFk%${5&t9zrKLOE?M#dwG) z+OTW7rja4x#g|)|x+DX1{R)gAy~!_-s7kt2bNVal4Vk&do?7V3QSsOt@dnD^O*g&@ zinm)YMMYnz<(Lh*q@xp8gfKYuA<25~Qo*C#gQZwzPDf}IMM$m2=i*ZhPTu#t0Xv&F|Y z>!Sw)CTTA&6Mf5qlQd82jr=&ASwror*xhvzx6{&9Sy&X|n*B_=%N?D2FU~!A)-P=8 zZC2soV2;my)t1wA(d5qLk^3$|GZt4Kl;uOR=Xk-j{&CN*`=xUe zgsGJyqk?MW>oUyaAPx8JVomcLnZib#8gfut=5Go;QNVdHi~@1IALwJ9S;sOdM>QfhI-hY zptn}tezhDYvu^+8$$jqQn*}=E1qK>#vuCdI>t>J}?x+-DI;ll_`z#4FL0n4L!;7qC zys=mzUw$O`4pb5C*XuUfOTQOCO%xc;PQ$~rjIe1#8TGSGLz#crH4FDuMV(wbe4jv8 zDXB_$hPcVVlOa5@wXwaWU2S{}*1JRLm`2%|uEf9Jh7yuVU|Q3F4;ldA3;@sp0N^93 z@pJ|)GIU`JN(cP$crC20Ra0X#;gSqpj z116ByM8+ccmx)by6UrG_H{7W**ujfd?CJNKha{O;7&@Ztt>TfL^Shz0uaJ3|(oSb_ zoXzvA-_~5T`u(lS+;G#QXkLHpGByl(BXI?nCnlh60x(!?ID zI#cz9`Fs^Jd`n|vDfqz}_Ru1~mIF1NITwsjt`^Py`U>x6EIsR1SytXO&!Fz8c=_^1 zR=3CZ)lYN1v+(dI0n27_WDj0X6r9xBnirqX@msm7-qrs4iw)HA`6a^p%FZ}#0RUhC z0J;DG0*kqyK#Qb>#QZ;d+t?FXaJJ}!!w@_cqUH+y@2k293GdzO(>M0x9-qqMA|yN= z=+b0<%>7iv79ruWp&tUbl(p2xMM!wt)c<0=H+3m>skDob@J7}DB{Uuj(|A_^12BMs K06>JjEB_M>2cuR1 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-0@2x.png b/osu.Game.Tests/Resources/special-skin/score-0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..973f2a51be308f0045dba4e47a22a222b5b7ad98 GIT binary patch literal 3283 zcmV;^3@r1BP)R=P!1a4rXG35H@_;`8mjGvwzkdO50fj)%dwo-SkPNI0@#cN~ZUVmq9=_K% z=?|&E7NDaW1W~=f*_wQ%lzI`+_H25ld6nGl=D$sKqB}6pvJ7Bif&SArbF>~fj zCQX_|R8*897i?>5qEfHZYSa&j^+yzl~(CQZU* z>YkC>>2z}N;6c``Swmf2ow_?A{rq3${YWFQxnC-#cH4Ng)r+ihZ?;t!VHuFqh)gb{Q0a{ zvEr`23jScVS{XHJ6lG;)baZsM=qT}u?A1C(uMn>R^JKziv+>F+uiQ5xxG-SA08A#6 z+IeOHe-*961A1yd0Vo%(0)Xu7Y)VQ>uvjdi*1KEK-rml%Y162!t(ECN1J9^s18ot} zVwj9}5pi*GY}l}YzJ2=|QRi+!j~+cpOiX0Yo;@->40s1?t{!`-gx5; z;^N|rs&ls?H#e8`^mLhu0Dh{L3q*+Vz9-42`0Mof&yJTMk0kcqB@m;*fS+Ax_+y#D&@_ig9s3l@uof`S5- z_UJGp{2U)4hD$eWHXDyU_Lz}%VO3>AEJ9iK?n9s;jGI`jT+s`0OKcg<2qn%Q^`Ct5NtODJ>mOf2{6yXuKYlzzh71Y0BCA%dqO!75rCWiWVsv^_ zSg);efeRNduwumuoK9y**U!$*R;f(aKUzXW0Uqyw5v}3WsZ*4dm8tY$AO)BPEEk4O zq2N3wM@NnvVeeke`BK0bGGqw7d-s;9SYR+dgs=%Q07ytkATu*lqnJLdS+hocq8|g_ z1wPj({<-MYD`ax*+O>3cb_QJslgY%;p+i-A1U`iLfxIzs;>3_=mX(#2oIQJ1raFNi z1O8t8c45b~xrn;DIxSb6E~KWWs#H1}Li7P9%S2&eVMrGS*uH(cN|geE+|fl~t4!?M zw=bl+3?4k#i$X()eBrzRATcqK5hH>Q9s%HRIM~16%U$@Ej#KU5B&ij_C|_3IaOoix%4+EIk~ zhP+o}yw!~>SFWhknIKO!NZts=S9<5p9hGW95h79EJ7B;7^#I-Y;tQ|vftNA?AYN_} zk&%%+^w2{=*GXgO^%BCPMGP7=NIgI|G=%Vq+XsM=@OG2Z*lTG2qR9EeFy zP7e8%{p_>PyePU`2*n$Oc*1({w7e1O+{fv3Y6($=B!tJ4>LQmhVD#zZb>Dg{AuI5G znaIt}4XHjiZro5`*LvUvk`NwQB+bpi^l9GE81MCXfNg;L1hKKPj2SZ~r23pY=ao5B z<@(1ZghzluW7rD{$;rtorDp=;5xJc_c``kEgp&LA-_U>p_te-WM2kGbYj1BiB#xlr zD9s4)&~)G_nV33tYRL7m+wCglb`HzEqDNYZM)Ia^XiVI`-9?zug=Ni!A;(_DTsZ%QD5qFe@@Ngbz^sqreBMmtN z*ewz@QpCEB0^!*%1KiPGtJTWNl`9RgYYrSZpi)QV4f%cjr@TW6$OMhFv^0u}iYP5D zmFawS4xz$iGO>E~YOM}eH)?BZsj8}ysjr3TZpczs%7#^KZLMMU#7i%|r0a&vW;3t8 z`l=zpCA5P=9|*VM|E?+he@FHpIh{_e;U*~b>C=Zzn>I0S+_)~wCM6|NQc^;3aj_xS zMq7v-DxnT#M*(+k(6M93$jQl3j|mDfF)@^slyLm`amvffIdkTW+es21AJ5RCL&?j_ zW8AoLhPVJ+sHv$@U-J&&FKRjUihF@Yu8oR{3blO57&B%}$fmL$Y}&L*r9RMd5UJhq zVcEjGeEG6gT;hSj&6_vby?eJxzo}JL72-BTDc;X2o>)S1GI-M+EzFb}4 zc2W2@eELj%8S!7Ly=zHHiD71UccQekRBM8@SdFNw@Rtq~BP>mK5Hu+%iEZ1q-M5QX z&YnHX?Af!`u--1!$)g2-zOPo{)0^oMtf8TS)2B}}Wy+M0-DXCkv9XZ_3l^wp&9_CG zqVHU1fv)bRT12AVDXZ76UE}K2t4x?M;l5eW=5RQ8@x>R_879Dw0!>T_1B^y+Xp zShj2#hYufCcO4SRssX3g1p1=43a`=+0e4U7O8d{t%QJMw7zpa?>shj72}h55wfrg( z6Vc^{*VE%Q2&IdGUM}*|rAt&)RFIjON&o)+^;|n>oIH7w=bn2`D>dni_@IyO^n+k} zyHwzB;;$PPi-lRUW-)i}T-9@J7;fLb&4vvd*tl_{R)D!mh!E80nS$;12LpT5Ogw<7 zs3@jRoyzp-(+$bYL*zYe+qR9j-+tTI=TwdY-xFUU3JRf&e|>?C;#JW^Qc@E6`T6AJ zTqWYc)1DQd?U~O-&8u<>j0{?Kc~-PRt*?hKzfONMX;{-Gy&XeFgk1 zh+K=iiU<*|+a;#+Mhft6@r5(5tn&LE{e<1|j+h1qbMqa-q5O;-0qYLj&B;QdNPPdM z_;f*n7=8Lni)C8H{H9vi1lz^>jxfRqBaAS@2qTOz!U!XbFv18Uj4=LR@gJGn5#;iT RO-BF#002ovPDHLkV1n-$NE847 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-1@2x.png b/osu.Game.Tests/Resources/special-skin/score-1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..78cadf49bdf241042a4c3c5af4720e8bacddcc0b GIT binary patch literal 1986 zcmV;z2R- zmiLlK=MMw({?D0np8uTtbDkG+$RURua`-*NcV6=ak|3z+ZuWlZLCG5b&<} zV+>)%7NlDUz?%cVhxYb%J^d8$70{cBfbGCp-3ki8qD6~nZf>TwwicJm-UWT-%9V6`xh)18VgLU9DzyT@o#Hn~fVcF7%jKfJzMg&i_Ss=$ zN-SHp3{BIBMx$}65^yCG@DJ5^x7*E;BS+Y_ZJX`J8o}*$Q(9Wez`#J9Dh8e~67V|k zj-DtkF6Q|0<80ix(Gg>fFg7;E{rmUz)HpDx3fKdDpeJU}p3SLKr&zIKg(HS0LtkH? zzWV_50ulWn-XvDn$tk6%sEDSfrfD}Gpu4+UrGhB??k4@Zke{E=i4!MSvu2G`Hl{>Z zSC>ltgigQ$U}-!>(=?79J4Q`SjgvN}L}zEGO8tmVfYiBME*BLQ6;9cf5z-0h1AdOD zL?RKqUN7V0<4)R`6hlKp+`M^HPmKdtP{zRD^f|h^x;T9Juv0dsL}zEGx+CoX!YBg1 z1U}N|YH4Yqp`k(jvE#UU^{Pt!C;mm<3%v4^v%Tt*1uxexh^-nqjd!eHLiVq{3)4%E5yN z>Fw>Es?8Ga-o2|*QrFJZ2l*|q8F)r`$~sbI4uwKAH#cW%t04>oNWPYAov4?AzXQvG zS>jmtoDf#8&v@?KIit$6#=U#@RBB2sV1!YzoPQ&PH39#PCq^O>+S=Mo6#oeQ{rxIs zss&OZrmo~xtQ&T=fDwL>YPx+pdCm;sw19~85Q!wunIRrLc%V}M(?3`(K=L0I78aT= z&_IB6vbS1*WN|@3f$0K=hlf>4Qe?FN$;*+KmyPe(7(+utDka^9OxtBzrg-cHINgOT zMI;hY4^k82;%^dG3y^lKX5H>1BO@x6a0szlfHdvo=jWR$%s@cGfUsJCWU&nb5(b2= z>wsAS6B83EmEfqF@#{c3%;-U!39J?%mG?LbkS>Xfii(owvW@{HR+X4nh(ga11n*iHe zz|wVKI=Yah=Yi?yLY4~1%ge*CD_o1yDnJ>4Q-VPkvQ>a|2)3dNm6j$%H%kGkwjSyYHe*?%yu4hco|ft`UBhpX z{(Y-F5(^hDR4FNX*h&GmxKx}JAX#ih_d$d>E8dMi=?AiH0<4&U07^0Hc80y2*MhtL2gJeiByPP{y%jNkYI(W&Dek0><hYqRqFYzzd zbks)}6Sws)ED+Gs(?erpBYwYMO^N~y@dqmfAR-Bi0AjHi;cz(XQ`wa(SMd3Kw6?aY zvC;_R-w@F=V1s~PfypW3^5x5{U%x)>kAlGGuDN}!cKY>7iKp?=yix;_Z<3_r2zXSHhgI(>!pG7lEuM}2QRmHY$ z-5RT|u8x(Jm1XlP;=h5NPT!`!XY&2Y2@!<*f|Ew;p^4XU6Ivq0XH-N;zUt%~tm#k# zd?qgDRD;7}CV5u`+D_RA>BJk|9$<}FJ%1H%UfhTWZ|9Ih4msqILk>CQ@H@u;0p`6% U)kg*k{r~^~07*qoM6N<$g4w3CJpcdz literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-2@2x.png b/osu.Game.Tests/Resources/special-skin/score-2@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..10dff4ccfa2637573d8e211698c9a8b69f49eacc GIT binary patch literal 3302 zcmVou@P)crE00009a7bBm0013_ z0013_0gvVJWdHyG8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H140B0D zK~#90?VNj5RaG9xKO6*nAVHEtTt;~&;-HymK$cizQDlWnG>u%-i1Zk%q1BSXtk#sI z3FC4(#!Tr9)3Cr%nq`?atsYYv4w*_2*--gF%rJy5Wr2eCGXHGuY&Pd!?&Dl@*4*z} zXR*%r?AMol&VKxUzkR_6AAIn^2OoU+e}o1ET?G1N(rpUTT|R7zNw|yb5#)G45Vxfwx3?m;BhmRE!5U0pEHk(%#h#R030f zY@uU;=Ybw~gbW`(yie0KSM$9Eqe)mKhFOB`#E^VLlfJfykF0~8k*^Tiim=shk3);Uv-Gp)QDct@QcuDa?f%FD}1OG`u3G-uoGHXC20ux!~fy1Ke#`b*$Spu=81M_u#> z;0Bp^;DHBt=%I&z@DrqaLGOREaL9f<6|88Ko6`T6Na&cMMv-eb@S%UBRqUio{JJG zDJjIp#>!M6aHpD17Ca94x%_bc{Q2H&qv6QT&Q_@#RYDegt2}B?nKH$r9gjV9cXwkp zn>lyxoZ)3(d+oI03X&N(U&SdS{ zwS#n=b;|82PdzRADQtr6p4yvlE zY+?bdRx5k0#TpZB$oRlb)VVc6K&tX=!%9 zx05GN^2#f(@X|{!(blGGYHAjCs9xaj{p4krUFM0tL9kk_Dm7r^EDu@_0&&2AC5zQ+ zrKYBanwlCkO~Yg|VKSKr2??R6rw6my%+aGqIdkTW-8KNv0Y$(MK$ZM(#*7)BDOB+F z*I%pDxAKiFxKj+{cZ=m^teVqmwQ}Obi6OsB310w9f%nCI3@~7Fzxn2yoh{2L96fqe zrT#77s4K1WKsk^I+y#8#X1(74&j3>-u>eoX{-l_g7^X~_;${_t(bm>BU?vcs&taJu zHYnGB6IO|98n6Jk3HX_VJ@hv~15g3{O?V7sTn{XfiTm%rA3s0i+*^D1?p1AF`+xx} z9D9Og928ffn7XbIUWYK@1Nv4>?57-*F$&l$ujCRE63EQVbVrw6yz|aGD)pvHI3rlb zS>R(g+Ok?66EscZi6@>g)Y{qJ-p>2)zpv7}RKolARY(U07{YRMbD28TSl?K6bu|`? zMW*V7`*i@kEqJtW&DiQOnM^ESzTD7Cy1Tons;W}yKk23Qw&0CoZU?c}ed?*FJYKe{ zsI08SY&Of(H^7T}X}u|U9`Fm9SiE>KS6_X#p_O!Wbg*U17M0!r=&ppkCD;TEXhmFH z9FIKmh~X8Ll$59*2s7||d+EI)*iS58!upB8z(CfmTW7fC^P`VG;sJv|f_7OG-9#nN|37$G ziIS2M!^0rVW;65V&Exdx(=vTbxJSQpwj6g`XABo>wOT1EDpJLs z7dAMj#o~eBXN6nUhWPk+Hg4SL6}Rg1&p)qc&MgN1PQKE`U)j%TAFjO8H;BpICN?-Ym=|7n!7E~WdwVG;C{V>(fJN?##Rb8Wgz-GEp9l&H zV$-HgOq@8;-MSs3tgMX2#zvK11^m;Ka-DglgMkkLn=(z)C@n4J#v2E@UkyP+Lj!l; zeYg7EzA7RV`t0W%8qW0aW?3x2iWMuoCbqk~o1&s3RqU6-#Osk*oDjTB4I7w0f4*0C zKm8~zEmcE?`@~TIL%eQ|Ug-$ormzKpCnhFRSy}0|5T=*~g%ggh6 z5O{oiJRu<=eD|I1ULaDqS^HhBeZs-s2pj^Ai+{=q;iB!*D+D`()=vT3Fc}gO!tUL> z2?+51skyqkIv#uMF<1Q0da#J#*yqH`aKH&SJ;9m6f&swv>C-7MFZZMYF2QQG($v(% z-o1ONt*xc8v61d>)${5!8pLA77O0~4N{7ie7drG%(=?Kjl1NHQV%f4~SS%KrnwqGu zucyAgo~EWIT3T9E$C*7$5r)MS5xAqTMZQj^E%>FMFfks};Fe3+J& z7Ft_dX=`ia)TvXnw6t*k{GeUqPSMl4f|F$*aCmq)@4ox4p`LngqO-G;HEY(W9*I7o z>yX~i`>F8HfN(S)I&|oQG}Z`&hK90c%^FftQe@h%KYm(IutnX16&DwKB^cCO&@_$v z?z>N=7X$wGCf5O;zsAN!^78UjBicoc)YMeQjvXsgBY|jpg5Qv%)neMW5$e8sgXk+)Y>ELz5_66(j*>!_+ir1(+zK=k;ur%puN3arlN-$dP9U||2Dmc ziHV6UT)2>|tSrOB7(B(XW5<{~cdkrz1HnTb9y}+4PWynF>LAzN-p=mbyV<&RD{XCU zgolS49xUoE%F4?4MfwsmC1Di@LLg;XE$GLY%H0XnPg>UF@F4bd&#_t)2C09 znVG51EV-@XBYka{76ntX7=paWMpJ`<@|&_tX#R0ojZ5R)L~(lJMYQz zDM}ps$peOTIYdQ8k&%(X?Af!KHfJw2U_j0|SZoJm+%*ic!V!j2s~c=E|7RYStZV)M|87k;VbQt^#17K7Z7 z~wVOpi7L(JM1O6kkEh`BD|?ioQW{FgNszhK4~CPx1;Vr_p;207*qoM6N<$f(Qg()c^nh literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-3@2x.png b/osu.Game.Tests/Resources/special-skin/score-3@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..89eb5d322b4805a54f2c742d58cad6bcdb7699cf GIT binary patch literal 3273 zcmV;)3^wzLP)qkrVN%ii7|?A`8N zoDSca9oXOQ^YXoY_IZA9&q5cv(1oWNh9K8(0tSk|J%ubvv|BGZZUIff?1)}O`vETj z6M*r+3xZ&GmF1m46>vcup8)59uXO64CPV^rfTKVQ(5~~*0OSL6MOz_9Kj0lPv#yf4_E^2v_C^P3U~`x1@u-^piiGZ3>Yu~v)N2kRMgj%L`C6rIlSToZEC7)0}hBE`l=s&S4}h-*wN81YPDJ!H*OrGM~@~UA%VEKI3gp}jW~zH!S(Cc zDK9VQ{Q2{gl$7x0mtS^R1o$2Bd-2+Iiq42e1MdL~mF|s>jwU@lo%Hl{tX8X<_Ee$0 zy`A#%a*iE4#<63^sH>~gQ%+j+64R*_^_jTl!(RvTg7bW0i)4KR#p}{IXU#`(L+oB{YHCxJ0E}i zF}b<9N>8{Q_$9E}SNU$T^VvY5SPwm5wOZM>Z5x@HnS_Uj>(zhNFc=J2EEX~|GpVbq zqoSh1<1&y6JO<7xm6sCD1P)3x7y!b9JUpD0D_62`;X=A~laKDF3#Fx{WM^m7+S)1=7XUM*0;BlrkP6${+DJ-Ddg@66 zj2}OqH{X0yE=~icNd+z@kHp1+h#9WBx|*3YXJRk}+rZI|goFf2OG~+P=Z;it0zPn^ zNFP}ZFyDPrQ$u8AB;&@73srvuhQVN9*sx(7Ja|wlejeB_baD@(*MLN+n)lgfpJ6hY z7&&reDEl1{`uFe8xpU{Zd-twOWB~2}r%_l(KLHj2JrF~HvA{9LXo&nl?^*CLaLtf^G z#bO~fHI>xVRN~{~6$>{sG*DGl#r^yDvD@wPb&j4rdtx$~uv)Fe#>Q$QC&!d2Q{*b5 z1*q)k(Yyp40tWfIAv!vmAwz~>GMQ*^Z>PS#o~o)U>g(%OcPu(Onn{x;v0%XhT@;ho zUw@qo7cNLeNx*3}X4+f8N~G}mJ^dQmb0gsCiFE!NoO;{BZiOm)r93U~sP3w`qeV3%F* z2MiS+!#*zI_3PLD;iL$KiPC@{L|teA{wxlIcz>a6I4AP)D4!4@RSrurMuMf(H*C$VGRNDD~e4;>E1) z?!%ZdW0c-8)o?f*a(x$}+J0+9Gl4Q-tW>yY(IQQiUcP);Zmv}V|K(dC`YKQi{0->q zapBchvDs{zDnEDboLuxT*FV}`2z4+6Swe>%t#m_5N(vh{Zq%$=78Df7MeYSPSVa2@ z`*xmqii*}iL_`E@)~q2bD@${O=g*&~y1H5_dLrfk(3xnAc=vt?OaXkl3JnGWNl8hp zTD6Md!-wnAcwSzfTy#V%hoBSD9{@iSs#b>!U|3ifHk*y?>}*|a+|#E|Q&LhQ7jKfD zXn84K5D~6Po!vNM#0b*U)0r`22L1Z=)6k?hnwy)+$jG3krba40Dfbx#`b3usueeV| zf9TMmBqt}6nwrY6VZ#D!rgN-Yw@yy-E8y4i`vM{Q8(@Rd1&hT(T3Q+@DJec}RXwqD z=S~U>3+3X?VvY8Ize9I{&~rU7b?Q{Iv$Gj9W{kgm4H$d(?&aNg-<6C1Bm7>E8Pw)qKU#-1`!CJJ$rT_ zeGM1|1qE!`vPCX_2&4f2?5w=np*uo)l9-qnav$%eO`GJR20`+3HRaVlQ4gQw;K75{ z_D?r7G&IOX>1s*h2T@hIF~LMkOpIK#LZk$#f?A>;9%z63wt7LqR-Ge4CldtrcE_$u2=l zOA9AXoS>|%jJUWsOeT{S4ys`^8nM}IR99Eaz55e{+h;xB-T`qc>nF&x5>}X zcL#SY7K>&_bZ0Oa47~jE%bYxUQo6<(#FBGFet#h9%~9Z-yoV!HQe*lxG8a^*_-q91sJ zOxfXxc>)rg%>(RET8;gj=_~ zV$og|hcayF1a3$LdZeq@)z#71=rK|oT~Tf+LLDDXlAa75I#hFo7cX9vi^}E70wE?k zK}6f!7(H4sTAp}AT_=_s5ZcVz zwQDs;xE1WuC%`|H?hOUe2_pN&HV%@u$2(MNL8Nu@QE5Nwp~3;bK^=7@=j z;qc+Z^y$;5%`vs63__`A~m!F;0KfWOFnIqTQ2*Gv-N(4j+el3ys< zp)LfI=nfGRaby1c`6MMJ`7WUnt*xzm_@UQUwD&vmKL?BGJHo%@#@MlAS+i!1E|niR za6oRBH3PdlDiaK%KNlfJH)3OB*}QqPE_>P<8yl4}P<8dS>)lnm4c|KD7J6ku3J7sWo0G%_wSdBa{-?_Xc9S2Vq<chh(&Dk7=!y8aL_nVd(n{3B43SwA0im|GmL*G;@bKY7skj~ZiJ15Tg4SfA zG13s?lP6C!i%gSFc{>=FOXo8a0aEy;Xf6 z2ZYvh{P=NJuU<`Ut=DAwfe3YIg4T5)OsIYy`&S|&BFN0lWZJZ8j2%1H=N7DM*RD}m zSV&=Eq1^0p;ehxdHGio}j|I;xT#VmQx)>Q5iPdUl(4ax|>eUNGp3*JvR0t3zJ`waMvAwc0az{iiaPX2H zLNq+g;we2Z*&N?2K8ttxX-KH-LKnKwg)VfV3ti~K|0VthJx0}7u0N6j00000NkvXX Hu0mjfn7}SR literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-4@2x.png b/osu.Game.Tests/Resources/special-skin/score-4@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b44e9ec4602c84e77a5fdd076bda3292ccb8d5fb GIT binary patch literal 3170 zcmV-o44w0dP);C zsB?cacXXeeM_;RmDZh5}1~|A@~6g%#ELZ6IDtq^71ad-iNkrrYhNy1JT* ziVF7Z*~6VXUVZQYU=Oeq*lvL@mZ4f>l7O9Zhq`p>QgU;1JzucdY^0>5ke{DVQBe_X zZEak*a6vA^2K*d24qR6B$1+qO*M9;2Ne|+3>YxL=SBPX@8{ij-__Ti8$c#- zThS*=P^mHLz+dDt%FD}{IB}xi3o|n_89R0?hYug7r>93V4*>d!j=axN5$1hhvX)3o zOXI!w-otLU2Xt{-S{gk)J$&`mSF$+|*a8^#*5M?uU(uT@zN+`k%0$!61%a$z* zy0&N(78dGF&X5YSm{5dS20W%El9G~m@x>QIb|57sMQ<7&a$yz{tT55Qo3dfSf(69H z#F$^GP81atk(ijM zB(5UQ$0f9E9RG>&UELyZk`Q{fdUgXM^ zE3&CaJoOF(B_^UErX#CFxZ+i1M+^v5nAhdnHEPr-o_XdO<(u!^xl?bl0nZ0f#6U4$ zTVwUpERd!YFaC-i7B60m&8Fr@Q&SV4e)_53?>+wblpMXJ>oaJ32b}O(*CnqPQGH1=2rEh+DqQ40lGhr43 zp4s8x!GlBkYwEdk=Z-$c>meZ_fuy9Qkjiy99E=$=Mm9wOx#m~+$4nTn+EP+dLcf0f zRPF;nRaKSg*d;YJRjF))M|)`1PhccWwtg1<$}85yr@#F2OVd1EP&{ZxjvT2sDLt8t zgjp&lo_+RNqwr%$96NT*G*7k03?4jKZwjql3iQI*#5ll1QBe_AaP#I(>g(%G^V8DO zl*%_){q(&Y>Ic0r6M+$0A|WAx=bn2`rF;)nRaHLoGcq!i$~TBRD|L!`VZ5II)22;R zo)>AHIdjG|&t|jf2WdfLAdHgZdSSdqy3?mmS1H?z)2C0H<|QX5``rlOkNa6;1_Bd2 z8Fo9%50^n(df~mmG5EDM=33M$-;Qe z_f;RCGQNa~i;J_Q#uyCnhN_e&$9%n%7%^f*?{A05-Me>9^Kx=>l-9mA5)%`#*=+I- ztzp1!v2E4`Todb27XkAQPoN3&m|XQ@V`CXIWQh4yL6Vh~#kq6m4D<5y^DVhYR8$mB zr;|&UE@>tkP;BtkROBi}<~`SRsN zNBdp)7$^k$huG$CSOYX+5@cgsoY?|kXw02Em(tQwPrSuuvoU`Bc(!cWqCBvZDJUpl z^XARLghz{jqd>0?N~;!Gzk&(gv{rj!*H|)6GZtCjlICt(GH*VaZv9XbFzx|fF zx;mPg4EF;Z6Dyz{nn9~~o$|MQb(RkUcDtR-%uF&fGkfJ#S68!k?OOe$cAD5%_y5-$Lpg?c(%ok;0 zyaKW(PMo;ke)Ty{r&B-fNz%?;$-;Pz0xK&kX>Dx{mydmd-EL>lph2?9rY+RT!W@-% zOm=p5vT4(%aQfLN7_4)3pcBR|<_{h|`sgE$9z7Zk|M~=%%cb9r@;%UoLCkl9+zGqg zZpzEceXa*R%v4lV=uOA9t!nzX=sn=9Si80Zbai!c=+Gg$y1JM!VL~92w1+Z;G>fK#VVF?{&&hgP6{`}VPJ-8#MbeX+k70tguYRP3XD!YFgh zm@yO;6)|Vd9HY?U14n0PCmT0z7;{#N`B%V=(H=HS7DeE8vq+`M^HpWQCbNq8-l1{0>s1AY(q+(8r@8_U$G zQ<*Vi1`{Vv4105PyWO~4E^2COID7UiXV0Fcwzk%6U8@~f2=v;C9ZLK%7g!0*4P*ng z!{Hz+D~t5>bP5X#nKETc2)Fc2cXv1Y_U&W$?%mYa*9RIlaf$PnhPw?^0=vn;BGHrg z-l=TD^y$-Cvu2HDn+0y&xwfnL%MtQGr#zc>4m%KB~_a01v291zj>6n~on z8yXt8a^(uoKOf9l3UgMjTxq&JR>x@}#%sVq5o$5zew;s?AgbsTv1QG0MR5K4^~{(t z!ze3Yy1To{&CNABSJ?=h6N9yXiBGRy*BAHuV5SWi1B?^ri#+ST2?=`BRB^sanQ#kpT~0JNH!BsEotWi%RRt?vDH$w{;w-lfaVjM~KAxwaemdX+u3x`SX=$l`O7Oi1I;nM9g1!)@ z1^AKp0C4i;Nm5c$$jZv{dvR@TEoEh8T)TEnpY=Lmeo`z*`s%euBSlES>#)`2$&;Bo zcP^7AO(H2NiI|ue+-^5bO-q? z7)ujyRg7v60u@%qULH1Vk7C~uAwq-*5h6s05FtW@2oE#=2SCwVv_fL%-2eap07*qo IM6N<$g8i!gR{mkre z@9*4u&$(aDJ@=m9?{_bF@ZiCN>*!qEuOBcL7zG#rJx~qY0ImYH8n^R1NdWK@-~`YF zblUl~0Y`uz^>u^Y$p+vX2O`+}odBNc>&9zI2$17~5c01D$OK*h{QCNYy2H^J;tQMu zl57=*hlewE>{ucrBMAx$qPDh{>({UI_19lJs_zN#l^AP^ffDh%1w2rFHX7hakW65s zYzPVpV%4fu%%4A>Aw!1P+^L|TfZe-ylb@g8)3($Bx5e*{^!F}M2DJ9{fUaOqkmbNX zWW%IMlX&~>w~2^|aO%J(PMjb;J)N?$GA-KK0aQr>l>-&xZ@CbxL5qrP3E~A5$b(@* zLIT;@+4%YOYzSy;YvatBGn_nmk}FrPaQE(AE!u342GOf-ic9h<;8ag9MO%Vc^^X4i z`*Zm4VTKPMuD-<$4HOj>QC3z)X=y2U@7|@Xtc=RaN?Kdpaz(TWQ8Iv|qNh2;mLR#n zT=UA3B}>?`V~6G~mFev4q`JDAii!%WUu9(_m6es$)YQ!0uCKD!;iHeE}T3TA1IaC|) zYtdtDq7-BiaM-*O8ym~9W5?8-ORrH~T}^p;Ib~&Kl$4Zk{``50i;L}To(XIg0$I^d zu|7>+nK5IAmTh(`27>{E!NA0c6D_uqk`l7AvdGQNrL(hB-uxG0JlUi?j+bKH>LDyK zF;V-L_aX@i38bZ^v1iX70s;b*#jgTCQyRS#%M<0o=bn4cU2W}EUVQOIva_>^iHT7b zWhm!bvLNxmAoEH{NC?r<(e7?*uMry?OJ-)KvZwd~X|h2UWT;#a7Z>O5HugHhhYx4_ z_U%gZ0>E-9$bwk)Cubkg|KCVSNnz5YNlJ67dEt8}2*C2?%ax|t<^@@hD0z#purPPG ztIv4mnJzvn%el*f=;bX!Lqpx&u0F%YE!c81kp&qnZxI|E?Cy5;8SU-uN|WV@)ou@Y zngzLk|Gv`HXkPeU_K<>t0;Q?QydVo=HE2Aog0#1{larI9G+SH(=E=D=LbUpTt zbw_n|HK$LX=H9(~G&Fqc?F$G9z~A4W@bGZL!orA%h`?Ylxb>g$&O7fYdq_L*ck_x_ z5I@-!;Opy)PUmV+uPgca`E1#;g{Gz^r?&U=^CL1cl8A^1!o$NY%Lapi$jC^-!@~&- z40O8wrluyo{PIio@88eavuBmXIl?Ckk_dXB#%v4@4(8KOKXtl7M;MJp7A{=qv>49f z>+4HsXeb7QfzZ%U^m;vm23hW&w6wHfGMT8Vs-mcCTaiw{W2#5m&0l`4Dcv_4RlgxpdG{8P* zgfHZdvcN2el{-n{U2>VCq9PtWdL)~wgio`}laYcDNt_sId591%+6B>W04K!&YB!2z z7R1U!t5rzA#sg5H6Jj7@EDgXl2ipt;B7ty`k`X1GvCokOsget{6a*3U zI&k0s2?+_d+OS5eZ`l6$b%-0$dy^6QAJPMd+UWtl|V%Dr#ZmT#ZCWd9pmJu8r z%+;$`ZAI_nMfPJ9a2|Nz!soCKx1PgdcD3+^F%^J zLs`6dF*==&OP4Mw2i-(*YyOv$ZiQ}93eqNg3scR8>({R{Z{ECK9bA>@bUKofk{CaJ zJfD5`8IK=Z-GRpdErjVAFhF-6?vs>>`S69c%l`E;Nw3?)sizi%{JzS1%AIE{Ea)`6C zvXUc5jxccGKw@KKZTUEQjR6A&kd&0fp+kpcXI7AyH9PXfI>eD6kAZ&zbHqRkKy!05 zXV0D`J3AYr(THBJ$6zoxIE?$y>-9W%@PMmVugYd0H9lKs;+3Hy+&9zS2E&F8BPAt; zl#~>EGg!BA`}S=XELb3$s?|7~oDJZ50j~i&yL(M6K0cn*)KpSaQ;CX-a8x3^hKCOyx>%)sY>3>&o9!&F3L^7Bc+FEqXz^8hv(RhTuCaIT zUJa_Wrna`ri{$rQJ(d=N$kYLQ#XGuj!0(i=y(W!S7DVO_ zuw8^MEj)Q*^Qv;0%a<=JXTTL|CVB1@1kppb$cFLb$7@=_$&)9QrvIopp1WphI^bf@};73?wTni!o!yXjV}}Ljxau^pVoM3uv~zhjVB-h71<}9V;>PdOaV0_#tD* zY8!-m_uY4uz2iQxTkUbR5@fLWR>(Nn7#A1EC!c(xY0m(-aN&Zj*JuAIL{Wtn*||Qz zSzwB6oIH6lyLRpBQG%^Aw6wIaXwf3d%gbf+6%jsBoeQSL81hG@7}KXu=l%EJx0Mf~ znv9GLr5NoZ2CPY9oZ3rfB=9TQFm2j2w~2A_;zd6G_+zDc4{$-NI@De=ABg`B0MXIW zTM(iqRpmku{w@ zs}W?F>?8^a31Q|;yIw|@>FDU-_19mwH7WeP+J`V(s1d}<1r{D2j+d9VQ=^WK4z_LE z#;H@Mltupr-11QqHD_QIPqg8aP(^2FCvUy=7KaZXRu+8)EYm8Mt3-_;cK}OBzP7fO zTeog$(x{G(4tDI=!NG$El|`k(5%rDjJ=LQ&&*2C1*A;4NYM48BuH!wrp|!P@H{N)I zqeqV_iz-D5gj*72%%~Nl8L%Xx-MMpzp+koeaxF3$2Y;D_?7r0ztE+rn$|0Nc{i z!p4moxpvL@@M>OO9;;WcRz8v=Vzkp;4Jo*S7P(T@Vt`#@e)4T?Z6!B1mxP1_Vq;_N z?)doeV|MP`$$Rg;XX}-e(<0`heM+?hv`QYkCT@etWs-=Fj6&)a*O zvKjb|2xUAqtQWD7o_=?=P~?g|mCO`*YR(AJ4E#Yk6?hs65Kdo{9UT116wD)V%aMnjrHKcg9i^DJb3Wn!Gi}6-!uLTewPWxMslx900000NkvXX Hu0mjfcb0&K literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-6@2x.png b/osu.Game.Tests/Resources/special-skin/score-6@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0e7bfc377631cde00c12db3a7008f3630522449c GIT binary patch literal 3305 zcmVNc=P)3(Gul4%}@T0D7s3-6k(CUT&w%B)}eSCae ziO>iv1pKK>{2_oX3ZLFX=$nT(4j-t?Cfmos8OR_32_7%($yX5EOOKiO_?&q zT3TA#=9inBYmJYOcP7GB;1N$W(Bz3i1%3;xl!>UQC^l`{M9-c*on|U7E+#WGld7sJ zHK_`i0F)TnNKe@T(}4A|z@ed`Y}~k!K7IPQ%s6P!ASO?q%+;$`DK9UVlX?Luz~6yy zjcTYT1c(vSSq~c#7#PU<_3IfpaG+ZS^z7M_i4!MMU0u!T)2HPm3y=tG5H^Hv7%f0A zU=I*#BmDgQc7W#ontbYNJvPav9XbpCr`>r{eT9b*oX!jEx;P_oZ69B{OpdiMNAFt968&S3q0{8)WfPHv3o6Rg=zMO!7 z00WBuFEnP4AS22(LV!O4aWXM~{(K&O*pLfk$Mx&iRqCn{Z_Pc#u}KPeDaBE6xA7J5$X|O5indP zG)8OPsIRYQ$&w{1oeSJDqWsPZ@B`rYGBIu1G@g9&Nh8|ifF(*}ZgnZgVO zVelAz>u=bwfj8cGL#6KkPXVQdR@OlQB7tAa#GE;E=-6svw8DoHK`Gp z4jeY5A_W~81G3~fzkmP!%$YOCb&d{T!-fr7ov#C?0RJ?sA_eZ=0gnoAj2*AM@(O0N z*^ow*mzR^1lcUl%#17IiLn?6scLi7^-z|}mkt8K08P5mJMg&?^}90$I9&axs3^vb z8)rn_ARHcUis=W86#Iy0#Gsh0xBm8AVo}2KvbQiRD~rK{2kWiZ4vWP?cz8H9H8s@K z)X>=2=x)}5zzf1j`Y$nNbQtYP|0eJo+d*`6H1EClp5d!K8TIw`R904U>Cz=GU%t$R z3l}IUDWRgGLN$zJGy=>pF#7w{CIp;6e86*~8(( zhn`GSLk*}s23etuoQS6Nn8#;R4TC@Lz_O1>thY_|+2SA*bO z`5+}FrCW3!VA!x>tX{pEx8Hu7e*OBX$qxW~RrkCzI1?Zac-(%Hj}Iv+DNau9Iy~{j z6YSWrgT%x{HOV5T%PH-Z;cOOoP5Apj`1ziF_F28{^$5&nGm|Dw!elaW{P=M>$xn<2 zCBQjn`LzV_12zJ7pLTkBy4(ADT>+EHL~LvercRxz3h`RTPzla3^6u7@y?w)q^72_ zbLURC&TnmPW$DtTl$7Y(2i1sAvOn9h01j6_GBVQe`9D29T{Tw`uC7husw9Z{+8+DU z+SaexLmQz) ziFFvEp`mRk+aFl~2S1ll8y*0)wY6INd`E!sK#fM06Ji(G5Z6<1aIi{+*#5}(_#pWV zBL&#LeLJ^r-}tag!QDBuuWsCW1I+i%;17T>h}u?f&i4m%47 z2;km(@6}bQ4W~|>QmGGJ1UvhPIAHtOG<4`tMvNHYK^Qx~T%R3kuBQ&V-fN8=*cJw!GEn&ecCeY#Ggsi{e&obB(u1T2<`goFepPMoN_ z4a$lTv5^9j0L^0B`R{>&0^GfOSEaNzE`BHCj_jBEzWeUuwbx$Lef6uVs`&cruVt!1 zY_@_j2-kg0$?hX7!4ZAU$&8%3lg1&tDqk_}Iv_GKl5N|z z8P<8k?O#*lU~FC&Cb%6=1-N?EkYH>=LqkbTO?8@XGMSh?dp3D_d4?~)4;(n4QU&Ui zFE551qA$;%KW|t=($dlh3kzfY`t_VYf1Z|>7W($>%a}1^m_2*8)^b%h>g($%C@4_r z4^%=Hz`G(L1DpzQ_Uu`j zo0|>a5!P6l8IH!rM%Jxcr_$@RY!G=2xCGcOhT7U%PMkR5$#!}Sxw*OOAYL!tXlnym z0NeZfb`%yCw$0Jaaq;3s-g)O8m3|Xwvm!#nE9@|8?BBoNz<&9g9+j1q&SLnwC6+H=uH`{~$<^B>ssMGsHko+;{r8P*qsNe+pU;*pTiVd# zl|HA?@_RcxK!t^ceEzv1L75)IfddD~%F0sd3K8h#5?TTr6JA-6HYZ%f=yfTM967>* z1q;;G>mA`%`*(Z!oy7>1i&tX+xOVLt0|pG>kw-l85r+*zz|YiG zpEI};;Boo06-7lw#KgpOn^>WvM~^aZ-aP8+9KOS31%3(q)!lMj#R?x6&yYXjQ8;+; zAR|VM(AsJFPO*FUZeD)*Wi6k@OF)PA(Od~|6Zk?zUHTy0TLlFL^y$-w;lqc!$m%(4 z+O&zqix+FT?J|Mo9WAfDXz?$^Kw^)oXl-q!xVV^e=gu*D^k|nKL@^SLjg4eyXR~(g zT6I9S0t;LjJFei)7bK1eN8*oVr)5P&1v_@^z+$m@WG7k1>C>l~H*cPnY2GLX>ozMN zchM1->(jty;mCBrLk~T~%$YNpJb5xfL59564`P(h&CO-+-o0AW@NF^Gwefm%1ReD` z4;6d)@mh)g{{F?lR^J$xQ2|UJv_wUr zQiD(>P)Q>dg-8WVp-pii5GR7hU|R&A1;Pd6fGwL>Xb52NL3kLq#60}q+&?mVnVZ|Y z+ud7pfbJu$bo0B}y}9qs&d&UPzg>tDB}x?Q#M*eQIA9Qv2HXPf*?8=Sh8WeY9?%cU7TC|9P0|x?d_3Bm1%gZS#DdEC} z3wp=70n7#tgxP-EF)PS6V7^x9)vFh4*REyUxN*9b@7=pcX=y2Mzx_5hZro76ZU+{M zV?HpLx$O=GiewRAd+jv}3kw785EBzaPEHQ9XV2!+rAvJN`RDT2all-0ynPniZ#!lL zSq4nd3Zq7iV&%$}#Kgpe+|lWDQczGpLP7$E4Z!m1wFjGI4o14qv!Gqbqe?Rx``OMqK0lyV@ugSo-f&PFKXazb={ZJSf9YbaV zJGDYuS{hYVRm8Y?6woSzkkI`a$)-P>6XO+ z7&~?>n>TMJGc(gj^*GUEelEtaI^e%RCGZDeD)5-)#wZx+A-#bsK#FE4E-ofFH#h7b zZ)xVRKK40 z<(FS_{P=P2*4Ws{wQJWx9WEPKDz-*o9q=yD5o*U!J;VVtixD4yjT<)xoQT~oE?&Gy zU0oeVj~=D2u8#BP&l}mT30MjIT`VDV2_;A_a8xTKCMHr{T}`iEz09<~$8o#e96WfC znwlD_tE;(n>sGMpF5qS0s@|8O1X&KO&}(1O3Ya%<9ss9LpH_uQ6aJhn z>bMfDhYSYJYK54X820VkM_O8%{r{qexpL(S%a$#prlv;yYAx_<*%0h@;d6J%&CPu% z#Q^Brw=WwuY@nc^K>g}hz>~5em>~IbVd~VWcDC`p6B`@Ls#UAV&d&BRi$$xKK!PNQ zRj8K<6D9x0b9Ooj{@(o^lcuRQc17q3k_3#E z4P(cS)v5OIl9G}_R#uj5iUURvK#=bN@me7}JDZG*M=$u+zkh$#lt}qlH*WN^-@JJ< z;68o&^r5Jzh!G=3*w&7wrY6HV%=YVyXHof+}uoKqfgw* z18kQKJuXN|Nr~#=2~0vy%@${7^Oj zC0L+H!Dkr}?q$4t_iokXcP(pzI7HIQR|SWI^z?MKu4$^PtGRytx@`KNaQz!0LBy54 zJpJ_3mfLdt_;JpiJ13jkMf}$XO^`&nw>X_nTL!R-iVD@V1u&EpBAndbla!vG9@a#0 z*REZvsaz!g`JnylWZ9UQn5fn>O?!Jg`}X+_72EY|>oQL+gcD@%>goz~a{SPKAxSpc zA;_^~$JFU@vosM4MZ8|tfmN!is?;pVR-jC;t|mykY`k;lj^!WMsI9G4O_e5+>L!cn zJcv|@aNH0deBd`!Y*qiWh9<}rdE{?tX;Ei$ra683v}!VwdikPUm^yVTv9Z=3+_-r0 zqMG>#@Yg`~H9TxOP4NHd)HaOukY&c zqC5rY+qbXf$=HDd2XMRHvguB!%LN@5;-3Jox3{yilUHATl`UJgguHxOTU%MNVg*Z< zETN;rCn={>Tn0w&<-d>%GiT1UJQ;iceZMPX8*p8}_SfiT5X}TiRk3n%a+ot`4&%p< z=kdoMCq6!&mX;Pyo;-=m<)WgZLND3P11thI8~6_+0k7xR;c&2f_ii=hXo%+KW@gNo zq267-tKK;D&>OAFfZqXsmW`)QoznAE2crY{r9m+esoP%0j2UA|48%lC5#tjhVh|+y zzgnaSgzRDg_>Tx_7!@NP_?c|b66Va(+1bg?ojX;tpF=kg!7k8`MAChUkqltLW$|e0 zQ*wT*0cHYywL(%-5_x%fCU*at0P7aRi^@KR-Wk z%^>u34V6P*x4 zv3||aL{bnRzsKasll8o8A-Qnj0w+)UJcM~Koc9StN{|>apn91yWlG@MK~T~x0QLiy z!f99}7$>h?l9Q7yhZ#pI%(1*;^X53*ABv`pZa+w%`7VYh?F2c zPa{p7I5BX|5Zt|cS2cx|su&4DQsjpV91aK1J@=gDgX!e1TATX|A|Xhw>~vBf+ae-}PhM0wi8-yUt$NO6qXBhYB6Z1pUGP7{hI#9)x6}*+w+LigCK7`9 zgwjGhmL8B@yLRaX!~Q7VxM!J22;%d2c|${k=}d!)iVBu5U#^<#fxm^@I6GesX_PO3 zC7PG)=FOWVBqSJ$(7D}iR;^max^?SR5pRjP&6RK)7b&0WCN&OISXjt{1q*y`qaKfk z=H_M&9Xdo=SsC^9^?DJ+F5s0=LF@;eK0u4!s{xafle-5G9^5@*#E5RE(;4bDf_H&A zk^VDwF-0Wzn|Z;cyd4(d$w!JTvGy=3f=9eR@s$T8gwiAQ60U%E16)RKV2_A5`(@$S z&4Z|Nx)@^;#oMM@BHf@UQKCeN5+zEMC{dzBi4rABlt-EW0~KSh5fL~fGynhq07*qo IM6N<$g54AmHUIzs literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-8@2x.png b/osu.Game.Tests/Resources/special-skin/score-8@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5726c519ae9776ee5b1c1bfc829fcd18e26a6d41 GIT binary patch literal 3714 zcmV-|4t?>7P)-mefsqI=+pi6_kDdJK!5-N0t5))CYrl&&sbob_>Bd0;-?kodw>q$ zBJcxnZYUb%Per5vxj+u^m%teD3w5)eE5M(C!@wcnlAqe^Q<8u(;C}(LAATmF9w-K) z?)Jl~F&C%;2K=M|wcj=1_u>`v8gE~X`+}fL zTJ5hxM0a;LdcB^`&Q4leTlwXeUk0nP4|oUoSKzv*58{m?{vP;)qb{n|YMC=<4w;#m zq@|^ikdQ!3Obnr+t`89=lZm#rHfm~WsIIQ&QzU@YJjJK3+~kIt|GF4FU06? zLuzU&#l^+Uo;};mD&42Qzn{-P|D2sWchcF}sa8Y}tOdSuwN7^wu@v}Ez~*=$yx&H=&(Q^sJ5$N>%jAr?VUP!O-Y@(Ry9^UPi8 z1&+{YG|ZYci}dt#jvqfxe}BL7bdE6D8ysHmr0yxee* z*t~hOzuN3KCQqJ>UazO6r9~;u6zd|JRjk^paS(XSau5>}!+`?_h>eZ)XPf;-Z*MOv zR;=Lc*|SRVG2pM2f*|R-T%{5KHf`E;&nf{REG&$T8#l`0MdD{gR>VIk4`$As$>PO} z{nf@HWA5C!WM^l~;!@>-^3@m*oK=n+AAa~DX=!PGXDWo0EzO--@|Xh&RJ9M3)X9QpbAJ~7_bty{@5GUgh=IUuVOH4L)f@ety0z`i*6FR4QVgvV^ae(*pwoY~8w* z!oos+{`qH*>iFV|FFd|hNmf=CF)=YpQ7n*#LJ`5jr2h3FIGB`_6t8b=Hk;YJc{3Fi z6>2@e4Ez`H3h)S!3`_?8S@=F}@WtXTC@6@Gj0{;k1BKBPp&T#5!^7QjsX1cj&Ye_M zRmtK3@ots@|0iVeKpyZUuw0(a^78T+GseyrZ`AM-5v|0Mvan15lB74iZ7vqW8xN$=kT|=RWE)jDKM|A5eSFU*VUTU>kwr}4~Sy>s4jg3yrhK7bRZQ3-lv$L5u zZyqye&h+@cgK_ibO<80_p$M~ZSmapFe*gXVEM2QHqL+ z)FMgu6qS{g==FM~=!S^bfQljjo&)|zDX6Kb;pwNJ_Q}HMZqV7;NqKp>EZz-R=TbH2 z?QP(QaM|fBLW9A;XP-9YQ?6X|Caz!aN0ILCO@`WRJ{&g{ngsCR+uM2Y!3PNm32|`w@Sv`)j>5u1E?&GSpIRr(1uKJDF%iJq7B7yHIE*Iwh;v14kd-xtqg;};k#_IVrlTo@gT6e)lh3CMwZT%2JwS#WL_K`?@UWXrcJ125rAu}hhMSm1$T(6IaI+Sb0*y)2nh+{?YG}Xr_(vV@-C2)lES`y`;<|wD3Skl)X_DrDq;)p zw@N`lK>nnZGP zGQq*Fc2>uH`|YmRCy9>-Bu}(MKFPa>P;A zETcz{X5qqxJoVI5%$PC5<9p@i=8}|@q+B&?fhA()X@fJhFBBQ?R@Sdy&!R<(9ADuC zlgY$;@4d%MFTF&4eZ5or({0G`t|D_DdCndfe(q?j4H*bZe1NVG&Hb(|9-XW2PGvX zMGh4tsy(-(9`TRLgS@;vrca;lQA4(E+a@PxARcxT@SzASDG3%K!QY9TNB~ZsKJC%{ z!@|PQ>2!2;by-9sg^$H%I#E-^STUejDJUrLs1cVhU#6m>LKe4*CB6Dc`RnTNm{5qd z8z~;dZN`ruFJC{&6-6vj4x-biPxrVt96WeX4hM7!ub1BORaRyx52OSSw@?{d>?R2{ zMc8;+9)9>?j~gH*dLs5-4W@)3u`R+T!!f~DJhBU>}=-DnWHwoctk^k-KL%I)XELE=<*psK|$o^=33qVCX6PM>@6%Qg~y{4F@VKE9I1i zW)YUKLsrC($^gi@bLZ&kk(UDZ2G_4&XYbyU`bCA>o~O z-jVGGqcR^tM$MqI+4%LcUn?y|}6Q!l4&c53)Y%rV6l$4anN&r~zObIyg zSzZy5V-RV1y}iB6ojcb-@!`SNty|SziT`lrivf-l@so&gT21ce=4N_&dhXhn0i0nn znJ6nOW6vJDv9K9fDbfO*;z$u@F=(y^tUE@Uo0~a*{yaH3Im0HnbN>8!UVQOIzW&`5a2X%}{i zjErQ>nl&t6zMS#n$GdZHx43!pCP$ARW$)g-YKi3*J_DY26zFw^JEPu6;NL`Qo_$3c z4H+33JpTCOIx#fx&D^k58#<@E1??>&45 zpAsoPxKSfA*8Kf_x>n%{A71%N5=mk+YqfCG`{Kd-faAc+z+d^Z@%}Oq!+>nyAu-dW z2rJc@N335gj#@=xYpv+^Lz2-tl(urHh?yjd_(r7ISf&@AW;XV9+$z?Z;3fjZzM zaPvOaQ7Z$08Nly=e*m4Lp`QF~z!_jI@SO+UAa@7?(tt9c)r$uA_B#Xo3Ht+b9YU?flkJl|6!lgUIvLINX4j%3)dVZ_D75gHmwNJt1lK|$DT zHrm_Usjsi+%9ShB)zwj3TZ_$R>nYo7LJ>Q>t-P-am=CNM16emL77LRnPbMubjpXEH z1`HVBQRYoeO;lD^vUBH74j(?;lQPPHp94*vHo#j21OtB%3UIbm zK|y*ZnwvLovTfTowrtr#OG}GUlzL#g7;xRjo7xQk{tRTP0TU-qI38e`+yt0<>Rgbg2X^GLl!0`CQ@8nOl)kdr}fu@*=!~?HI?x2aE>28j@|B9P!bHx z1pd_9!04_5ehvIo7K|A)hK(CHGH~EPZ|kp(q@*OGqM|r_*y+&^6RXF~(o(w{?kHd( zutpZd$H%jA<3_^5!hET>Rz{B=OXq2X>S)iZLT#`c&5)#7Z&6|1Zsi$<-TPqC> z4dmqH(A4Dg=zjuCHHzO;5BMc8OBSqJwTg)oC%R7Y5D*d)!r;M!IdI^BEKU&9;&nB? z$wfXEAWs(LJ>JMXXvU8p&$G`yD~n9PVl|$T0^Ss>DG*cM%$YO&RZqY2 z(n~L?qSCp6S)rydJTk_CN~G`aHLJ|-rHDO0BS zt%7#Dos%a|a_G<@Zr!@Yh!G>m&CO-VkRe*~4-O6{Jw2VBJ9o|m2|CIl*tTsOqehL= zQV|FD?%iYc>eX!DzTIUUi^am;y?eDNzon&xl#~?Nnq&h83EP3w7%*CHd_+V9Pe1*% z&lONB^V(~#_1JiT>({Sy;lc$iWrl==s0JDnFhRa>FjkH=cI;TK-T(IO+r0ep%T!iY zszEm3kKz!di?6@_T3gx4$;qmytF0~-p!4qAY&MoJUv6Z*xd4m<<^i9}0S4wSuQ4#u zecSPmJP11NaCB~Xii(Oja^#3AJ|+w-j$K-u9H`UIFnsuMRrG}8A6Wq-Q$-KxV#bUa=tMD& zAk1VSDZpu~*RP+?XO7tIc8(o8ri#}o*J0xLyo*VbCaJqPpK(V4?Q&>)d%K#zV;UM7 zXl!&c!!?PKU* zuRalrFokYC!0EKHxw%ry`}XyzOjlgJdQ}y58As*$;ZiS& zJi=r$xqVo=Xwf1@j~?wtOz*gI<%%k*bNu5{0hcdd_PJnpp6LY(7O-g1BA?6ZiO)X! zOciw*A>?y`1li3cJUrZIYfx}-FtcXOA~7+s>lm%0ql1Wu2vSl~C@d@_H#b+C6NM2C z4Gp~i{`<114fus)K$iCcC*)Yl3eIc#^yx!JMg|!f8Cvr5l$x3vRdiOK_T@*QN)EVo z?V8Wyz{9|)Q>Rpslifj9z+JIR1F@3ZvBPWUtA_>y1I*v#3w4*UMZPF6FE^S)9~l}N z8n|@HX>9KR4$1;m0UrQewx~OI?y!3GYJb<$Z&X%RstdYPs>g_`fL7HRV&A@f>L~ii zaQygjRrFU?V01iQCEQ-S%qUe=RamW7CQO*%*C|UUR;!h_-+o)QEv^uIk}iy_4jsT; zz-cX9TwF|kem-~a-Ze<_@Nx9$QSRKiBa3bT-MWM;v&$u6uylG}cJJQJ+_`hLtr7gm zo;`b1QH5~XccQocJVyB2IXMIc1qCr}+BD|RpHE_9qK^7%rKP2Xw6rwZ+S+7s3eas% zbmxJ7Lb%2z83o40#W8vEWTsA?N^)}YLor5t_~C~X78c5)D`KhNZIAD6Z3h6q6$VY0 zX(A&ddFGjCNJ>f~DJh9jqeiJ);`_p!IdjxQnAKtc?1rbe>pAgmfZIkpi^W1xQW9BN zS)`?<(Wj4kAg!0$+FD+G@kLqGDeiu~QG9RuYY>nr{PUleeF%FaIy#zn-+kA}fk_)H zR;*B$@Vfz*-d(;vu#sZTkS&za?F2%_Y&KI~UhbE{^XAQ)WMyTkbN>urm#Z|so<1}J zTZLQAAYc^mW8n9|Y1P^c(An8ZadEMa#TrvqR;D)ob76*aiOvVo2AmhaQXvZ#Q|T+R z1=t_k?53tBDk>^e@vn^Lei?szKiw1VwqMG^=;&ze1#Q~2iPl#6xREb~r+AOt5BIO{ z15RF+x?iqnY;0uf)~%{|op=+qNBp4xi?Ay+3H#e$G%$X{Mwu8P+v$L}uD-2?J_YJ6c~q4w^-X3ZM)DDMj~ z3%P~v9*}7yjE|4!t+(FNz5qIO=n$WLqMo+1N<4aQp?l|YOLn!}zkfd_lZl~2hiWbc zT3cJm&d%n>4W}2wDuM4Rhp;`Ovj;pTyPlZMW`+(O>Nii*qM{C>l;roHz7k1aJiYv|` ztOU==7j<=Yu4B51)oNww(xs~RxLv&1)y?z4Q*BU`N70}Vq!P2EmIdkTW8ni~d z((5z272q@v8=TF%jm>5wKR;i6BOGAAD+{kT=vKf{c_mg;Q{&Y*(B9roK|ui@f86aO z5NClO_Gabr1U*Ca|F{EW$ljmr?d_zcrMaC!b8|C!d3hW>cu)~d$b*~rVwqpGT^$3(k! z?IJffSM}&X-1!Xg(WFO)RIw=P`uvs2WU^16KHXkcR%ZY7(@*WSwYB!@>T3Ij4IAuZ z#*A_G$*d#7-2O;X=;~Ww0|ySY$Hm3jLqkJ*`d-)vI=&V5pjo2!1UG)I!1DWKvA!>) z3*S6c3M36W`EETRda>G9tJG!xM`LXVw`@S+Q&LtY7$f?(b(2Pes1w$KWwzl&M@_bAH}B-#>-N6;cZlHkfd_oP&-1)~ ze4fYW{R4s^2!bF8f*=TjAP9mW2!bF8f*=TjAP9mW2!bFKn4$t+s}6V$umBgp?|@JF z`9Grrm;@dGS1HOFa36TNz>{If^T1cYWg4+mQjMuiW7H`RgNZf9m@hNh+_Lpgxv=4R^a>sei0RmXL}LtrZ(!P~$*U`hlI zhXa?(MQLfNp_m$*&4%CaXK!yW$z3_7uF{P&`tv7&cN3M`+FBMC7O1SO%t|gnAP}It zyPJ!Pq_g`D@I$&WrgRNJ1#nLtIGxU%rvN1Rtm?hlhvxdRuYpwd;$EE zs04#SMn^}vyu8d=E)t30@p!cHZ@CIS1U^y+8yg#pjEoQpg>n*HTU#R%iKrtlaFDAY zz^A}3>R@GMh5r8jRGVFwtE(&AZnrl6UT;h$f>GdgWj3$zdc7PS9c3lBxVXs9&W<{Y z1Jn5m0{j7doixy7qN1WAV>!Ry&-nPbHvU#w;=Bdl14h(=-EPlVFdPmuI5^1Z>8U#Y z8+hMn>@9=#$CV{`TOC$aRpE3xGnN||7+`<@#!2#l@?x)x(F}g9l{1^oOioT_^zh*G z`RMEGV|)9?yU~}x=Y|(Bc%rr_d#@xBl$V#|a=GZ}=+M1rG#X`YZjR~cY3-4M@&bQH zSz<%@pzn) zk`iLE7@<&zKp?>G?k*mWhpnwGy}QhP;GIH#OuC)p=M8Q73wWzgQd;Ko5IYAx1D<+R zascJPH(84P3XB3bo=S5e({D4cD5V2vP{c2RfKt55{d!aRa486a@PFZ7+!beE>)ugf P00000NkvXXu0mjf6h$Ne literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-dot@2x.png b/osu.Game.Tests/Resources/special-skin/score-dot@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..58f38a22b5a0ada67b88f6b5c1d66a534a84d73a GIT binary patch literal 930 zcmV;T16}-yP)uK@W>8Vxfiwn}S|Cco8z~A`x`#R(9~V27%#0D9l56 zF$6nAE?9^t=%JFr(1-|DBGF9(!!5RDz}*dJy3ES#=+1r$9sUo@;s2ZWo%dmw_hUd1 z1VIo4K@bE%5ClOG1VIo4K@bE%5ClOG1VIplJX4UT=ePhTa0)m9>;WO*2e6Z;F*3pq zTnBu>Pe8M{z5~<16`=6A#R`C1Kp=%wvTGTrJ1#i~@H|T~%X0s8EMhS&N1Ofr}_V$kI;s-7Ok$ecA1l|A_jbT|?8NI!|^z`)LcDpGo zJp6g7s>=HMI^*NxEG;dWZ+n3U`4AihZX1J^mKH`vMyRf?PNO?$n#SPZATu*F#`u>$ z`;}Y-J-~b8>}zjtXKZW?yWO6aTpUGF=2%E7Rm?0C=_KF_=;Tq%^hQW( z1_^kG6ZnEc^U>p!mXFHvDn007>cOLVP!n|^l7I;8`^TN^yY|{^!`^ExuyMxzjlod9 zx%Zrt^{q7@-~7IBfddB)95`^`z<~n?|MocD?hgFU=?jblf`M*8An+S-1-Jkl7C*JO zyQ8+^1o#0ix4R>D&=1G}$^fGcKXt%Iz$iT$*W(HC0!9L1K!5SmO(?=o^0_9U3OEn^ z3>*iJ0sDU^cUTCp9GIoy2FeB&0XKE-TdQ;cVuAO78XNBkd3F!pP2NnSSY8!2Vav&4P0j{?}{c@av)j*MZgh^x4Z7Fzp!aBwgY5fSw2)r(%edeN<0H~jqk@b&e@$?5;UQdwC^d3iZyWo4W= zae{*f4|4we`Ig$=2Fw-1qMkU3o9h{=(96qC>mn|CU5x zotlodgsT|eC(8@`{QQ_PV+Io^PNY|_UiP+Y;?${A?AWn`ojZ3@Q=@p@j{s4?Rn?7{ z<0OW~IH@owD2U9=O#1cfr>fFz+qRLKnkp3)iFwiP&RN3%Y?eRL?(Xiy#l;aF9gU}_ zr@if2rMkMB{QP|K^73eCXpk4503HM`>d@3mAVn(Z+O;bM1qB2J1*xv|*s){Go;_PC z`V<&%Z#^wNmnm{(1P2GRb?a81dFB~il>yM9LkAKP5-2V%=I*=imKXO2iUBu`&CHf7 zBOoAvyu3V3$^f`_?V4PqYAJ6`A>qI>si0@io^08&h5r5ft7*q(`t<2TK|ui{MvRab zjR4kbsCojhQ7Z8B^W)7o-y}FVSVOh{$HyOkEEj#JrXD4QxB&Um%~6*wUC7PN#oxaz zR=SpWd3mv6!v;o=9xX3=PFz@f83=5b?sMGT-Ff4UH#E(E&HVJ!Pwd^hS1$fkODslcn)+4a;^M-qufD2u!Ctj<wRn)>7laR!!31yiO>VbmyH%rti6?d?rkTAF+bI#`%DY@q}2 zKF~udOioT_{CKA3OIiJxKv~Wo)`bxja(sPg@qFUS65e_d+s^4ebt4~&`|#H zhd;>0amt@I!v%N;xJN2{;)y4C^2sN4>04P@85=fikc&41pQx=mKHa=|lcc01Zr(JrBVMs)hOrf?LY#m< zN((1Vnxv&}-3SZ}Br-BmF19dvNcam7BNYr7FhFVeQ%zc08f9f=a`Akj6xB$3*IyjK zfY=v=hlgvaTUVl@qU55nRzCV)z<)~x-Me=uJ3Cvqz0=1Ze@t<4v0PjL6l$$ost~hV zaLAA$xVZd&&Xpn}BIKgM3PbJ?;4P^IqGQL7H_@Z2V5{TEMe(4`{ zue4y`z=3+yuNwgY0rHF50q7(B2(U{Wm;vDC=0-+F20eTB)TIwKH8m_;xRBp|`%Nmo z2}~D9yPAgvWA8RmMvSx z`|rPRCinF8BrYzFr=NaWeNRDY*y$wxeM1~0o0vIsCR3(N(bS%lf`S4i^T=kvV%@6+ z>5ye+UF+VxyWN!)6ckt}1E99HmNjeEP*|vK&hzo{k&8Ntv3ssmIDGhUl9Q7)wI$`~ z(WAWf+G}$0cfhl{H6mzMNC&`lH0J5)iHC=W-Ie|GpZ{#JJS!`U#>Phbdnxep@{)_9 z0JAF<930G=HEZPi7S)_Tf1bp|M0vj}fvIiboYAZhv$3F4r%uYBZiQjPhP7B;Sy{=s zbLUjmjo6IIm?Rx`eSCb#%*>=?$BwGokWy1qLwtNZm6erJaTD+a(DL1P<^aKWM9lE>^CK%Oi$jMFv2Wi#N=r+rs;VL=D2U|bWKB4xAC#x3C&!K*Ys+@+=H|_tgolUA&m9O2 z4Q2D@&8@oY*viS1Cz&~OCbhL@^YtZhiux~I8fk&%+qGJ{)2ywn<va*tdgao-V>cxtx&6R;{AM%Z~ptQ8KJ=(XK0|yS6{qE-G#`^W^ zx#NyIbg93gp@Bt<7IERi1$j}N@NjPzqzXAAE%@XU)n_caV>BAszkk1(Z0XXa+;h)8 zR=;B}si~=yl$6ND8N#8i-H<9|x3pmY{{8ax_if|Afdgjym&YD^jHy$n>Q;YIQ4#OF z^Nw8nsn)|LHAofmm2|UHS64@FZf<+`#{#3#Xx5L=&`_2xUE1Pz?BU3fBc!FJ$;GF| zxr&9yURxu#FEokWFNleB#E20D1_oMwvz^$rYZpaDMIg>L^78WV^Rvs}+bU%dl@WuC3P3(Y?G-sBqW5w!a{s~eJ#J;#$35_ zg-0KKl&Y#K38TP7yQs98_`}$sM zzm$A?qeRe&sVp)wlB}#O+}yN!a@tNqLjy4}F{X(&K0cm5{pnA-)P3vLE#}Uht2BvU z5O;hxl~tDlsX{qTHhkU!rET;3B4EbEkt0W#KYzZGaj#7<8jWVFo-t#_5E~n-Tm36m ztWZ)0VrWvU48*bW+rl(|znTiHdB3{@`{aR)0|pErGc%KbfB;p!wFRTmNP2oYn>TL; zphu4$y#4mux(%7Rxw*Xl`s;RmConJ&e}8{`eSL9ucBZPTiVGJmw6+Ru5@8R?a??~x zv=oleSp;O79j^QO`m%WOVx~@=Y8H&62X%FIEMLBy4?p}6g#YcfZQE4GY{=QSZy$+? zi55G4IX*r^;^@(%?A^PU&p!Li+F8$GvD3H7^wHWC?E>tT zd;b9p9XgbyOP6XodzbRfH{Y;o)haV{cWP>?lGmate){Pro_gvj>gwvOe&)XW?qkZ7 zDNL9!!R`aPbLY;Hlas@H@4crKbNI7}cW*Vm#g<##2iOW&Tp1@PCq|DRO-xJ-Lxv2o zr~f7@Dk@mFZXF+d@PXMMGHu#4Qc_Y{{I0G1`s=SmM@Mt{@@2*H!Gj00c=2Lg93o0e zN?5jR8Rg~W@}d*KaKIvvM2f9rjzQRTUKW;N3qnIfnJ{4jW5rD6DKUn0s{ki;e{6%KYqNf!|(O$*GW!J z=I~()6WB*$6>Iez+umMu7hV=Ejw3-qK@1!?kkHUjdiLyTIt!PE6Mz5x_ezg8etv!w z7Z>YdG;C^WV*dR396Wf?qU@oE9^%CpU!+3^#rsP$w{G1cDJe1^7x$xPLk zDy{(r3G<8vYLD+e6n;>nMKFx2O^}?qbLZ-&jH03<3uPD#29_^hPE1Tpn=1p!5rNFq z;|1hedlODu-OMT-_ObLLFV z_1lA&mlpv60rKOFJ4JFE3sXy5+$L_~ksK!cg*yp{HiI}OzAW;jnA-OD_h-kB9dzl^ zMRVgPPo8AftXUQdg^3d17tWf{au!xXrw=e$u- zQ9*Qcw8inWpPwIl_UzHkO00(S=g%`~(j@ujaV%iA;?O60#diK8kD`r@jnzdNb#-+t zSg=6R7hukuIk#0A0NuKEQ;KU`C>OM+LLLBCNCn~H;mn&iPjkghO-&>wCi24%KUkD` zdwVl&+BD5==)tU67O|>P(jY0?Q6T|h@;5Ca{Qdn&Pfyn+#BBBI)f_sc9EJSggAeMS zbwVwJ2M;DRG*l{b7A6%F?V%6@umvzhk2bq~>M@CL*|LSLTen&*zyE$!R`51M!K>43 z2yc&=wIxdPh8`E{;K75Wr>D30nXs@h)%9PvaDjq?0!m9uF>=@86%1BS$i3%ox?) z^^uX0a<53UC#0=cS;K%YrOw>p;o;=v(;o{<=6!U2EKyOQhq)UVQ zot&IlwQ7~-d84MLhPb#mDk>`EMG4~U#)R5orJ|z3+7Yh_qtR&h$zwALhSN4z$V8Dd z(Zu7AKhBsjV>DIT)YL?BajO|-U9Dt^{v3E+ngaIX#fz$Zpxav^ z{{j3>IzNSkgy<6Um!6)kw7lIVlD!}t#!W|pb#-;}Va+B&LPC^=l~E!atJxT+ko~Sz z6e5yM$CGWLkQ`|Msk5^)si~=&_X355g>2cfMJ_H8Id`SjX0uG73eMB5;^N}Mnl)=$ zJuC6|_g9q*Mj^MP>8{aRAy3K^*u=)hYR*gb`RAYW$}1LuOaBrFWjADUv++zJB+VxJ z_wUbN|N7S!O)k@>O;fc%Q8?Z;jeB}3Bt*Eqm&HVQ%JU(n;U)l^x=gUUQliJQJ4dNZvKy6`w)Zh7B>a? zd3boRefxGzX)Uf?xk7YwG#4+LUH3-uWPE7lYkw4}Z2{=ryEnUb?NZ$by*YjQG><;| zs8n=AL_yc1;}a4mRR%y(Qj#WR)YsRuaN$C^G7uJ0Ysx_Ql9?RS%F4<(d-kmAe(25L z|85b$d!Shv=%kP!c{-$#BS&hsu&!FQitoO&NFkCT(rC2Ab+JS>p=JZu7Bn<8@b0_s z%Ee}nuO13nCrvQvEY^$NyLZbs zwAI2F*o0Pvj0emfg#`;1X!89nEiGlunl*CqNs+eO*1Q}$q(Q6|6%|TDMY~d4TdNdm z{gynflU9Y8B`^#N3)3`IH8eD^V#NyiimFmf;w^go$+#{yrzSRT+^A|3rx)4T+4A|Y zPMDX>(4vsOQq!@MlM^W^DVhw2`T6PW*r|rflEW&0My|ruCl9`#Q=z*_= zPd@pC>C>kx88yEYiO*WRSZWpWn>2%SZEbC9?~-bfBS(%C>k< zdh{rV4Bx2AqI5$%F^axIb#--ARaJ5I>Q$~^zi!Kr_Ahbn zKHt_bXgOK~c771%cT@OkeSJM&eDMW^g@s(ce3{zXT8u^`jg5^oHa2qW)-7CJUCGPK z@#>}9|Hd0DJSQx(YTp>GPjI1r7=%kxZxIpJ%ii|@ z{~qWp>__MAP(}ry&4cdWh_-UJb`kH-jTA4w?;}nXJ;a^YQFs9E5y@G8X`_BPuo2hW z_%eG34jede;J|?c2M!!KaNxj!0|yQq=)nI1u7<(si)(nL00000NkvXXu0mjfNcn{c literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-pp@2x.png b/osu.Game.Tests/Resources/special-skin/score-pp@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e8616e72841c2590d510fd35d7257662cec508ab GIT binary patch literal 4777 zcmb7{XEz%T1I1%+O3f;&X3bi$YZR?rqgD|#_Fl1StF2}zvA38FYKu+O-doH@QCnlR z&-s6Z=bm%l-Sc~Q?>RT_oz5Ey(kG+<0DwaMt%^PX0PqI@01m{2002M+A+8Pp07yLE zntB5OEHVEXCwcG8761S+`KX%s7`WT{_*r||0{r~^gdE+Ry+2xe*b2FO*~5OzJplk{ zfa)qrhW@$7c|ksg^MO5~>5pRYm3Qgz`wX7(y;Ii1rPFifwN!Gpg~s5LxzOQ&Vl^uV zBQnd?RU3qyh2xr(%tVb?&~>M%JvUQe zi1WC{M9ZJ-M&z>nN#o?k<{#IW|A%5-v}%3UH+o*8A#;w1HKZk z(JL~sC<-;OVcpRH5BabQ`Dwj1Fy3HANeXO0`?_)eqB{A+iE52xjZBU>ks6tf%pmT9 z;F|XAJ-hT$szd+n(MQ*Z;e>>|;qai5BHzVpR%9p?eyOs?iITadr|Y@vISD@pOePV~ z;~0b~kQLHQkUZ!B<`w0pG%=DYpR(Er+ACX0U2r#WLZ%L6m1t*3#Z^ZtSV!>Pt__e< zXcJ~$zh?!)s(6kfD@ML1J|*lt+}zRD{m~-+r&jHdV~xA(<2=X3I*XFd&fN2JFCp_* zMMuXMlrjCQi%`5!o_0z1dyWQuVU|xK4pYzEI*x&X*GKBZUs&?IV)~V|wJGQm2oeUB z)zwKL9qX&c5`g*ySewX{m$lY3aaE)#8#8bsZhM-I3JN@^VqTF~;rf?R!3SnPwJtD~4@kV#01RUw&?30gjNs zaSqQ4m!Se($$MlOc&5jN;}MBk^#Ib+(lBdnVgOH1&$+p|cjo3=o}MBE1O(#1+rJj+ zX=#koUYvgGz6^ew!IJWUm#3P@=xD<8^YbNSs6q$ixVWZf%wXXpY}saQM{=BQO*hd9 z2Qydo`0mdM4>$MFMK*5ryoQbr<;~5_e|Qi`S6La?qTQ#+eY5-WWV0X~kLcE-Xrt9@ zk976B>rN!qt3m`qAG(*wH~{6FIS-f3p<1X1P3HM+99-l;I`Y=m%yshR)NO6KfUqP= z7MZCsO@_d$U#iHEht{iw4+ksF&M7Mopu<-_m`8^TwH!P=Z=hwA*bfSU{5<1yPbtjt zWyE=o);qEW1_lbrSV)f`vTPw(_7mj4Zn!TU-eX$7+k!Bp<17D%tHo?lTMd)ZRhsLg z<^EA9e^r%dP-Hvd7!96Cfl3^5F`TWL#C{@oy1J(QBM@fVV1)w%y4sASuzPtapbHtr zoje_BTul|inwrEB2*g*p07S0O;|8Tf*-tcOH=BDzQbE@>tb61yUPqH#?+hi$T~59B z(xh0GwoemeC|#(>=TyXzdV9ZLjd4;Kb${Vk;<%59f+}de_3G%-4E2Q1! zd7K_nPtCgmB@Fk-nMg=Tz+kYIXq#4WGHyP;i5>Re5ISg2b0 zRr%D4z>60z;2rk`&GNK1X5ut)2?-yki&YU=5vv-34F07ul$sL(UgzbE=K0aGpbd0P zo%8zLxK@sMoL-3V*aOAwY-G>YWyk9egSF{5)z*gpV%w>PBzy3HiKDbIV=>? zQ3HugV&F5M<8YU0bg?I|WxA7-6BfIZEbaS?)~K2eHXc20#~~w=v(+DSf!#?K^W0Vm zpk1>m*UIkO=n8^57ERR`6@?3TVNjt-@?w6x25J9KPJ%dLAE^pRpR zkx7iOak=Kx8pfg*2ur%T_?ZraRci?g2<+zhZCL-v6s$38j^FB!AqpOEZIx0>W*zU1 zBx>G9jnf@9CoEhLRo2znZS{Q~9feAN!oF;@8}GAgycs(rYh2cHa)OI$pD<(hvOw1+ zZDs=WWZwH@TLW>A+s*y%v1bU3-}t`~x})N6`3gNN&CYo6hQ2=KOd$)nCeu(7b4A8s z)n+&z5%zb-$^C?T%c^OU12yvRuOC*klB5s0C%V>5UzAEv-RzmK_RHIaRN%BDUVZURs0@D9wvS#Pg?&1=&8@|X*nSmT)1 zOdRYcuSD()%S#acJD96VhY3X8-u&`zaauQk!{HIX%js@?xy>U5KHH)R*eaebK(^=; zxG;{hSggNfwxytn2+eibriAs+qune}$XV_x747Z1R%^S%VEdt}uMQ8S*0r&J>l93M zED0xFI3zT}_@R<#Cmg9FBwoG4WGViaq$>2$O_}obw5+evr4yW+v>oCx93rwlC}w{0 z1e~hyLrSRR$SNrPaB-dJn9o#m>NV2pN3j$#)#H7`^3lHOvSr>ZL9Ib6B7p)Q8mdQp zbukPTpSzVnS0^kSGSy&lJ_(k6&55}ge=8<|<9&nZ&~3J)6IkXi@x?ihNC2fcWv~&G zR&?@5RTKD~Y*^4=U3iYxuE@%h)#bB@myZ~q@CowYVb3P6pBkCFf!?70EfzYhKR^C%C<$^F9GIIWK_2kYn7H&eYr(5u06GdKg&r#yVpwm{* zon5wP@A3Kd&Jurk85`0j%bmY{w>bIZhrt*9s~KSt5xr}#L~3dR!!>U5`8>JnrH@J$ zn_C_!Sy?;@-Xz8h>m_ff=8G@igmtD!8{o5fidy&KBfIwG&vud(`1S&Y82ALF7~+!` z5<+-SlZXlN6c_6(0#225|FnnZDui0Hg5SqUKb#6;h0R;vL2}zwv1j?TbtxU;$<1~u zEV2+VDve7Cbhq^Fqeff)t^6&f(Z)G>-5JpvZ5 zlFDJ)71#_qIhw7|wO>ZqDuEh{iz5swbR1Q+d-nGu8+@l@Z_tAY42>ZVf!tbO6+0lm zJxBBn43;|syr(A1>^cR@WBOdK|K6S0A1>66W<%X*%q;1aF<9V4)#t<@ zINRoJKlFv=$I>@99nba#$g!EJk7*Qp)Ju##5T>rK&d$Zf#kq;J?k5Z&{r&_SY4m78 zNGA8w)x0s>!~nU-s)7iCLZLMuT6E}nu79>(%Pv0=90gLV<@1wV!VGvJn2z zm#J}B=GM~EC^~+lk?ozH82G)UAw%%?0EtnZ4;e44TExItk9Vfsp?ZF_?=#t+BPtMm zV!NKxW^Lx7q=Yl6@UPP)*py6bUPUPO>1WP5%dS!HN%;yyZ-^45O~qccLvMv{!9I$o zV0VU#;o}gu6>DJRbN_i)@O8%~~yM?{&Dnp%XgaiG}<`nSC`7mF#1kB=W28_VdrhAndq(d9p1 z?U;faK*Q6rvW7cOx_=bCjxj_TtBT9EpiC)TZwc$*@a>Z1SKEz~uMC#WIPA9b$-pPk zz)e+92P7$yi0b^Hq5w|+9fl%e{R7OQZIXTfczJs-H#t6*g&dvIt{l)bC|bTfjR7vJ z8XFsX-(x+$u*#3-%Liou$0H7H0^O?EVBXV4ft9znfjaqesBa&Km%4&l>X+s>E@?A0J=Cvdi4!qW9UhssKIN;Nak2nVe%fnE8!Uvzc;TLH|}^ zWo0Y^AquaquBxiq8in$kcYn%`k0;g8(BS3gzp&@YdXA<$-VWA0T~R$$4+xO1HmbGR z><;c#`?@NqhU)m>;WpCl(6K(z?t8e@U>$jM%*IRbJad?BcO+7fT|lt4uI`Jp_dfpP zunromxrt4C*D;aeh}mg89skJasMEoLarki$kIlShOT_202*2NJOb&a%*rb~4ggWep zJYXrgI$2$1rPbZ_ad!5u3!I8AYd@*YKT} z&caYn^SkFoWQxjcZB>+~f{bTx{t z^-)2LhsB_jNev#-hbQ6EHWhP5s`&{JM<)g$EUCm68;4qAit3*@hHH z!pwVo$)K_EU@}oNh>3xuB+?rX|lPtW?rt7H`*P1 zw_8esR*2-S5K`d>FAm7&c<&Sy7eB8tsvb>U88W&={>zjzhH z7Irg!yEs4Zd5`s+nVGqV>@}GPlvY%vr=}JKuJh4sq}6se=Txl3Q#T3-z2f3fOSIbP z3@9xxZ~y$%L+WX+tlyWTBNxcg(w5-0O^YmFjY^2IGe0^K^4vmdy) zH9Y45h+q1(LUslav2tJ4JfeZaj{%|6l|F008Q$Ix2O_RuTULLxy)* literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/score-x@2x.png b/osu.Game.Tests/Resources/special-skin/score-x@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c61724db1f021dc25216f444bf9b2a9551a107f5 GIT binary patch literal 2801 zcmVBd1||a&fm9$8 z2nTGy4d5nl9XJX6Q`mgL3akWv21El5z;@sO(C1^>{UiXZffIloaCrIY15N|K2Oc%} z?7eQvZUlY#$iz zx#xfkPX)?-@ZUhDdV`;zAL;4oq@|^il9IxR5hDl*31Qf;8MdAAu5g{nIIM7;x8_wpc7o zojR55>}+PunniSU^dNQmmd?&j-hKC7cJAEC)vH&vZrKYgcT<@};F2@p=jTUtbv5DP z;dFF#uxr;YcI?=}H{X1tRc~k6B{(?PD{%len~m|~$Fp?l zQo_Q*ICbh2eSI!k#*0z-J&o*zz>l1XjEoF&bMLq8pr9Z!GBU`?$>H|x+g!MCLCu>Y zM*26!TOAQZhF9oCiQ;WF+(E&2w9BR#q1K_wOeuDM`KbbKqIoAPYPL zY?Uu=+O&y|jt)JV1~6*WD9X#r2@Q3bz5;-KK%A(v9N0X3{!29pNkHg_`nnQq}h`}w-Y4rE^ zXWhDWgoK26EH@}9h|J7P4jec@Z*Q;D91Q#yIHe`<4$uqy)M+?>{=BY{)E)8h@mMSt zYHMp{^GINUJY7BW%rh)lu)ynbBO)T`>FMF*Nx4fi8h8Vxi8?6*E;sYNeo{KzF&zmMv2U!*sD$))d$yCggr1QD0BN`p2yjrHt6qKeRd=fiUD#|k%F4=k{PD+K z>=6+WtXj3oaJeH#j#N#d3?dNm7W;c{OG^tC6%~dnAQKf8#rEynS+QaTVPRn;B_;9t z>#q|N6Jxl{ufP6UHQmOG{os1BMFAi%FpziNd51?IHM$deF!c5Hk(rrES67#89s{&^ zTBfX#H?Vi_-etpv4G*lMMyac-Q^j2aTJX?B0d9#EPCo|^9^~}t(+{GahI#9)w^Y*+ z=NE4lntuW6oC$}+!7H!4GT650q0-vg%6sn(SiA3Ze(^?NpWF`t&YnHXM<0DORCV-; z($Z3ObZ-JabSAvrDjWg+AsaSt-YhS>z7vigKTcIum137-} zJ32ZjDk@U{$^Vg?CpraQ7CV4`s;jGgt*0US`uZp=ETpZiO*Y>HeyO@d^zLTgloKaT zobaWdhAA&E*LwD!flpMUH;!MK7_l3-00aaCaPs6ytX6eX^5D3B{W?!S{j@r;R027| zGvR{XH+WO{B?0K|?WMJ~b?_WROhrY7D(;%tPil^M`W-Hk=L_Jo&ptCygfScrht{s9 zNc`h9F(UADInmkKX`l#WT)uo+Z84qd^rd1%piBFZTm=uDD_5?lrX#}T;T9tTUE2D+ zd-vSmIm9$IHL0d^p7IzG7%L}y_0{M>W5fNWr#wakj*}A_KF|Lv^a>0FT%0BkQ=o>g zbhxKHdIe5%daD3pV`FulUHFQI-({?)JbDFY%ZaC+dP-+@-_UTCqoBqSurCMz&&usnJMx*U+qnKOs5 zurR%K^cjtR>a*H+-p5;k^MN_C;koCY(_6P8VeZ_ys%idUfezjX3UFzu#t^j@^8}jn< zNJ&WeOC&z-;xqFwj%;)^eS z=S@?_X0x$!4pHJ3AYTMXzJ; zTPzj}W5$e8z1#6XJ#Zckfn~storz(?hOuSK7DkO4Ww;UmhYlU0sHn&#>gmdrD|B^r zF>Ts3pURGli=)22UUh!`5GV)iauXe=P6_$>`6MSN;|nbq8Nu;?A?+6zDR0M?^#n*?Gf=5d#+bV}N6_VJO`H8V=|lB!M^Ov%ecRZt%qy zU-c<~{OkQ3dUw-M)x2nm;jT@<~tW?cg#TmNGDtXVI zJuF+cjAO@+srP%urAwF8AU_B2FO-SCUQF`wPD5*JEBp5CqpPcnsP^Lzf!EMD z038h+)^f*LtyU&YnnYGs7852+AT>3W@Nid$RzpJrYuB!&uC7kaep5vB%N=D3@HY{R zK7i2BP-e`SL1tzqlP6DR?AU=W&~xX`@$$qZsyvxYg)J668;>m^X^cwIL`BQ+8h}fNlZ)((b3UZEEcX_z3OiCtppZ&s=G7H z_;VkB6w(Sz^7Js_-+Wu=&xo`>XXMy#ZwLf_1)MeTuswPoCjJ?(0nG-Ux8K0?T!<6# z+*`#Du(W;#^p)sW1HyWB;wKi%e-LBBZ8xIsh&S(c*=2k)c7FsOBUbJKLU66tuTO>m zEAWVj@_0n}iCV?T{P2#LO-xKoOiWBnOiWBnzCZjAFh_Rf0Exov00000NkvXXu0mjf DwF+8& literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 0eafe33343..56cdbb1e8d 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -79,6 +79,8 @@ namespace osu.Game.Tests.Skins "Archives/modified-argon-20250424.osk", // Covers "Argon" unstable rate counter "Archives/modified-argon-20250809.osk", + // Covers legacy style performance points counter + "Archives/modified-classic-20250827.osk", }; /// diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs index eada72326d..68f314d52a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; using osu.Game.Skinning.Triangles; using osu.Game.Tests.Gameplay; @@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Drawable CreateDefaultImplementation() => new TrianglesPerformancePointsCounter(); protected override Drawable CreateArgonImplementation() => new ArgonPerformancePointsCounter(); - protected override Drawable CreateLegacyImplementation() => Empty(); + protected override Drawable CreateLegacyImplementation() => new LegacyPerformancePointsCounter(); private Bindable lastJudgementResult => (Bindable)gameplayState.LastJudgementResult; diff --git a/osu.Game/Skinning/LegacyPerformancePointsCounter.cs b/osu.Game/Skinning/LegacyPerformancePointsCounter.cs new file mode 100644 index 0000000000..e59a4a80b4 --- /dev/null +++ b/osu.Game/Skinning/LegacyPerformancePointsCounter.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 osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Skinning +{ + public partial class LegacyPerformancePointsCounter : PerformancePointsCounter, ISerialisableDrawable + { + protected override double RollingDuration => 1000; + protected override Easing RollingEasing => Easing.Out; + + private const float alpha_when_invalid = 0.3f; + + public LegacyPerformancePointsCounter() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + Scale = new Vector2(0.96f); + } + + public override bool IsValid + { + get => base.IsValid; + set + { + if (value == IsValid) + return; + + base.IsValid = value; + DrawableCount.FadeTo(value ? 1 : alpha_when_invalid, 1000, Easing.OutQuint); + } + } + + protected override LocalisableString FormatCount(int count) => count.ToString($@"0'{LegacySpriteText.PP}'"); + + protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score); + } +} diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 1028b5bb9d..dca258e6eb 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -14,6 +14,11 @@ namespace osu.Game.Skinning { public sealed partial class LegacySpriteText : OsuSpriteText { + /// + /// The Private Use Area character representing performance points. + /// + public const char PP = '\uebd9'; + public Vector2? MaxSizePerGlyph { get; init; } public bool FixedWidth { get; init; } @@ -23,7 +28,7 @@ namespace osu.Game.Skinning protected override char FixedWidthReferenceCharacter => '5'; - protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; + protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x', PP }; // ReSharper disable once UnusedMember.Global // being unused is the point here @@ -116,6 +121,9 @@ namespace osu.Game.Skinning case '%': return "percent"; + case PP: + return "pp"; + default: return character.ToString(); } From 3aad0868af6865e193df1dbd5108afb9ec9992b6 Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Wed, 27 Aug 2025 19:57:58 -0230 Subject: [PATCH 010/308] Remove duplicated declarations --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index ee3f7805aa..068471ae49 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -133,9 +133,6 @@ namespace osu.Game.Tests.Visual.Settings { public Bindable AreaOffset { get; } = new Bindable(); public Bindable AreaSize { get; } = new Bindable(); - public Bindable OutputAreaOffset { get; } = new Bindable(); - public Bindable OutputAreaSize { get; } = new Bindable(); - public Bindable OutputAreaSize { get; } = new Bindable(); public Bindable OutputAreaOffset { get; } = new Bindable(); From 61c3aad53791c838d5d20e6f93a77d6bdb0ac9ab Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Wed, 27 Aug 2025 20:00:56 -0230 Subject: [PATCH 011/308] Fix conflict --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 068471ae49..e9f70180e1 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -133,8 +133,8 @@ namespace osu.Game.Tests.Visual.Settings { public Bindable AreaOffset { get; } = new Bindable(); public Bindable AreaSize { get; } = new Bindable(); - public Bindable OutputAreaSize { get; } = new Bindable(); public Bindable OutputAreaOffset { get; } = new Bindable(); + public Bindable OutputAreaSize { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); From 038bf3fddaad3787749b88865a1a52d665517496 Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Wed, 27 Aug 2025 20:11:28 -0230 Subject: [PATCH 012/308] "Conform to aspect ratio" uses scaled area --- .../Settings/Sections/Input/TabletSettings.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 35304f7c34..a355205a85 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -167,7 +167,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input Text = TabletSettingsStrings.ConformToCurrentGameAspectRatio, Action = () => { - forceAspectRatio((float)host.Window.ClientSize.Width / host.Window.ClientSize.Height); + float gameplayWidth = host.Window.ClientSize.Width; + float gameplayHeight = host.Window.ClientSize.Height; + + if (osuConfig.Get(OsuSetting.Scaling) == ScalingMode.Everything) + { + gameplayWidth *= osuConfig.Get(OsuSetting.ScalingSizeX); + gameplayHeight *= osuConfig.Get(OsuSetting.ScalingSizeY); + } + + forceAspectRatio(gameplayWidth / gameplayHeight); }, CanBeShown = { BindTarget = enabled } }, From cab0b3451f5f3b6e6cf374dc25b1d7309eb07cd1 Mon Sep 17 00:00:00 2001 From: dnfd1 <117837311+dnfd1@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:26:45 -0700 Subject: [PATCH 013/308] Override OD setting to set extended limits for mania EZ and HR --- osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs index 0817f8f9fc..9514f72fe0 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs @@ -7,5 +7,14 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDifficultyAdjust : ModDifficultyAdjust { + public override DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable + { + Precision = 0.1f, + MinValue = 0, + MaxValue = 10, + ExtendedMaxValue = 13.61f, + ExtendedMinValue = -14.93f, + ReadCurrentFromDifficulty = diff => diff.OverallDifficulty, + }; } } From 348713d83d86fb22219910d3782c46e1a7956e6a Mon Sep 17 00:00:00 2001 From: dnfd1 <117837311+dnfd1@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:27:49 -0700 Subject: [PATCH 014/308] Allow OD to be overrided --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index da5f5df200..c6eaa75e9e 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mods }; [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))] - public DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable + public virtual DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable { Precision = 0.1f, MinValue = 0, From faad1753a4f9e051fffb4c41d8e4f5f54f7c12e2 Mon Sep 17 00:00:00 2001 From: dnfd1 <117837311+dnfd1@users.noreply.github.com> Date: Thu, 9 Oct 2025 05:13:17 -0700 Subject: [PATCH 015/308] Round OD limits to -15 and 15 --- osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs index 9514f72fe0..c1c25ad62e 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs @@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Mods Precision = 0.1f, MinValue = 0, MaxValue = 10, - ExtendedMaxValue = 13.61f, - ExtendedMinValue = -14.93f, + ExtendedMaxValue = 15, + ExtendedMinValue = -15, ReadCurrentFromDifficulty = diff => diff.OverallDifficulty, }; } From 0d68e5eeeb305b46e73410a97a6ccd7d6f4014d9 Mon Sep 17 00:00:00 2001 From: dnfd1 <117837311+dnfd1@users.noreply.github.com> Date: Thu, 9 Oct 2025 05:21:08 -0700 Subject: [PATCH 016/308] add inline comment to explain larger limits --- osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs index c1c25ad62e..ce70fdf73a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs @@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods Precision = 0.1f, MinValue = 0, MaxValue = 10, + // Use larger extended limits for mania to include OD values that occur with EZ or HR enabled ExtendedMaxValue = 15, ExtendedMinValue = -15, ReadCurrentFromDifficulty = diff => diff.OverallDifficulty, From 1ec6735a3523a24fd185b80c82ad6a1d3d26d922 Mon Sep 17 00:00:00 2001 From: Loreos7 <86934170+Loreos7@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:17:08 +0300 Subject: [PATCH 017/308] Restore original delete button name --- osu.Game/Localisation/SongSelectStrings.cs | 4 ++-- osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index c81cf97f09..5f940f8a56 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString ClearAllLocalScores => new TranslatableString(getKey(@"clear_all_local_scores"), @"Clear all local scores"); /// - /// "Delete beatmap" + /// "Delete..." /// - public static LocalisableString DeleteBeatmap => new TranslatableString(getKey(@"delete_beatmap"), @"Delete beatmap"); + public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete..."); /// /// "Restore all hidden" diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs index 7e71fedfcb..ae06522b30 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(beatmap.BeatmapSet != null); addHeader(SongSelectStrings.ForAllDifficulties, beatmap.BeatmapSet.ToString()); - addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => SongSelect?.Delete(beatmap.BeatmapSet), colours.Red1); + addButton(SongSelectStrings.Delete, FontAwesome.Solid.Trash, () => SongSelect?.Delete(beatmap.BeatmapSet), colours.Red1); addHeader(SongSelectStrings.ForSelectedDifficulty, beatmap.DifficultyName); diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index a52d3fa216..cc55286431 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -268,7 +268,7 @@ namespace osu.Game.Screens.SelectV2 if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem(SongSelectStrings.RestoreAllHidden, MenuItemType.Standard, () => songSelect?.RestoreAllHidden(beatmapSet))); - items.Add(new OsuMenuItem(SongSelectStrings.DeleteBeatmap, MenuItemType.Destructive, () => songSelect?.Delete(beatmapSet))); + items.Add(new OsuMenuItem(SongSelectStrings.Delete, MenuItemType.Destructive, () => songSelect?.Delete(beatmapSet))); return items.ToArray(); } } From b600860540ed9dd8ffa535bfe1eb14b281201cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 12:42:36 +0200 Subject: [PATCH 018/308] Implement request & response for fetching logged in user's favourite beatmap sets --- .../Requests/GetMyFavouriteBeatmapSetsRequest.cs | 12 ++++++++++++ .../Responses/GetMyFavouriteBeatmapSetsResponse.cs | 13 +++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 osu.Game/Online/API/Requests/GetMyFavouriteBeatmapSetsRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/GetMyFavouriteBeatmapSetsResponse.cs diff --git a/osu.Game/Online/API/Requests/GetMyFavouriteBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/GetMyFavouriteBeatmapSetsRequest.cs new file mode 100644 index 0000000000..87a901c98e --- /dev/null +++ b/osu.Game/Online/API/Requests/GetMyFavouriteBeatmapSetsRequest.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. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetMyFavouriteBeatmapSetsRequest : APIRequest + { + protected override string Target => @"me/beatmapset-favourites"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/GetMyFavouriteBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/Responses/GetMyFavouriteBeatmapSetsResponse.cs new file mode 100644 index 0000000000..f728b8ea0b --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/GetMyFavouriteBeatmapSetsResponse.cs @@ -0,0 +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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class GetMyFavouriteBeatmapSetsResponse + { + [JsonProperty("beatmapset_ids")] + public int[] BeatmapSetIds { get; set; } = []; + } +} From 0f1bf35bd9131aa30ecce9ef39ccf50c96baf9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 12:48:41 +0200 Subject: [PATCH 019/308] Add favourite beatmap set tracking to `LocalUserInfo` --- osu.Game/Online/API/DummyAPIAccess.cs | 6 ++++++ osu.Game/Online/API/ILocalUserState.cs | 2 ++ osu.Game/Online/API/LocalUserState.cs | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index dbf5964416..c01d0ca480 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -238,10 +238,12 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); public BindableList Blocks { get; } = new BindableList(); + public BindableList FavouriteBeatmapSets { get; } = new BindableList(); IBindable ILocalUserState.User => User; IBindableList ILocalUserState.Friends => Friends; IBindableList ILocalUserState.Blocks => Blocks; + IBindableList ILocalUserState.FavouriteBeatmapSets => FavouriteBeatmapSets; public void UpdateFriends() { @@ -250,6 +252,10 @@ namespace osu.Game.Online.API public void UpdateBlocks() { } + + public void UpdateFavouriteBeatmapSets() + { + } } } } diff --git a/osu.Game/Online/API/ILocalUserState.cs b/osu.Game/Online/API/ILocalUserState.cs index 3ccec1c9ae..4c5cbcf197 100644 --- a/osu.Game/Online/API/ILocalUserState.cs +++ b/osu.Game/Online/API/ILocalUserState.cs @@ -11,8 +11,10 @@ namespace osu.Game.Online.API IBindable User { get; } IBindableList Friends { get; } IBindableList Blocks { get; } + IBindableList FavouriteBeatmapSets { get; } void UpdateFriends(); void UpdateBlocks(); + void UpdateFavouriteBeatmapSets(); } } diff --git a/osu.Game/Online/API/LocalUserState.cs b/osu.Game/Online/API/LocalUserState.cs index 5da9289d89..1359d62ae7 100644 --- a/osu.Game/Online/API/LocalUserState.cs +++ b/osu.Game/Online/API/LocalUserState.cs @@ -16,12 +16,14 @@ namespace osu.Game.Online.API public IBindable User => localUser; public IBindableList Friends => friends; public IBindableList Blocks => blocks; + public IBindableList FavouriteBeatmapSets => favouriteBeatmapSets; private readonly IAPIProvider api; private readonly Bindable localUser = new Bindable(createGuestUser()); private readonly BindableList friends = new BindableList(); private readonly BindableList blocks = new BindableList(); + private readonly BindableList favouriteBeatmapSets = new BindableList(); private readonly Bindable configStatus = new Bindable(); private readonly Bindable configSupporter = new Bindable(); @@ -62,6 +64,7 @@ namespace osu.Game.Online.API UpdateFriends(); UpdateBlocks(); + UpdateFavouriteBeatmapSets(); } public void ClearLocalUser() @@ -76,6 +79,7 @@ namespace osu.Game.Online.API configSupporter.Value = false; friends.Clear(); blocks.Clear(); + favouriteBeatmapSets.Clear(); }); } @@ -125,5 +129,23 @@ namespace osu.Game.Online.API api.Queue(blocksReq); } + + public void UpdateFavouriteBeatmapSets() + { + if (!api.IsLoggedIn) + return; + + var favouritesReq = new GetMyFavouriteBeatmapSetsRequest(); + favouritesReq.Success += res => + { + var existingBeatmapSets = favouriteBeatmapSets.ToHashSet(); + var updatedBeatmapSets = res.BeatmapSetIds.ToHashSet(); + + favouriteBeatmapSets.AddRange(updatedBeatmapSets.Except(existingBeatmapSets)); + favouriteBeatmapSets.RemoveAll(b => !updatedBeatmapSets.Contains(b)); + }; + + api.Queue(favouritesReq); + } } } From 6b56a0611b8ffa9890782b3f80f9bb3217f9b095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 13:04:50 +0200 Subject: [PATCH 020/308] Refetch list of user favourites on every change to favourites Is this lazy? Sure it is. Friends and blocks do the same thing, though, and I'm not overthinking this any more than I already have. Being smarter here would likely mean being more invasive with respect to listening in on all outgoing API requests and silently updating favourites on that basis. Which is "smart" but also complicated. --- osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs | 1 + osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs | 1 + osu.Game/Screens/Ranking/FavouriteButton.cs | 1 + osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs | 2 ++ 4 files changed, 5 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs index 0b2aaf0bc3..f1ec1d1965 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs @@ -62,6 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1)); SetLoading(false); + api.LocalUserState.UpdateFavouriteBeatmapSets(); }; favouriteRequest.Failure += e => { diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index eab394c8f6..215e521d42 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -74,6 +74,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons { favourited.Toggle(); loading.Hide(); + api.LocalUserState.UpdateFavouriteBeatmapSets(); }; request.Failure += e => diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index 019b80dde9..7f1c4e82cc 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -109,6 +109,7 @@ namespace osu.Game.Screens.Ranking Enabled.Value = true; loading.Hide(); + api.LocalUserState.UpdateFavouriteBeatmapSets(); }; favouriteRequest.Failure += e => { diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs index 2db3ed7613..62ac8a07b4 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs @@ -233,6 +233,8 @@ namespace osu.Game.Screens.SelectV2 // if the beatmap set reference changed under the callback, abort visual updates to avoid showing stale data if (onlineBeatmapSet == null || ReferenceEquals(beatmapSet, onlineBeatmapSet)) setBeatmapSet(beatmapSet, withHeartAnimation: hasFavourited); + + api.LocalUserState.UpdateFavouriteBeatmapSets(); }; favouriteRequest.Failure += e => { From 29787360ba5f45575623701774e31894ce87858c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 13:08:24 +0200 Subject: [PATCH 021/308] Change `BeatmapCarouselFilterGrouping` constructor params to required init properties --- .../BeatmapCarouselFilterGroupingTest.cs | 10 ++++++---- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 7 ++++++- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 20 ++++++------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index dcd7a5a8fc..0668c60825 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -368,10 +368,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private static async Task> runGrouping(GroupMode group, List beatmapSets) { - var groupingFilter = new BeatmapCarouselFilterGrouping( - () => new FilterCriteria { Group = group }, - () => new List(), - _ => new Dictionary()); + var groupingFilter = new BeatmapCarouselFilterGrouping + { + GetCriteria = () => new FilterCriteria { Group = group }, + GetCollections = () => new List(), + GetLocalUserTopRanks = _ => new Dictionary() + }; return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 6b78967b93..761fba80a6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -105,7 +105,12 @@ namespace osu.Game.Screens.SelectV2 { new BeatmapCarouselFilterMatching(() => Criteria!), new BeatmapCarouselFilterSorting(() => Criteria!), - grouping = new BeatmapCarouselFilterGrouping(() => Criteria!, GetAllCollections, GetBeatmapInfoGuidToTopRankMapping) + grouping = new BeatmapCarouselFilterGrouping + { + GetCriteria = () => Criteria!, + GetCollections = GetAllCollections, + GetLocalUserTopRanks = GetBeatmapInfoGuidToTopRankMapping + } }; AddInternal(loading = new LoadingLayer()); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index fa01343cbe..14de07ba24 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -39,17 +39,9 @@ namespace osu.Game.Screens.SelectV2 private Dictionary> setMap = new Dictionary>(); private Dictionary> groupMap = new Dictionary>(); - private readonly Func getCriteria; - private readonly Func> getCollections; - private readonly Func> getLocalUserTopRanks; - - public BeatmapCarouselFilterGrouping(Func getCriteria, Func> getCollections, - Func> getLocalUserTopRanks) - { - this.getCriteria = getCriteria; - this.getCollections = getCollections; - this.getLocalUserTopRanks = getLocalUserTopRanks; - } + public required Func GetCriteria { get; init; } + public required Func> GetCollections { get; init; } + public required Func> GetLocalUserTopRanks { get; init; } public async Task> Run(IEnumerable items, CancellationToken cancellationToken) { @@ -59,7 +51,7 @@ namespace osu.Game.Screens.SelectV2 var newSetMap = new Dictionary>(setMap.Count); var newGroupMap = new Dictionary>(groupMap.Count); - var criteria = getCriteria(); + var criteria = GetCriteria(); var newItems = new List(); BeatmapSetsGroupedTogether = ShouldGroupBeatmapsTogether(criteria); @@ -215,7 +207,7 @@ namespace osu.Game.Screens.SelectV2 case GroupMode.Collections: { - var collections = getCollections(); + var collections = GetCollections(); return getGroupsBy(b => defineGroupByCollection(b, collections), items); } @@ -224,7 +216,7 @@ namespace osu.Game.Screens.SelectV2 case GroupMode.RankAchieved: { - var topRankMapping = getLocalUserTopRanks(criteria); + var topRankMapping = GetLocalUserTopRanks(criteria); return getGroupsBy(b => defineGroupByRankAchieved(b, topRankMapping), items); } From 14d0982b6c6665b6d4ec5061b4317751bb2433cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 13:30:49 +0200 Subject: [PATCH 022/308] Implement grouping by favourites - Closes https://github.com/ppy/osu/issues/34494. - Supersedes / closes https://github.com/ppy/osu/pull/34744. --- .../BeatmapCarouselFilterGroupingTest.cs | 30 +++++++++++++++++-- osu.Game/Screens/Select/Filter/GroupMode.cs | 4 +-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 16 ++++++++-- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 17 +++++++++-- osu.Game/Screens/SelectV2/FilterControl.cs | 3 ++ 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 0668c60825..e439a18ded 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -366,13 +366,39 @@ namespace osu.Game.Tests.Visual.SongSelectV2 #endregion - private static async Task> runGrouping(GroupMode group, List beatmapSets) + #region Favourites grouping + + [Test] + public async Task TestFavouritesGrouping() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(s => s.OnlineID = 1, beatmapSets, out _); + addBeatmapSet(s => s.OnlineID = 21, beatmapSets, out var firstFavourite); + addBeatmapSet(s => s.OnlineID = 321, beatmapSets, out _); + addBeatmapSet(s => s.OnlineID = 4321, beatmapSets, out _); + addBeatmapSet(s => s.OnlineID = 54321, beatmapSets, out var secondFavourite); + + favouriteBeatmapSets = [21, 54321]; + + var results = await runGrouping(GroupMode.Favourites, beatmapSets); + assertGroup(results, 0, "Favourites", firstFavourite.Beatmaps.Concat(secondFavourite.Beatmaps), ref total); + assertTotal(results, total); + } + + #endregion + + private HashSet favouriteBeatmapSets = []; + + private async Task> runGrouping(GroupMode group, List beatmapSets) { var groupingFilter = new BeatmapCarouselFilterGrouping { GetCriteria = () => new FilterCriteria { Group = group }, GetCollections = () => new List(), - GetLocalUserTopRanks = _ => new Dictionary() + GetLocalUserTopRanks = _ => new Dictionary(), + GetFavouriteBeatmapSets = () => favouriteBeatmapSets, }; return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index 06d3a71b0f..e2bc1faae2 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.cs @@ -32,8 +32,8 @@ namespace osu.Game.Screens.Select.Filter [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Difficulty))] Difficulty, - // [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Favourites))] - // Favourites, + [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Favourites))] + Favourites, [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LastPlayed))] LastPlayed, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 761fba80a6..e55f64f847 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -28,6 +28,7 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Select; @@ -109,7 +110,8 @@ namespace osu.Game.Screens.SelectV2 { GetCriteria = () => Criteria!, GetCollections = GetAllCollections, - GetLocalUserTopRanks = GetBeatmapInfoGuidToTopRankMapping + GetLocalUserTopRanks = GetBeatmapInfoGuidToTopRankMapping, + GetFavouriteBeatmapSets = GetFavouriteBeatmapSets, } }; @@ -809,11 +811,14 @@ namespace osu.Game.Screens.SelectV2 #endregion - #region Database fetches for grouping support + #region Fetches for grouping support [Resolved] private RealmAccess realm { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + protected virtual List GetAllCollections() => realm.Run(r => r.All().AsEnumerable().Detach()); protected virtual Dictionary GetBeatmapInfoGuidToTopRankMapping(FilterCriteria criteria) => realm.Run(r => @@ -838,6 +843,13 @@ namespace osu.Game.Screens.SelectV2 return topRankMapping; }); + /// + /// Note that calling .ToHashSet() below has two purposes: + /// one being performance of contain checks in filtering code, + /// another being slightly better thread safety (as could be mutated during async filtering). + /// + protected HashSet GetFavouriteBeatmapSets() => api.LocalUserState.FavouriteBeatmapSets.ToHashSet(); + #endregion #region Drawable pooling diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 14de07ba24..159d8f137e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -42,6 +42,7 @@ namespace osu.Game.Screens.SelectV2 public required Func GetCriteria { get; init; } public required Func> GetCollections { get; init; } public required Func> GetLocalUserTopRanks { get; init; } + public required Func> GetFavouriteBeatmapSets { get; init; } public async Task> Run(IEnumerable items, CancellationToken cancellationToken) { @@ -220,9 +221,11 @@ namespace osu.Game.Screens.SelectV2 return getGroupsBy(b => defineGroupByRankAchieved(b, topRankMapping), items); } - // TODO: need implementation - // case GroupMode.Favourites: - // goto case GroupMode.None; + case GroupMode.Favourites: + { + var favouriteBeatmapSets = GetFavouriteBeatmapSets(); + return getGroupsBy(b => defineGroupByFavourites(b, favouriteBeatmapSets), items); + } default: throw new ArgumentOutOfRangeException(); @@ -429,6 +432,14 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(int.MaxValue, "Unplayed").Yield(); } + private IEnumerable defineGroupByFavourites(BeatmapInfo beatmap, HashSet favouriteBeatmapSets) + { + if (beatmap.BeatmapSet?.OnlineID > 0 && favouriteBeatmapSets.Contains(beatmap.BeatmapSet.OnlineID)) + return new GroupDefinition(0, "Favourites").Yield(); + + return []; + } + private record GroupMapping(GroupDefinition? Group, List ItemsInGroup); } } diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index c845a9e146..a90ac3a4e8 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -57,6 +57,7 @@ namespace osu.Game.Screens.SelectV2 private RealmAccess realm { get; set; } = null!; private IBindable localUser = null!; + private readonly IBindableList localUserFavouriteBeatmapSets = new BindableList(); public LocalisableString StatusText { @@ -186,6 +187,7 @@ namespace osu.Game.Screens.SelectV2 }; localUser = api.LocalUser.GetBoundCopy(); + localUserFavouriteBeatmapSets.BindTo(api.LocalUserState.FavouriteBeatmapSets); } protected override void LoadComplete() @@ -237,6 +239,7 @@ namespace osu.Game.Screens.SelectV2 }); localUser.BindValueChanged(_ => updateCriteria()); + localUserFavouriteBeatmapSets.BindCollectionChanged((_, _) => updateCriteria()); updateCriteria(); } From 03adae4417a8b52c5af8bb00672e0a177442f4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Oct 2025 11:07:06 +0200 Subject: [PATCH 023/308] Scroll song select title wedge text if it overflows Instead of truncating. Addresses https://github.com/ppy/osu/discussions/35404. The one "tiny" problem is that the "click to search" functionality of these texts is maybe a bit worse now, because the clickable target is now the full width of the wedge rather than autosized to the text. Salvaging this is *maybe* possible, but *definitely* annoying, so I'd rather not frontload it. --- osu.Game/Overlays/MarqueeContainer.cs | 14 +++-- osu.Game/Overlays/Music/PlaylistItem.cs | 1 + osu.Game/Overlays/NowPlayingOverlay.cs | 2 + .../Screens/SelectV2/BeatmapTitleWedge.cs | 58 ++++++++++--------- 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/osu.Game/Overlays/MarqueeContainer.cs b/osu.Game/Overlays/MarqueeContainer.cs index 1b0b59abe0..07ef70981f 100644 --- a/osu.Game/Overlays/MarqueeContainer.cs +++ b/osu.Game/Overlays/MarqueeContainer.cs @@ -49,8 +49,15 @@ namespace osu.Game.Overlays private Func? createContent; + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + public float OverflowSpacing { get; init; } = 15; + private const float pixels_per_second = 50; - private const float padding = 15; private Drawable mainContent = null!; private Drawable fillerContent = null!; @@ -71,8 +78,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Horizontal, Anchor = NonOverflowingContentAnchor, Origin = NonOverflowingContentAnchor, - Spacing = new Vector2(padding), - Padding = new MarginPadding { Horizontal = padding }, + Spacing = new Vector2(OverflowSpacing), }; } @@ -105,7 +111,7 @@ namespace osu.Game.Overlays flow.Anchor = Anchor.TopLeft; flow.Origin = Anchor.TopLeft; - float targetX = mainContent.DrawWidth + padding; + float targetX = mainContent.DrawWidth + OverflowSpacing; flow.MoveToX(0) .Delay(InitialMoveDelay) diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 6217a9bc9e..5cbde6ba57 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -49,6 +49,7 @@ namespace osu.Game.Overlays.Music RelativeSizeAxes = Axes.X, InitialMoveDelay = 0, AllowScrolling = false, + Padding = new MarginPadding { Horizontal = 15 }, }; selectedSet.BindTo(playlistOverlay.SelectedSet); diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 11819cb485..a58aa27e24 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -121,6 +121,7 @@ namespace osu.Game.Overlays Origin = Anchor.Centre, }, NonOverflowingContentAnchor = Anchor.Centre, + Padding = new MarginPadding { Horizontal = 15 }, }, artist = new MarqueeContainer { @@ -136,6 +137,7 @@ namespace osu.Game.Overlays Origin = Anchor.Centre, }, NonOverflowingContentAnchor = Anchor.Centre, + Padding = new MarginPadding { Horizontal = 15 }, }, new Container { diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 530b1348dd..69f4aaea4a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -20,6 +20,7 @@ using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -49,15 +50,13 @@ namespace osu.Game.Screens.SelectV2 private ModSettingChangeTracker? settingChangeTracker; private BeatmapSetOnlineStatusPill statusPill = null!; - private Container titleContainer = null!; private OsuHoverContainer titleLink = null!; - private OsuSpriteText titleLabel = null!; - private Container artistContainer = null!; + private MarqueeContainer titleLabel = null!; private OsuHoverContainer artistLink = null!; - private OsuSpriteText artistLabel = null!; + private MarqueeContainer artistLabel = null!; - internal string DisplayedTitle => titleLabel.Text.ToString(); - internal string DisplayedArtist => artistLabel.Text.ToString(); + internal string DisplayedTitle { get; private set; } + internal string DisplayedArtist { get; private set; } private StatisticPlayCount playCount = null!; private FavouriteButton favouriteButton = null!; @@ -110,7 +109,7 @@ namespace osu.Game.Screens.SelectV2 TextSize = OsuFont.Style.Caption1.Size, TextPadding = new MarginPadding { Horizontal = 6, Vertical = 1 }, }), - new ShearAligningWrapper(titleContainer = new Container + new ShearAligningWrapper(new Container { Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.X, @@ -118,15 +117,15 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Bottom = -4f }, Child = titleLink = new OsuHoverContainer { - AutoSizeAxes = Axes.Both, - Child = titleLabel = new TruncatingSpriteText + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = titleLabel = new MarqueeContainer { - Shadow = true, - Font = OsuFont.Style.Title, - }, + OverflowSpacing = 50, + } } }), - new ShearAligningWrapper(artistContainer = new Container + new ShearAligningWrapper(new Container { Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.X, @@ -134,12 +133,12 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Left = 1f }, Child = artistLink = new OsuHoverContainer { - AutoSizeAxes = Axes.Both, - Child = artistLabel = new TruncatingSpriteText + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = artistLabel = new MarqueeContainer { - Shadow = true, - Font = OsuFont.Style.Heading2, - }, + OverflowSpacing = 50, + } } }), new ShearAligningWrapper(statisticsFlow = new FillFlowContainer @@ -214,13 +213,6 @@ namespace osu.Game.Screens.SelectV2 .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); } - protected override void Update() - { - base.Update(); - titleLabel.MaxWidth = titleContainer.DrawWidth - 20; - artistLabel.MaxWidth = artistContainer.DrawWidth - 20; - } - private void updateDisplay() { var metadata = working.Value.Metadata; @@ -229,12 +221,24 @@ namespace osu.Game.Screens.SelectV2 statusPill.Status = beatmapInfo.Status; var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title); - titleLabel.Text = titleText; + titleLabel.CreateContent = () => new OsuSpriteText + { + Text = titleText, + Shadow = true, + Font = OsuFont.Style.Title, + }; titleLink.Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)); + DisplayedTitle = titleText.ToString(); var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - artistLabel.Text = artistText; + artistLabel.CreateContent = () => new OsuSpriteText + { + Text = artistText, + Shadow = true, + Font = OsuFont.Style.Heading2, + }; artistLink.Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)); + DisplayedArtist = artistText.ToString(); updateLengthAndBpmStatistics(); updateOnlineDisplay(); From 1a49d030a0b5322b58557f63df8d91218c16c0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Oct 2025 11:08:01 +0200 Subject: [PATCH 024/308] Fix marquee container not updating scrolling state if its content changes size This is actually possible in current usages if you e.g. toggle "use original metadata" on/off which will change the width of the underlying sprite texts. Or by setting window size. Pick your poison. --- osu.Game/Overlays/MarqueeContainer.cs | 39 +++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/MarqueeContainer.cs b/osu.Game/Overlays/MarqueeContainer.cs index 07ef70981f..2d651abb00 100644 --- a/osu.Game/Overlays/MarqueeContainer.cs +++ b/osu.Game/Overlays/MarqueeContainer.cs @@ -3,8 +3,10 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; using osuTK; namespace osu.Game.Overlays @@ -21,7 +23,7 @@ namespace osu.Game.Overlays set { allowScrolling = value; - ScheduleAfterChildren(updateScrolling); + scrollCached.Invalidate(); } } @@ -63,8 +65,13 @@ namespace osu.Game.Overlays private Drawable fillerContent = null!; private FillFlowContainer flow = null!; + private readonly Cached scrollCached = new Cached(); + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + public MarqueeContainer() { + AddLayout(drawSizeLayout); + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; } @@ -72,13 +79,14 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - InternalChild = flow = new FillFlowContainer + InternalChild = flow = new MarqueeFlow { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Anchor = NonOverflowingContentAnchor, Origin = NonOverflowingContentAnchor, Spacing = new Vector2(OverflowSpacing), + OnRequiredParentSizeInvalidated = () => scrollCached.Invalidate(), }; } @@ -98,12 +106,17 @@ namespace osu.Game.Overlays flow.Add(mainContent = createContent()); flow.Add(fillerContent = createContent().With(d => d.Alpha = 0)); - ScheduleAfterChildren(updateScrolling); + scrollCached.Invalidate(); } - private void updateScrolling() + protected override void UpdateAfterChildren() { - float overflowWidth = mainContent.DrawWidth + padding - DrawWidth; + base.UpdateAfterChildren(); + + if (scrollCached.IsValid && drawSizeLayout.IsValid) + return; + + float overflowWidth = mainContent.DrawWidth - DrawWidth; if (overflowWidth > 0 && AllowScrolling) { @@ -126,6 +139,22 @@ namespace osu.Game.Overlays flow.Anchor = NonOverflowingContentAnchor; flow.Origin = NonOverflowingContentAnchor; } + + scrollCached.Validate(); + drawSizeLayout.Validate(); + } + + private partial class MarqueeFlow : FillFlowContainer + { + public required Action OnRequiredParentSizeInvalidated { get; init; } + + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + if (invalidation.HasFlag(Invalidation.RequiredParentSizeToFit)) + OnRequiredParentSizeInvalidated.Invoke(); + + return base.OnInvalidate(invalidation, source); + } } } } From 33e42d280948f66d9c0cc7797bd474d0cbbb999b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Oct 2025 12:11:38 +0200 Subject: [PATCH 025/308] Fix code quality --- osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 69f4aaea4a..a74872eaa7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -55,8 +55,8 @@ namespace osu.Game.Screens.SelectV2 private OsuHoverContainer artistLink = null!; private MarqueeContainer artistLabel = null!; - internal string DisplayedTitle { get; private set; } - internal string DisplayedArtist { get; private set; } + internal string DisplayedTitle { get; private set; } = string.Empty; + internal string DisplayedArtist { get; private set; } = string.Empty; private StatisticPlayCount playCount = null!; private FavouriteButton favouriteButton = null!; From 819da1bc38dbc9676f8f1ee3924bfd8d9cb50347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Oct 2025 13:24:47 +0200 Subject: [PATCH 026/308] SongSelectV2: Scroll to selection instantly after a filter Closes https://github.com/ppy/osu/issues/33379. Pretty sure this matches song select V1. The two call sites where the old one does instant scrolls are: https://github.com/ppy/osu/blob/30412ba3f2c5b6debb9a0e3b930da6cc156852db/osu.Game/Screens/Select/BeatmapCarousel.cs#L672 which happens just after a filter, and https://github.com/ppy/osu/blob/30412ba3f2c5b6debb9a0e3b930da6cc156852db/osu.Game/Screens/Select/BeatmapCarousel.cs#L683 which is a bit more difficult to pin down, but generally appears to happen on changes to the visible items, which on `SongSelectV2` triggers a re-filter anyway. --- osu.Game/Graphics/Carousel/Carousel.cs | 44 +++++++++++++++++++------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 7b8991a0be..4a40862a6f 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -160,7 +160,18 @@ namespace osu.Game.Graphics.Carousel /// /// Scroll carousel to the selected item if available. /// - public void ScrollToSelection() => scrollToSelection.Invalidate(); + /// + /// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels. + /// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation. + /// + public void ScrollToSelection(bool immediate = false) + { + // if an immediate scroll is already requested, don't override it with a slower scroll + if (scrollToSelection == PendingScrollOperation.Immediate) + return; + + scrollToSelection = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard; + } /// /// Returns the vertical spacing between two given carousel items. Negative value can be used to create an overlapping effect. @@ -400,7 +411,7 @@ namespace osu.Game.Graphics.Carousel refreshAfterSelection(); if (!Scroll.UserScrolling) - ScrollToSelection(); + ScrollToSelection(immediate: true); NewItemsPresented?.Invoke(carouselItems); }); @@ -681,6 +692,23 @@ namespace osu.Game.Graphics.Carousel #endregion + #region Scrolling + + /// + /// Scrolling to selection relies on being fully populated. + /// This flag ensures it runs after validates this. + /// + private PendingScrollOperation scrollToSelection = PendingScrollOperation.None; + + private enum PendingScrollOperation + { + None, + Standard, + Immediate, + } + + #endregion + #region Audio private Sample? sampleKeyboardTraversal; @@ -821,12 +849,6 @@ namespace osu.Game.Graphics.Carousel /// private readonly Cached filterReusesPanels = new Cached(); - /// - /// Scrolling to selection relies on being fully populated. - /// This flag ensures it runs after validates this. - /// - private readonly Cached scrollToSelection = new Cached(); - protected override void Update() { base.Update(); @@ -887,12 +909,12 @@ namespace osu.Game.Graphics.Carousel { base.UpdateAfterChildren(); - if (!scrollToSelection.IsValid) + if (scrollToSelection != PendingScrollOperation.None) { if (GetScrollTarget() is double scrollTarget) - Scroll.ScrollTo(scrollTarget - visibleHalfHeight + BleedTop); + Scroll.ScrollTo(scrollTarget - visibleHalfHeight + BleedTop, animated: scrollToSelection == PendingScrollOperation.Standard); - scrollToSelection.Validate(); + scrollToSelection = PendingScrollOperation.None; } } From e240817087c4886de830322fc71d08f2fb2ddb2f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Oct 2025 18:03:28 +0900 Subject: [PATCH 027/308] Move quick play chat entirely to screen footer --- .../TestSceneMatchmakingChatDisplay.cs | 49 +++++++++++++++++++ .../Match/MatchmakingChatDisplay.cs | 20 ++++++++ .../Matchmaking/Match/ScreenMatchmaking.cs | 42 +++++++++------- 3 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs new file mode 100644 index 0000000000..d8e42cd946 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneMatchmakingChatDisplay : ScreenTestScene + { + private MatchmakingChatDisplay? chat; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("add chat", () => + { + chat?.Expire(); + + ScreenFooter.Add(chat = new MatchmakingChatDisplay(new Room()) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(700, 130), + Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Alpha = 0 + }); + }); + + AddStep("show footer", () => ScreenFooter.Show()); + } + + [Test] + public void TestAppearDisappear() + { + AddStep("appear", () => chat!.Appear()); + AddWaitStep("wait for animation", 3); + + AddStep("disappear", () => chat!.Disappear()); + AddWaitStep("wait for animation", 3); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs index 4ff6a3cdf6..6a01642907 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input; @@ -66,5 +68,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public void OnReleased(KeyBindingReleaseEvent e) { } + + public void Appear() + { + FinishTransforms(); + + this.MoveToY(150f) + .FadeOut() + .MoveToY(0f, 240, Easing.OutCubic) + .FadeIn(240, Easing.OutCubic); + } + + public TransformSequence Disappear() + { + FinishTransforms(); + + return this.FadeOut(240, Easing.InOutCubic) + .MoveToY(150f, 240, Easing.InOutCubic); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 9292287c3c..56667822d2 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -29,10 +29,10 @@ using osu.Game.Online.Rooms; 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.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Users; +using osuTK; using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match @@ -87,19 +87,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private MusicController music { get; set; } = null!; private readonly MultiplayerRoom room; + private readonly MatchmakingChatDisplay chat; private Sample? sampleStart; private CancellationTokenSource? downloadCheckCancellation; private int? lastDownloadCheckedBeatmapId; - private MatchChatDisplay chat = null!; - public ScreenMatchmaking(MultiplayerRoom room) { this.room = room; Activity.Value = new UserActivity.InLobby(room); Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; + + chat = new MatchmakingChatDisplay(new Room(room)) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(700, 130), + Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING }, + Alpha = 0 + }; } [BackgroundDependencyLoader] @@ -156,13 +164,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Width = 700, - Height = 130, - Padding = new MarginPadding { Bottom = row_padding }, - Child = chat = new MatchmakingChatDisplay(new Room(room)) - { - RelativeSizeAxes = Axes.Both, - } + Size = new Vector2(700, 130), + Margin = new MarginPadding { Bottom = row_padding } } ] } @@ -183,7 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); - Footer!.Add(chat.CreateProxy()); + Footer?.Add(chat); } private void onRoomUpdated() @@ -326,12 +329,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); + + chat.Appear(); beginHandlingTrack(); } public override void OnSuspending(ScreenTransitionEvent e) { - onLeaving(); + chat.Disappear(); + endHandlingTrack(); + base.OnSuspending(e); } @@ -347,7 +354,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match return true; } - onLeaving(); + chat.Disappear().Expire(); + endHandlingTrack(); + client.LeaveRoom().FireAndForget(); return false; } @@ -370,6 +379,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + + chat.Appear(); beginHandlingTrack(); if (e.Last is not MultiplayerPlayerLoader playerLoader) @@ -384,11 +395,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match client.ChangeState(MultiplayerUserState.Idle); } - private void onLeaving() - { - endHandlingTrack(); - } - /// /// Handles changes in the track to keep it looping while active. /// From a3c78de71077543213050b1dedee06312bc8dec4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Oct 2025 21:00:38 +0900 Subject: [PATCH 028/308] Move context menu from channel to chat overlay --- osu.Game/Overlays/Chat/DrawableChannel.cs | 28 +++++++++-------------- osu.Game/Overlays/ChatOverlay.cs | 7 +++++- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 2f0461eb40..ad327f4b28 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -12,7 +12,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Cursor; using osu.Game.Online.Chat; using osuTK.Graphics; @@ -49,25 +48,20 @@ namespace osu.Game.Overlays.Chat [BackgroundDependencyLoader] private void load() { - Child = new OsuContextMenuContainer + Child = scroll = new ChannelScrollContainer { + ScrollbarVisible = scrollbarVisible, RelativeSizeAxes = Axes.Both, - Masking = true, - Child = scroll = new ChannelScrollContainer + // Some chat lines have effects that slightly protrude to the bottom, + // which we do not want to mask away, hence the padding. + Padding = new MarginPadding { Bottom = 5 }, + Child = ChatLineFlow = new FillFlowContainer { - ScrollbarVisible = scrollbarVisible, - RelativeSizeAxes = Axes.Both, - // Some chat lines have effects that slightly protrude to the bottom, - // which we do not want to mask away, hence the padding. - Padding = new MarginPadding { Bottom = 5 }, - Child = ChatLineFlow = new FillFlowContainer - { - Padding = new MarginPadding { Left = 3, Right = 10 }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - } - }, + Padding = new MarginPadding { Left = 3, Right = 10 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + } }; newMessagesArrived(Channel.Messages); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 7f4ba3e2e2..e7422d6f86 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -19,6 +19,7 @@ using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online; @@ -142,9 +143,13 @@ namespace osu.Game.Overlays new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = currentChannelContainer = new Container + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, + Child = currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + } } }, loading = new LoadingLayer(true), From 613c20836242e8ea5711218db8b4754fa1cbaf0d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Oct 2025 21:26:33 +0900 Subject: [PATCH 029/308] Fix partially offscreen quick play chat context menu --- .../Matchmaking/Match/ScreenMatchmaking.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 56667822d2..95e3cb0236 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -186,7 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); - Footer?.Add(chat); + Footer?.Add(new ChatContainer(chat)); } private void onRoomUpdated() @@ -445,5 +445,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match client.LoadRequested -= onLoadRequested; } } + + // Contains the chat display and a context menu container for it. Shared lifetime with the chat display (expires along with it). + private partial class ChatContainer : CompositeDrawable + { + public override double LifetimeStart => chat.LifetimeStart; + public override double LifetimeEnd => chat.LifetimeEnd; + + private readonly MatchmakingChatDisplay chat; + + public ChatContainer(MatchmakingChatDisplay chat) + { + this.chat = chat; + + Anchor = Anchor.BottomRight; + Origin = Anchor.BottomRight; + + // This component is added to the screen footer which is only about 50px high. + // Therefore, it's given a large absolute size to give the context menu enough space to display correctly. + Size = new Vector2(700); + + InternalChild = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = chat + }; + } + } } } From f96be84c5749d83ac2d1aa7e0b8453ce513b1e7d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Oct 2025 22:23:05 +0900 Subject: [PATCH 030/308] Fix tests --- osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index d7f79d3e30..877dc7eaac 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -471,7 +471,7 @@ namespace osu.Game.Tests.Visual.Online public DrawableChannel DrawableChannel => InternalChildren.OfType().First(); - public ChannelScrollContainer ScrollContainer => (ChannelScrollContainer)((Container)DrawableChannel.Child).Child; + public ChannelScrollContainer ScrollContainer => DrawableChannel.ChildrenOfType().Single(); public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child; From 0558f9f2d9a6d605df2549644a9f3fa65996bfb8 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 24 Oct 2025 22:42:28 +0900 Subject: [PATCH 031/308] Add SFX for 'jumping' in quickplay --- .../Matchmaking/Match/PlayerPanel.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 1480e866a6..d43863b4c0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -7,6 +7,8 @@ using System.Globalization; using System.Linq; using Humanizer; 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; @@ -15,6 +17,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -93,6 +96,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; + private Sample? jumpSample; + private SampleChannel? jumpSampleChannel; + private double samplePitch; + public PlayerPanelDisplayMode DisplayMode { get => displayMode; @@ -128,7 +135,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { Add(SolidBackgroundLayer = new Box { @@ -222,6 +229,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match // Allow avatar to exist outside of masking for when it jumps around and stuff. AddInternal(avatar.CreateProxy()); + + jumpSample = audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump"); } protected override void LoadComplete() @@ -238,6 +247,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match avatar.ScaleTo(0) .ScaleTo(1, 500, Easing.OutElasticHalf) .FadeIn(200); + + // pick a random pitch to be used by the player for duration of this session + samplePitch = 0.75f + RNG.NextDouble(0f, 0.5f); } private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal; @@ -396,6 +408,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match scale.Then().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); + + // only play jump sample if panel is visible + if (Alpha > 0) + playJumpSample(); + break; } @@ -403,6 +420,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } } + private void playJumpSample() + { + jumpSampleChannel?.Stop(); + jumpSampleChannel = jumpSample?.GetChannel(); + + if (jumpSampleChannel == null) + return; + + float horizontalPos = BoundingBox.Centre.X / Parent!.ToLocalSpace(Parent!.ScreenSpaceDrawQuad).Width; + // rescale balance from 0..1 to -1..1 + float balance = -1f + horizontalPos * 2f; + + jumpSampleChannel.Frequency.Value = samplePitch; + jumpSampleChannel.Balance.Value = balance * OsuGameBase.SFX_STEREO_STRENGTH; + jumpSampleChannel?.Play(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 1af462b692e96aa3c2811fd3be2b1307bc8dc158 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Oct 2025 23:07:04 +0900 Subject: [PATCH 032/308] Add very simple countdown timer for quick play stages (#35433) --- .../Match/StageDisplay.TimerText.cs | 106 ++++++++++++++++++ .../Matchmaking/Match/StageDisplay.cs | 6 + .../Multiplayer/TestMultiplayerClient.cs | 2 +- 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.TimerText.cs diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.TimerText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.TimerText.cs new file mode 100644 index 0000000000..e2af3ef945 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.TimerText.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 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.Matchmaking; +using osu.Game.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class StageDisplay + { + public partial class TimerText : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private OsuSpriteText text = null!; + + private DateTimeOffset countdownEndTime; + + public TimerText() + { + AutoSizeAxes = Axes.X; + Height = 18; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = text = new OsuSpriteText + { + Height = 18, + Spacing = new Vector2(-1, 0), + Font = OsuFont.Style.Heading2.With(fixedWidth: true), + AlwaysPresent = true, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.CountdownStarted += onCountdownStarted; + client.CountdownStopped += onCountdownStopped; + + if (client.Room != null) + { + foreach (var countdown in client.Room.ActiveCountdowns) + onCountdownStarted(countdown); + } + } + + protected override void Update() + { + base.Update(); + + TimeSpan remaining = countdownEndTime - DateTimeOffset.Now; + + text.Alpha = remaining.TotalSeconds > 0 ? 1f : 0.2f; + + if (remaining.TotalSeconds > 10) + text.Font = text.Font.With(weight: FontWeight.SemiBold); + else + text.Font = text.Font.With(weight: FontWeight.Bold); + + int minutes = (int)Math.Max(0, remaining.TotalMinutes); + int seconds = Math.Max(0, remaining.Seconds); + int ms = Math.Max(0, remaining.Milliseconds); + + text.Text = $"{minutes:00}:{seconds:00}.{ms:000}"; + } + + private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (countdown is MatchmakingStageCountdown) + countdownEndTime = DateTimeOffset.Now + countdown.TimeRemaining; + }); + + private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (countdown is not MatchmakingStageCountdown) + return; + + countdownEndTime = DateTimeOffset.Now; + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.CountdownStarted -= onCountdownStarted; + client.CountdownStopped -= onCountdownStopped; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs index e428e3b044..b45e8054a0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs @@ -72,6 +72,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Direction = FillDirection.Horizontal, }, }, + new TimerText + { + Y = -38, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, new StatusText { Y = 32, diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index bd16c36eec..5b2876a989 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -851,7 +851,7 @@ namespace osu.Game.Tests.Visual.Multiplayer await StartCountdown(new MatchmakingStageCountdown { Stage = stage, - TimeRemaining = TimeSpan.FromSeconds(10) + TimeRemaining = TimeSpan.FromSeconds(stage == MatchmakingStage.UserBeatmapSelect ? 30 : 10) }).ConfigureAwait(false); } From afdebcf188392efd4494661b19140bc4c9cf5946 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 22 Oct 2025 16:35:29 +0300 Subject: [PATCH 033/308] Make CursorPathContainer a smooth path --- .../UI/ReplayAnalysis/CursorPathContainer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs index 1951d467e2..f23cf5129b 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; using osu.Game.Graphics; @@ -12,7 +11,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class CursorPathContainer : Path + public partial class CursorPathContainer : SmoothPath { private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); @@ -29,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis private void load(OsuColour colours) { Colour = colours.Pink2; - BackgroundColour = colours.Pink2.Opacity(0); } protected override void Update() From 72fa1553c317abfe27d76126d86bb9a05323d069 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Oct 2025 13:46:23 +0900 Subject: [PATCH 034/308] Add settings toggle for experimental BASS initialisation mode --- .../Sections/Audio/AudioDevicesSettings.cs | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index a71f2a6d29..4a9130db89 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -1,13 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using System.Collections.Generic; using System.Linq; +using osu.Framework; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -19,9 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio protected override LocalisableString Header => AudioSettingsStrings.AudioDevicesHeader; [Resolved] - private AudioManager audio { get; set; } + private AudioManager audio { get; set; } = null!; - private SettingsDropdown dropdown; + private SettingsDropdown dropdown = null!; + + private SettingsCheckbox? wasapiExperimental; [BackgroundDependencyLoader] private void load() @@ -32,9 +34,22 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { LabelText = AudioSettingsStrings.OutputDevice, Keywords = new[] { "speaker", "headphone", "output" } - } + }, }; + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + { + Add(wasapiExperimental = new SettingsCheckbox + { + LabelText = "Use experimental audio mode", + TooltipText = "This will attempt to initialise the WASAPI engine in a lower latency mode.", + Current = audio.UseExperimentalWasapi, + Keywords = new[] { "wasapi", "latency", "exclusive" } + }); + + wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty); + } + updateItems(); audio.OnNewDevice += onDeviceChanged; @@ -42,7 +57,21 @@ namespace osu.Game.Overlays.Settings.Sections.Audio dropdown.Current = audio.AudioDevice; } - private void onDeviceChanged(string name) => updateItems(); + private void onDeviceChanged(string _) + { + updateItems(); + + if (wasapiExperimental != null) + { + if (wasapiExperimental.Current.Value) + { + wasapiExperimental.SetNoticeText( + "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.", true); + } + else + wasapiExperimental.ClearNoticeText(); + } + } private void updateItems() { @@ -61,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio // functionality would require involved OS-specific code. dropdown.Items = deviceItems // Dropdown doesn't like null items. Somehow we are seeing some arrive here (see https://github.com/ppy/osu/issues/21271) - .Where(i => i != null) + .Where(i => i.IsNotNull()) .Distinct() .ToList(); } @@ -70,7 +99,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { base.Dispose(isDisposing); - if (audio != null) + if (audio.IsNotNull()) { audio.OnNewDevice -= onDeviceChanged; audio.OnLostDevice -= onDeviceChanged; From 79a76ce58734bd6a27be096c8dae19091566dbec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Oct 2025 17:35:48 +0900 Subject: [PATCH 035/308] Update AudioDevicesSettings.cs Co-authored-by: Dan Balasescu --- .../Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index 4a9130db89..b1c735e745 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio Add(wasapiExperimental = new SettingsCheckbox { LabelText = "Use experimental audio mode", - TooltipText = "This will attempt to initialise the WASAPI engine in a lower latency mode.", + TooltipText = "This will attempt to initialise the audio engine in a lower latency mode.", Current = audio.UseExperimentalWasapi, Keywords = new[] { "wasapi", "latency", "exclusive" } }); From 9ca47fc53a2d4c45f304e6de0d2118a44ce2bbb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Oct 2025 19:41:28 +0900 Subject: [PATCH 036/308] 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 f2853eaaa8..d05589ea8a 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 be7df2f771..28faf49455 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 473fb5720ca3d49aeed40e307ab032ff4deb1b30 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 12:11:11 +0900 Subject: [PATCH 037/308] Disable Discord invites to quick play rooms --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 668f63b910..bbdb719b05 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -189,7 +189,7 @@ namespace osu.Desktop } // user party - if (!hideIdentifiableInformation && multiplayerClient.Room != null) + if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking) { MultiplayerRoom room = multiplayerClient.Room; From 765b9a20b5a738c31c697e3a07ffac2529bfcf5c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 12:13:06 +0900 Subject: [PATCH 038/308] Hide quick play room name in Discord rich presence --- osu.Game/Users/UserActivity.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index b7b6c6f366..86c84c0bb2 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -274,8 +274,16 @@ namespace osu.Game.Users public InLobby(MultiplayerRoom room) { - RoomID = room.RoomID; - RoomName = room.Settings.Name; + if (room.Settings.MatchType == MatchType.Matchmaking) + { + RoomID = -1; + RoomName = "Quick Play"; + } + else + { + RoomID = room.RoomID; + RoomName = room.Settings.Name; + } } [SerializationConstructor] From 08621c4cc900e0e2fcb370eb1efb5ea3c2ef40aa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 18:30:42 +0900 Subject: [PATCH 039/308] Refactor panel structure --- .../Matchmaking/Match/PlayerPanel.cs | 195 +++++++++--------- 1 file changed, 96 insertions(+), 99 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 1480e866a6..5f36e64dd9 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -48,6 +48,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public readonly MultiplayerRoomUser RoomUser; + /// + /// Perform an action in addition to showing the user's profile. + /// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX). + /// + public new Action? Action; + [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -81,6 +87,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match [Resolved] private MetadataClient? metadataClient { get; set; } + public readonly APIUser User; + private readonly Action viewProfile; + private OsuSpriteText rankText = null!; private OsuSpriteText scoreText = null!; @@ -91,33 +100,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private Container mainContent = null!; + private Box solidBackgroundLayer = null!; + private Drawable background = null!; + private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; - public PlayerPanelDisplayMode DisplayMode - { - get => displayMode; - set - { - displayMode = value; - if (IsLoaded) - updateLayout(false); - } - } - - public readonly APIUser User; - - /// - /// Perform an action in addition to showing the user's profile. - /// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX). - /// - public new Action? Action; - - protected Action ViewProfile { get; private set; } = null!; - - public Box SolidBackgroundLayer { get; private set; } = null!; - - protected Drawable? Background { get; private set; } - public PlayerPanel(MultiplayerRoomUser user) : base(HoverSampleSet.Button) { @@ -125,100 +112,99 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match User = user.User; RoomUser = user; + + base.Action = viewProfile = () => + { + Action?.Invoke(); + profileOverlay?.ShowUser(User); + }; } [BackgroundDependencyLoader] private void load() { - Add(SolidBackgroundLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider?.Background5 ?? colours.Gray1 - }); - - Background = new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colours.Gray7, - User = User - }; - if (Background != null) - Add(Background); - - base.Action = ViewProfile = () => - { - Action?.Invoke(); - profileOverlay?.ShowUser(User); - }; - Content.Masking = true; Content.CornerRadius = 10; Content.CornerExponent = 10; Content.Anchor = Anchor.Centre; Content.Origin = Anchor.Centre; - Add(new Container + Children = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Child = mainContent = new Container + solidBackgroundLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider?.Background5 ?? colours.Gray1 + }, + background = new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.Gray7, + User = User + }, + new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Children = new[] + Child = mainContent = new Container { - avatarPositionTarget = new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] { - Origin = Anchor.Centre, - Size = avatar_size, - Child = avatarJumpTarget = new Container + avatarPositionTarget = new Container { + Origin = Anchor.Centre, + 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 + { + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomCentre, + Blending = BlendingParameters.Additive, + Margin = new MarginPadding(4), + Text = "-", + Font = OsuFont.Style.Title.With(size: 55), + }, + username = new OsuSpriteText + { + Alpha = 0, 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 - } + Text = User.Username, + Font = OsuFont.Style.Heading1, + }, + scoreText = new OsuSpriteText + { + Alpha = 0, + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Style.Heading2, + Text = "0 pts" } - }, - rankText = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomCentre, - Blending = BlendingParameters.Additive, - Margin = new MarginPadding(4), - Text = "-", - Font = OsuFont.Style.Title.With(size: 55), - }, - username = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = User.Username, - Font = OsuFont.Style.Heading1, - }, - scoreText = new OsuSpriteText - { - Alpha = 0, - Margin = new MarginPadding(10), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Font = OsuFont.Style.Heading2, - Text = "0 pts" } } } - }); + }; // Allow avatar to exist outside of masking for when it jumps around and stuff. AddInternal(avatar.CreateProxy()); @@ -240,6 +226,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match .FadeIn(200); } + public PlayerPanelDisplayMode DisplayMode + { + get => displayMode; + set + { + displayMode = value; + if (IsLoaded) + updateLayout(false); + } + } + private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal; private Vector2 avatarPosition @@ -276,16 +273,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match scoreText.Hide(); username.Hide(); - Background.FadeOut(200, Easing.OutQuint); - SolidBackgroundLayer.FadeOut(200, Easing.OutQuint); + background.FadeOut(200, Easing.OutQuint); + solidBackgroundLayer.FadeOut(200, Easing.OutQuint); this.ResizeTo(avatar_size, duration, Easing.OutPow10); break; case PlayerPanelDisplayMode.Horizontal: case PlayerPanelDisplayMode.Vertical: - Background.FadeIn(200); - SolidBackgroundLayer.FadeIn(200); + background.FadeIn(200); + solidBackgroundLayer.FadeIn(200); using (BeginDelayedSequence(100)) { @@ -420,7 +417,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { List items = new List { - new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, ViewProfile) + new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, viewProfile) }; if (User.Equals(api.LocalUser.Value)) From b7c07ad0e5a6c1482d353be70b79e4d52519cfa7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 19:04:16 +0900 Subject: [PATCH 040/308] Add support for marking panels as quit --- .../Matchmaking/TestScenePlayerPanel.cs | 6 + .../Matchmaking/Match/PlayerPanel.cs | 172 ++++++++++++------ 2 files changed, 118 insertions(+), 60 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index f64c7c9443..21567daabe 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -102,5 +102,11 @@ namespace osu.Game.Tests.Visual.Matchmaking { AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely()); } + + [Test] + public void TestQuit() + { + AddToggleStep("toggle quit", quit => panel.HasQuit = quit); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 5f36e64dd9..6884312f3d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private Drawable avatarPositionTarget = null!; private Drawable avatarJumpTarget = null!; - private MatchmakingAvatar avatar = null!; + private Drawable avatar = null!; private OsuSpriteText username = null!; private Container mainContent = null!; @@ -103,7 +103,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private Box solidBackgroundLayer = null!; private Drawable background = null!; + private OsuSpriteText quitText = null!; + private BufferedContainer backgroundQuitTarget = null!; + private BufferedContainer avatarQuitTarget = null!; + private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; + private bool hasQuit; public PlayerPanel(MultiplayerRoomUser user) : base(HoverSampleSet.Button) @@ -129,77 +134,99 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Content.Anchor = Anchor.Centre; Content.Origin = Anchor.Centre; - Children = new[] + Child = backgroundQuitTarget = new BufferedContainer { - solidBackgroundLayer = new Box + RelativeSizeAxes = Axes.Both, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider?.Background5 ?? colours.Gray1 - }, - background = new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colours.Gray7, - User = User - }, - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Child = mainContent = new Container + solidBackgroundLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider?.Background5 ?? colours.Gray1 + }, + background = new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.Gray7, + User = User + }, + new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Children = new[] + Child = mainContent = new Container { - avatarPositionTarget = new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] { - Origin = Anchor.Centre, - Size = avatar_size, - Child = avatarJumpTarget = new Container + quitText = new OsuSpriteText { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "QUIT", + Font = OsuFont.Default.With(weight: "Bold", size: 70), + Rotation = -22.5f, + Colour = OsuColour.Gray(0.3f), + Blending = BlendingParameters.Additive + }, + avatarPositionTarget = new Container + { + Origin = Anchor.Centre, + Size = avatar_size, + Child = avatarJumpTarget = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Child = avatar = new Container + { + RelativeSizeAxes = Axes.Both, + Child = avatarQuitTarget = new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Child = 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 + { + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomCentre, + Blending = BlendingParameters.Additive, + Margin = new MarginPadding(4), + Text = "-", + Font = OsuFont.Style.Title.With(size: 55), + }, + username = new OsuSpriteText + { + Alpha = 0, 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 - } + Text = User.Username, + Font = OsuFont.Style.Heading1, + }, + scoreText = new OsuSpriteText + { + Alpha = 0, + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Style.Heading2, + Text = "0 pts" } - }, - rankText = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomCentre, - Blending = BlendingParameters.Additive, - Margin = new MarginPadding(4), - Text = "-", - Font = OsuFont.Style.Title.With(size: 55), - }, - username = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = User.Username, - Font = OsuFont.Style.Heading1, - }, - scoreText = new OsuSpriteText - { - Alpha = 0, - Margin = new MarginPadding(10), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Font = OsuFont.Style.Heading2, - Text = "0 pts" } } } @@ -237,6 +264,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } } + public bool HasQuit + { + get => hasQuit; + set + { + hasQuit = value; + if (IsLoaded) + updateLayout(false); + } + } + private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal; private Vector2 avatarPosition @@ -304,11 +342,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match rankText.MoveTo(horizontal ? new Vector2(-40, -20) : 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); + quitText.MoveTo(horizontal ? new Vector2(40, 0) : new Vector2(0, 40), duration, Easing.OutPow10); break; default: throw new ArgumentOutOfRangeException(); } + + if (HasQuit) + { + backgroundQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10); + avatarQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10); + quitText.FadeIn(duration, Easing.OutPow10); + } + else + { + backgroundQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10); + avatarQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10); + quitText.FadeOut(duration, Easing.OutPow10); + } } protected override bool OnHover(HoverEvent e) From bb578d254dc7f74900bca069b4d8cb8eb17e191c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 19:06:55 +0900 Subject: [PATCH 041/308] Mark panels as quit instead of removing --- .../Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index 510698f46e..9fb5d258a8 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => { - panels.Single(p => p.RoomUser.Equals(user)).Expire(); + panels.Single(p => p.RoomUser.Equals(user)).HasQuit = true; updateDisplay(); }); From 98eb29c43d75c52efbf6492ecf6ded84e8c59e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Oct 2025 10:35:48 +0100 Subject: [PATCH 042/308] Add failing test --- .../TestSceneSongSelectFiltering.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 076d84479a..eeeb6f7297 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -88,6 +88,33 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("selection unchanged", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.Last())); } + [Test] + public void TestFilterSingleResult_ReselectedAfterRulesetSwitches() + { + LoadSongSelect(); + + ImportBeatmapForRuleset(0); + ImportBeatmapForRuleset(0); + + AddStep("disable converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + AddStep("set filter text", () => filterTextBox.Current.Value = $"\"{Beatmaps.GetAllUsableBeatmapSets().Last().Metadata.Title}\""); + + AddWaitStep("wait for debounce", 5); + AddUntilStep("wait for filter", () => !Carousel.IsFiltering); + AddUntilStep("selection is second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.First())); + + AddStep("select last difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapSetInfo.Beatmaps.Last())); + AddUntilStep("selection is last difficulty of second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.Last())); + + ChangeRuleset(1); + AddUntilStep("wait for filter", () => !Carousel.IsFiltering); + AddUntilStep("selection is default", () => Beatmap.IsDefault); + + ChangeRuleset(0); + AddUntilStep("wait for filter", () => !Carousel.IsFiltering); + AddUntilStep("selection is last difficulty of second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.Last())); + } + [Test] public void TestFilterOnResumeAfterChange() { From e61ae7ab8a0e68dafb83d43575770ea8c3bc4206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Oct 2025 11:06:38 +0100 Subject: [PATCH 043/308] Fix single filtered selection not being reselected after being filtered away Closes https://github.com/ppy/osu/issues/35003. Bit dodgy to use `CurrentSelectionItem` for this. Ideally I would use the global `Beatmap.IsDefault`, but I kind of don't want to violate the rule that `BeatmapCarousel` shouldn't have direct access to the global beatmap. And this seems to work, so... maybe fine to use until it doesn't? --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d6bd9c1db1..5e84ba0722 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -561,8 +561,19 @@ namespace osu.Game.Screens.SelectV2 var beatmaps = items.Select(i => i.Model).OfType(); - if (beatmaps.Any(b => b.Equals(CurrentSelection as GroupedBeatmap))) + // do not request recommended selection if the user already had selected a difficulty within the single filtered beatmap set, + // as it could change the difficulty that will be selected + var preexistingSelection = beatmaps.FirstOrDefault(b => b.Equals(CurrentSelection as GroupedBeatmap)); + + if (preexistingSelection != null) + { + // the selection might not have an item associated with it, if it was fully filtered away previously + // in this case, request to reselect it + if (CurrentSelectionItem == null) + RequestSelection(preexistingSelection); + return; + } RequestRecommendedSelection(beatmaps); } From f8769d2e443d28228aae377c6152b7fef1375a7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Oct 2025 22:59:02 +0900 Subject: [PATCH 044/308] Fix WASAPI settings notice text not displaying on startup --- .../Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index b1c735e745..5b5617bae0 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -50,11 +50,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty); } - updateItems(); - audio.OnNewDevice += onDeviceChanged; audio.OnLostDevice += onDeviceChanged; dropdown.Current = audio.AudioDevice; + + onDeviceChanged(string.Empty); } private void onDeviceChanged(string _) From 3c37ac17184370c695f0fd79a7641232147e5cf9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 23:19:28 +0900 Subject: [PATCH 045/308] Fix clipped outline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Matchmaking/Match/MatchmakingAvatar.cs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs index 53db2114c7..e0f46d89f0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { RelativeSizeAxes = Axes.Both, Depth = float.MaxValue, - Padding = new MarginPadding(-2), Child = new FastCircle { RelativeSizeAxes = Axes.Both, @@ -50,20 +49,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match }); } - AddInternal(new CircularContainer + AddInternal(new Container { + Padding = new MarginPadding(2), RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] + Child = new CircularContainer { - new Box + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4.LightSlateGray, - }, - new ClickableAvatar(user, true) - { - RelativeSizeAxes = Axes.Both, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.LightSlateGray, + }, + new ClickableAvatar(user, true) + { + RelativeSizeAxes = Axes.Both, + } } } }); From ce3b8bc77b55bb5164a5668b4f7c084745e55131 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 15:07:37 +0900 Subject: [PATCH 046/308] 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 d05589ea8a..8917bc9339 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 28faf49455..7e219e4b1d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 8b2b6517ca8417a3c8cbb723ea6899cedffb819f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 15:41:40 +0900 Subject: [PATCH 047/308] Fix regression of avatar animation --- osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 6884312f3d..fa4c8a11b9 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -185,6 +185,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match RelativeSizeAxes = Axes.Both, Child = avatar = new Container { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Child = avatarQuitTarget = new BufferedContainer { From 0205cf0fb99f550d5926702c2142cb9f91b96790 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 15:43:25 +0900 Subject: [PATCH 048/308] Render frame buffers at a higher resolution to fix blurry for now --- osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index fa4c8a11b9..0d5f36585c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -136,6 +136,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Child = backgroundQuitTarget = new BufferedContainer { + FrameBufferScale = new Vector2(1.5f), RelativeSizeAxes = Axes.Both, Children = new[] { @@ -188,8 +189,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, + // Needs to be re-buffered as the avatar is proxied outside of the parent buffered container. Child = avatarQuitTarget = new BufferedContainer { + FrameBufferScale = new Vector2(1.5f), RelativeSizeAxes = Axes.Both, Child = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) { From 22f11b6fa536ee2d74855c1bfb01bee4f4fc6f43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 16:30:31 +0900 Subject: [PATCH 049/308] Update test in line with new quit panel behaviour --- .../Visual/Matchmaking/TestScenePlayerPanelOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs index d5ab571a7d..c2b2b95d55 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs @@ -118,9 +118,12 @@ namespace osu.Game.Tests.Visual.Matchmaking }); AddUntilStep("two panels displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddAssert("no panels quit", () => this.ChildrenOfType().Count(p => p.HasQuit), () => Is.EqualTo(0)); AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); - AddUntilStep("one panel displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + + AddUntilStep("one panel quit", () => this.ChildrenOfType().Count(p => p.HasQuit), () => Is.EqualTo(1)); + AddAssert("two panels still displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] From 960170808715ea93d7a48496d59876fb13949bf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 18:28:58 +0900 Subject: [PATCH 050/308] Fix quit text on avatar only mode, fix avatar fade --- .../OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 0d5f36585c..e86a546533 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -354,20 +354,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match throw new ArgumentOutOfRangeException(); } + // quit text doesn't fit on avataronly mode. + if (HasQuit && displayMode != PlayerPanelDisplayMode.AvatarOnly) + quitText.FadeIn(duration, Easing.OutPow10); + else + quitText.FadeOut(duration, Easing.OutPow10); + if (HasQuit) { backgroundQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10); avatarQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10); - quitText.FadeIn(duration, Easing.OutPow10); } else { backgroundQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10); avatarQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10); - quitText.FadeOut(duration, Easing.OutPow10); } } + protected override void Update() + { + base.Update(); + + // Not sure why this is required but it is. + avatarQuitTarget.Alpha = Alpha; + } + protected override bool OnHover(HoverEvent e) { Content.ScaleTo(1.03f, 2000, Easing.OutPow10); From a40230da4b7fd5484ac7f3821cdecdb29ad87d3c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 28 Oct 2025 19:35:15 +0900 Subject: [PATCH 051/308] Ensure to never display "0th" placement --- osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index e86a546533..0eaf6c7a81 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -414,6 +414,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore)) return; + if (userScore.Placement == 0) + return; + rankText.Text = userScore.Placement.Ordinalize(CultureInfo.CurrentCulture); rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement)); scoreText.Text = $"{userScore.Points} pts"; From c524bf54325589393d48ce30ff49861815e7e4e3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 28 Oct 2025 20:39:09 +0900 Subject: [PATCH 052/308] Make `MachmakingUser.Placement` nullable --- .../MatchTypes/Matchmaking/MatchmakingUser.cs | 2 +- .../OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 6 +++--- .../Matchmaking/Match/PlayerPanelOverlay.cs | 4 ++-- .../Matchmaking/Match/Results/SubScreenResults.cs | 13 ++++++++----- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs index f596f2473e..ac97b114d8 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs @@ -23,7 +23,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking /// The aggregate room placement (1-based). /// [Key(1)] - public int Placement { get; set; } + public int? Placement { get; set; } /// /// The aggregate points. diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 0eaf6c7a81..e2455eb020 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -414,11 +414,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore)) return; - if (userScore.Placement == 0) + if (userScore.Placement == null) return; - rankText.Text = userScore.Placement.Ordinalize(CultureInfo.CurrentCulture); - rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement)); + rankText.Text = userScore.Placement.Value.Ordinalize(CultureInfo.CurrentCulture); + rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement.Value)); scoreText.Text = $"{userScore.Points} pts"; }); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index 9fb5d258a8..4b97400ebe 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -239,8 +239,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match 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); + if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user) && user.Placement != null) + SetLayoutPosition(Children[i], user.Placement.Value); else SetLayoutPosition(Children[i], float.MaxValue); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs index 797519a53c..b533a84b28 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs @@ -201,13 +201,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results return; } - int overallPlacement = state.Users[client.LocalUser!.UserID].Placement; + int? overallPlacement = state.Users[client.LocalUser!.UserID].Placement; - placementText.Text = overallPlacement.Ordinalize(CultureInfo.CurrentCulture); - placementText.Colour = ColourForPlacement(overallPlacement); + if (overallPlacement != null) + { + placementText.Text = overallPlacement.Value.Ordinalize(CultureInfo.CurrentCulture); + placementText.Colour = ColourForPlacement(overallPlacement.Value); - int overallPoints = state.Users[client.LocalUser!.UserID].Points; - addStatistic(overallPlacement, $"Overall position ({overallPoints} points)"); + int overallPoints = state.Users[client.LocalUser!.UserID].Points; + addStatistic(overallPlacement.Value, $"Overall position ({overallPoints} points)"); + } var accuracyOrderedUsers = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average())) .OrderByDescending(t => t.avgAcc) From 87b66685d6641957615eb91ebd30088cf5915ba3 Mon Sep 17 00:00:00 2001 From: Glacc Date: Tue, 28 Oct 2025 19:42:47 +0800 Subject: [PATCH 053/308] Always show HUD while editing skin layout. --- osu.Game/Screens/Play/HUDOverlay.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 806e593729..c6db8c2af6 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -19,6 +19,7 @@ using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -113,6 +114,9 @@ namespace osu.Game.Screens.Play /// internal readonly Drawable PlayfieldSkinLayer; + [CanBeNull] + private SkinEditorOverlay skinEditor; + public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods) { Container rightSettings; @@ -194,7 +198,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(OsuConfigManager config, RealmKeyBindingStore keyBindingStore, INotificationOverlay notificationOverlay) + private void load(OsuConfigManager config, RealmKeyBindingStore keyBindingStore, INotificationOverlay notificationOverlay, [CanBeNull] SkinEditorOverlay skinEditor) { if (drawableRuleset != null) { @@ -207,6 +211,9 @@ namespace osu.Game.Screens.Play configLeaderboardVisibility = config.GetBindable(OsuSetting.GameplayLeaderboard); configSettingsOverlay = config.GetBindable(OsuSetting.ReplaySettingsOverlay); + skinEditor?.State.BindValueChanged(_ => updateVisibility()); + this.skinEditor = skinEditor; + if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce) { hasShownNotificationOnce = true; @@ -347,6 +354,16 @@ namespace osu.Game.Screens.Play if (ShowHud.Disabled) return; + // Always show HUD while editing skin layout. + if (skinEditor != null) + { + if (skinEditor.State.Value == Visibility.Visible) + { + ShowHud.Value = true; + return; + } + } + if (holdingForHUD.Value) { ShowHud.Value = true; From 378c64b7f89a1ac29110799c9bde54ca8974a7ef Mon Sep 17 00:00:00 2001 From: Glacc Date: Tue, 28 Oct 2025 21:21:07 +0800 Subject: [PATCH 054/308] Only set HUD visibility mode to non-Never when skin layout editor is visible by saving and restoring HUD visibility mode setting. --- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 25 +++++++++++++++++++ osu.Game/Screens/Play/HUDOverlay.cs | 19 +------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 27317518a0..45b13466ba 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -79,6 +79,9 @@ namespace osu.Game.Overlays.SkinEditor private readonly LayoutValue drawSizeLayout; + private Bindable configVisibilityMode; + private HUDVisibilityMode previousHUDVisibility; + public SkinEditorOverlay(ScalingContainer scalingContainer) { this.scalingContainer = scalingContainer; @@ -91,6 +94,8 @@ namespace osu.Game.Overlays.SkinEditor private void load(OsuConfigManager config) { config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); + + configVisibilityMode = config.GetBindable(OsuSetting.HUDVisibilityMode); } protected override void LoadComplete() @@ -98,6 +103,8 @@ namespace osu.Game.Overlays.SkinEditor base.LoadComplete(); externalEditOverlayRegistration = overlayManager?.RegisterBlockingOverlay(externalEditOverlay); + + State.BindValueChanged(_ => saveAndRestoreHUDVisibility()); } public bool OnPressed(KeyBindingPressEvent e) @@ -350,6 +357,24 @@ namespace osu.Game.Overlays.SkinEditor leasedBeatmapSkins = null; } + private void saveAndRestoreHUDVisibility() + { + // Make HUD visible while editing skin layout + if (State.Value == Visibility.Visible) + { + previousHUDVisibility = configVisibilityMode.Value; + // only when HUD visibility mode is set to Never. + if (configVisibilityMode.Value == HUDVisibilityMode.Never) + configVisibilityMode.Value = HUDVisibilityMode.Always; + } + else + { + // and restore it to Never after closing editor. + if (previousHUDVisibility == HUDVisibilityMode.Never) + configVisibilityMode.Value = HUDVisibilityMode.Never; + } + } + public new void ToggleVisibility() { if (skinEditor?.ExternalEditInProgress == true) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index c6db8c2af6..806e593729 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -19,7 +19,6 @@ using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -114,9 +113,6 @@ namespace osu.Game.Screens.Play /// internal readonly Drawable PlayfieldSkinLayer; - [CanBeNull] - private SkinEditorOverlay skinEditor; - public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods) { Container rightSettings; @@ -198,7 +194,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(OsuConfigManager config, RealmKeyBindingStore keyBindingStore, INotificationOverlay notificationOverlay, [CanBeNull] SkinEditorOverlay skinEditor) + private void load(OsuConfigManager config, RealmKeyBindingStore keyBindingStore, INotificationOverlay notificationOverlay) { if (drawableRuleset != null) { @@ -211,9 +207,6 @@ namespace osu.Game.Screens.Play configLeaderboardVisibility = config.GetBindable(OsuSetting.GameplayLeaderboard); configSettingsOverlay = config.GetBindable(OsuSetting.ReplaySettingsOverlay); - skinEditor?.State.BindValueChanged(_ => updateVisibility()); - this.skinEditor = skinEditor; - if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce) { hasShownNotificationOnce = true; @@ -354,16 +347,6 @@ namespace osu.Game.Screens.Play if (ShowHud.Disabled) return; - // Always show HUD while editing skin layout. - if (skinEditor != null) - { - if (skinEditor.State.Value == Visibility.Visible) - { - ShowHud.Value = true; - return; - } - } - if (holdingForHUD.Value) { ShowHud.Value = true; From 9237c76942af5bc184d092ddcbba7a845fc34601 Mon Sep 17 00:00:00 2001 From: Glacc Date: Tue, 28 Oct 2025 21:38:28 +0800 Subject: [PATCH 055/308] And make HUD visibility mode lease when Skin Layout Editor is visible. --- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 45b13466ba..4db437f333 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -80,6 +80,7 @@ namespace osu.Game.Overlays.SkinEditor private readonly LayoutValue drawSizeLayout; private Bindable configVisibilityMode; + private LeasedBindable? leasedVisibilityMode; private HUDVisibilityMode previousHUDVisibility; public SkinEditorOverlay(ScalingContainer scalingContainer) @@ -363,15 +364,19 @@ namespace osu.Game.Overlays.SkinEditor if (State.Value == Visibility.Visible) { previousHUDVisibility = configVisibilityMode.Value; - // only when HUD visibility mode is set to Never. - if (configVisibilityMode.Value == HUDVisibilityMode.Never) - configVisibilityMode.Value = HUDVisibilityMode.Always; + + leasedVisibilityMode = configVisibilityMode.BeginLease(false); + // only when HUD visibility mode is not set to Always. + if (leasedVisibilityMode.Value != HUDVisibilityMode.Always) + leasedVisibilityMode.Value = HUDVisibilityMode.Always; } else { - // and restore it to Never after closing editor. - if (previousHUDVisibility == HUDVisibilityMode.Never) - configVisibilityMode.Value = HUDVisibilityMode.Never; + if (leasedVisibilityMode != null) + leasedVisibilityMode.Value = previousHUDVisibility; + + leasedVisibilityMode?.Return(); + leasedVisibilityMode = null; } } From a78b456e207981a229bf0bb209758af5a4480a94 Mon Sep 17 00:00:00 2001 From: Glacc Date: Tue, 28 Oct 2025 21:42:34 +0800 Subject: [PATCH 056/308] Revert value after closing editor. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 4db437f333..1425ff1e64 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -81,7 +81,6 @@ namespace osu.Game.Overlays.SkinEditor private Bindable configVisibilityMode; private LeasedBindable? leasedVisibilityMode; - private HUDVisibilityMode previousHUDVisibility; public SkinEditorOverlay(ScalingContainer scalingContainer) { @@ -363,18 +362,13 @@ namespace osu.Game.Overlays.SkinEditor // Make HUD visible while editing skin layout if (State.Value == Visibility.Visible) { - previousHUDVisibility = configVisibilityMode.Value; - - leasedVisibilityMode = configVisibilityMode.BeginLease(false); + leasedVisibilityMode = configVisibilityMode.BeginLease(true); // only when HUD visibility mode is not set to Always. if (leasedVisibilityMode.Value != HUDVisibilityMode.Always) leasedVisibilityMode.Value = HUDVisibilityMode.Always; } else { - if (leasedVisibilityMode != null) - leasedVisibilityMode.Value = previousHUDVisibility; - leasedVisibilityMode?.Return(); leasedVisibilityMode = null; } From 6d597fc8159f7c5a7df5677dd4a752da9fde902f Mon Sep 17 00:00:00 2001 From: Glacc Date: Tue, 28 Oct 2025 21:51:21 +0800 Subject: [PATCH 057/308] Null check for configVisibilityMode. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 1425ff1e64..58c3cdd4f4 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.SkinEditor private readonly LayoutValue drawSizeLayout; - private Bindable configVisibilityMode; + private Bindable? configVisibilityMode; private LeasedBindable? leasedVisibilityMode; public SkinEditorOverlay(ScalingContainer scalingContainer) @@ -362,10 +362,13 @@ namespace osu.Game.Overlays.SkinEditor // Make HUD visible while editing skin layout if (State.Value == Visibility.Visible) { - leasedVisibilityMode = configVisibilityMode.BeginLease(true); + leasedVisibilityMode = configVisibilityMode?.BeginLease(true); // only when HUD visibility mode is not set to Always. - if (leasedVisibilityMode.Value != HUDVisibilityMode.Always) - leasedVisibilityMode.Value = HUDVisibilityMode.Always; + if (leasedVisibilityMode != null) + { + if (leasedVisibilityMode.Value != HUDVisibilityMode.Always) + leasedVisibilityMode.Value = HUDVisibilityMode.Always; + } } else { From 89fffa5a1ae6bf2ff2ab2f2527b95aaaae772195 Mon Sep 17 00:00:00 2001 From: Glacc Date: Tue, 28 Oct 2025 22:54:07 +0800 Subject: [PATCH 058/308] Code quality fix. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 58c3cdd4f4..7755e55cb3 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -363,9 +363,9 @@ namespace osu.Game.Overlays.SkinEditor if (State.Value == Visibility.Visible) { leasedVisibilityMode = configVisibilityMode?.BeginLease(true); - // only when HUD visibility mode is not set to Always. if (leasedVisibilityMode != null) { + // only when HUD visibility mode is not set to Always. if (leasedVisibilityMode.Value != HUDVisibilityMode.Always) leasedVisibilityMode.Value = HUDVisibilityMode.Always; } From c779e142e65a1fed585335ed39314b14835c393f Mon Sep 17 00:00:00 2001 From: Glacc Date: Tue, 28 Oct 2025 23:04:09 +0800 Subject: [PATCH 059/308] Code quality fix. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 7755e55cb3..e5b0f3f307 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -363,6 +363,7 @@ namespace osu.Game.Overlays.SkinEditor if (State.Value == Visibility.Visible) { leasedVisibilityMode = configVisibilityMode?.BeginLease(true); + if (leasedVisibilityMode != null) { // only when HUD visibility mode is not set to Always. From 627fec2e3a6dca3c6aff4003a4eefddd9d3d9bb8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 11:18:23 +0900 Subject: [PATCH 060/308] Add failing test case --- .../Visual/Matchmaking/TestSceneResultsScreen.cs | 15 +++++++++++++++ .../Matchmaking/Match/Results/SubScreenResults.cs | 14 +++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs index 4d1a40cc10..80bf660226 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -137,5 +137,20 @@ namespace osu.Game.Tests.Visual.Matchmaking MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); } + + [Test] + public void TestNoUsers() + { + AddStep("show results with no users", () => + { + var state = new MatchmakingRoomState + { + CurrentRound = 6, + Stage = MatchmakingStage.Ended + }; + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs index 797519a53c..9e47d161ba 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs @@ -255,27 +255,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results roomAwards.Clear(); long maxScore = long.MinValue; - int maxScoreUserId = 0; + int maxScoreUserId = -1; double maxAccuracy = double.MinValue; - int maxAccuracyUserId = 0; + int maxAccuracyUserId = -1; int maxCombo = int.MinValue; - int maxComboUserId = 0; + int maxComboUserId = -1; long maxBonusScore = 0; - int maxBonusScoreUserId = 0; + int maxBonusScoreUserId = -1; long largestScoreDifference = long.MinValue; - int largestScoreDifferenceUserId = 0; + int largestScoreDifferenceUserId = -1; long smallestScoreDifference = long.MaxValue; - int smallestScoreDifferenceUserId = 0; + int smallestScoreDifferenceUserId = -1; for (int round = 1; round <= state.CurrentRound; round++) { long roundHighestScore = long.MinValue; - int roundHighestScoreUserId = 0; + int roundHighestScoreUserId = -1; long roundLowestScore = long.MaxValue; From 7b0121a43038ef46785fc0e08095cc3ffca820b0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 28 Oct 2025 20:46:48 +0900 Subject: [PATCH 061/308] Fix quick play results screen when no one plays --- .../Matchmaking/Match/Results/SubScreenResults.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs index 9e47d161ba..403b2836e5 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs @@ -344,11 +344,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results } } - addAward(maxScoreUserId, "Score champ", "Highest score in a single round"); + if (maxScoreUserId > 0) + addAward(maxScoreUserId, "Score champ", "Highest score in a single round"); - addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round"); + if (maxAccuracyUserId > 0) + addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round"); - addAward(maxComboUserId, "Top combo", "Highest combo in a single round"); + if (maxComboUserId > 0) + addAward(maxComboUserId, "Top combo", "Highest combo in a single round"); if (maxBonusScoreUserId > 0) addAward(maxBonusScoreUserId, "Biggest bonus", "Biggest bonus score across all rounds"); From 9a965a25465ff5bf955998b4734720d8deadd456 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 28 Oct 2025 19:25:18 -0700 Subject: [PATCH 062/308] Add failing drawable date seconds update test --- .../UserInterface/TestSceneDrawableDate.cs | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs index b590abf4e5..e78b4d2496 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Graphics; using osuTK; using osuTK.Graphics; @@ -13,25 +16,35 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneDrawableDate : OsuTestScene { - public TestSceneDrawableDate() + [SetUpSteps] + public void SetUpSteps() { - Child = new FillFlowContainer + AddStep("Create 7 dates", () => { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Children = new Drawable[] + Child = new FillFlowContainer { - new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))), - new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))), - new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))), - new PokeyDrawableDate(DateTimeOffset.Now), - new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))), - new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))), - new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))), - } - }; + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new Drawable[] + { + new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))), + new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))), + new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))), + new PokeyDrawableDate(DateTimeOffset.Now), + new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))), + new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))), + new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))), + } + }; + }); + } + + [Test] + public void TestSecondsUpdate() + { + AddUntilStep("4th date says \"2 seconds ago\"", () => this.ChildrenOfType().ElementAt(3).Current.Value == "2 seconds ago"); } private partial class PokeyDrawableDate : CompositeDrawable From cbe7da99adc9578ab1fe0161d93fdf9a28d8b0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Oct 2025 04:14:37 +0100 Subject: [PATCH 063/308] Fix screen footer overlay content being pushed to right during fade-out (#35481) * Apply some renames & drawable names for visualiser Optional but really helps me make heads of tails as to what anything is here. Like really, multiple variations of `footerContent` inside a `ScreenFooter` class, with zero elaboration that it's really content to do with *overlays*... * Fix screen footer overlay content being pushed to right during fade-out - Closes https://github.com/ppy/osu/issues/35203 - Supersedes / closes https://github.com/ppy/osu/pull/35468 --- osu.Game/Screens/Footer/ScreenFooter.cs | 38 ++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 777ec1790c..5dbc7a55ab 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Footer private Box background = null!; private FillFlowContainer buttonsFlow = null!; - private Container footerContentContainer = null!; + private Container overlayContentContainer = null!; private Container hiddenButtonsContainer = null!; private LogoTrackingContainer logoTrackingContainer = null!; @@ -102,6 +102,7 @@ namespace osu.Game.Screens.Footer { buttonsFlow = new FillFlowContainer { + Name = "Visible footer buttons", Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Y = ScreenFooterButton.CORNER_RADIUS, @@ -109,8 +110,9 @@ namespace osu.Game.Screens.Footer Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both, }, - footerContentContainer = new Container + overlayContentContainer = new Container { + Name = "Overlay-provided extra content", RelativeSizeAxes = Axes.Both, Y = -OsuGame.SCREEN_EDGE_MARGIN, }, @@ -126,6 +128,7 @@ namespace osu.Game.Screens.Footer }, hiddenButtonsContainer = new Container { + Name = "Hidden footer buttons", Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, Y = ScreenFooterButton.CORNER_RADIUS, Anchor = Anchor.BottomLeft, @@ -234,11 +237,11 @@ namespace osu.Game.Screens.Footer public ShearedOverlayContainer? ActiveOverlay { get; private set; } - private VisibilityContainer? activeFooterContent; + private VisibilityContainer? activeOverlayContent; private readonly List temporarilyHiddenButtons = new List(); - public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent) + public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? overlayContent) { if (ActiveOverlay != null) { @@ -267,12 +270,12 @@ namespace osu.Game.Screens.Footer updateColourScheme(overlay.ColourProvider.Hue); - footerContent = overlay.CreateFooterContent(); - activeFooterContent = footerContent; - var content = footerContent; + overlayContent = overlay.CreateFooterContent(); + activeOverlayContent = overlayContent; + var content = overlayContent; if (content != null) - footerContentContainer.Child = content; + overlayContentContainer.Child = content; if (temporarilyHiddenButtons.Count > 0) this.Delay(60).Schedule(() => content?.Show()); @@ -287,15 +290,19 @@ namespace osu.Game.Screens.Footer if (ActiveOverlay == null) return; - Debug.Assert(activeFooterContent != null); - activeFooterContent.Hide(); + Debug.Assert(activeOverlayContent != null); + activeOverlayContent.Hide(); - double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current; + double timeUntilRun = activeOverlayContent.LatestTransformEndTime - Time.Current; for (int i = 0; i < temporarilyHiddenButtons.Count; i++) { var button = temporarilyHiddenButtons[i]; hiddenButtonsContainer.Remove(button, false); + // temporarily bypass autosize on the X axis to prevent the buttons taking space + // immediately upon being moved back to the flow. + // this prevents the overlay content jumping to the right during its fade-out. + button.BypassAutoSizeAxes = Axes.X; buttonsFlow.Add(button); makeButtonAppearFromBottom(button, 0); @@ -305,8 +312,13 @@ namespace osu.Game.Screens.Footer updateColourScheme(OverlayColourScheme.Aquamarine.GetHue()); - activeFooterContent.Delay(timeUntilRun).Expire(); - activeFooterContent = null; + activeOverlayContent.Delay(timeUntilRun).Schedule(() => + { + // overlay content is done displaying, re-enable autosize on all active buttons + foreach (var button in buttonsFlow) + button.BypassAutoSizeAxes = Axes.None; + }).Expire(); + activeOverlayContent = null; ActiveOverlay = null; } From b4fd7ec10ffa81a4d887dda0578dfbf6bfede334 Mon Sep 17 00:00:00 2001 From: De4n <55669793+tadatomix@users.noreply.github.com> Date: Wed, 29 Oct 2025 06:18:00 +0300 Subject: [PATCH 064/308] Add a keycounter that has been actually used in `Triangles` skin (#35491) --- .../Visual/Gameplay/TestSceneSkinnableKeyCounter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs index 098f8e3246..8e9df5b2bf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs @@ -35,7 +35,9 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); - protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay(); + protected override Drawable CreateArgonImplementation() => new ArgonKeyCounterDisplay(); + + protected override Drawable CreateDefaultImplementation() => new DefaultKeyCounterDisplay(); protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay(); } From 050c10cec25a63e5c4cfc076c448b56474997874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Oct 2025 04:18:23 +0100 Subject: [PATCH 065/308] Ensure all invocations of spectator server hub methods have their errors observed (#35488) Fell out when attempting https://github.com/ppy/osu-server-spectator/pull/346. Functionally, if a true non-`HubException` is produced via an invocation of a spectator server hub method, this doesn't really do much - the error will still log as 'unobserved' due to the default handler, it will still show up on sentry, etc. The only difference is that it'll get handled via the continuation installed in `FireAndForget()` rather than the `TaskScheduler.UnobservedTaskException` event. The only real case where this is relevant is when the server throws `HubException`s, which will now instead bubble up to a more human-readable form. Which is relevant to the aforementioned PR because that one makes any hub method potentially throw a `HubException` if the client version is too old. Obviously this does nothing for the existing old clients. --- osu.Game/Online/Metadata/OnlineMetadataClient.cs | 8 ++++---- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- osu.Game/Online/Spectator/SpectatorClient.cs | 9 +++++---- .../OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs | 2 +- osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 2 +- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 2 +- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 6402962e85..75b0187388 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -89,13 +89,13 @@ namespace osu.Game.Online.Metadata userStatus.BindValueChanged(status => { if (localUser.Value is not GuestUser) - UpdateStatus(status.NewValue); + UpdateStatus(status.NewValue).FireAndForget(); }, true); userActivity.BindValueChanged(activity => { if (localUser.Value is not GuestUser) - UpdateActivity(activity.NewValue); + UpdateActivity(activity.NewValue).FireAndForget(); }, true); } @@ -121,8 +121,8 @@ namespace osu.Game.Online.Metadata if (localUser.Value is not GuestUser) { - UpdateActivity(userActivity.Value); - UpdateStatus(userStatus.Value); + UpdateActivity(userActivity.Value).FireAndForget(); + UpdateStatus(userStatus.Value).FireAndForget(); } if (lastQueueId.Value >= 0) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a58d433e7d..44cbbafe72 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -201,7 +201,7 @@ namespace osu.Game.Online.Multiplayer if (!connected.NewValue) { if (Room != null) - LeaveRoom(); + LeaveRoom().FireAndForget(); MatchmakingQueueLeft?.Invoke(); } @@ -560,7 +560,7 @@ namespace osu.Game.Online.Multiplayer return; if (user.Equals(LocalUser)) - LeaveRoom(); + LeaveRoom().FireAndForget(); handleUserLeft(user, UserKicked); }); diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 7f09fbdc9e..f245e8cf3a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -203,7 +204,7 @@ namespace osu.Game.Online.Spectator Task IStatefulUserHubClient.DisconnectRequested() { - Schedule(() => DisconnectInternal()); + Schedule(() => DisconnectInternal().FireAndForget()); return Task.CompletedTask; } @@ -290,7 +291,7 @@ namespace osu.Game.Online.Spectator else currentState.State = SpectatedUserState.Quit; - EndPlayingInternal(currentState); + EndPlayingInternal(currentState).FireAndForget(); }); } @@ -304,7 +305,7 @@ namespace osu.Game.Online.Spectator return; } - WatchUserInternal(userId); + WatchUserInternal(userId).FireAndForget(); } public void StopWatchingUser(int userId) @@ -321,7 +322,7 @@ namespace osu.Game.Online.Spectator watchedUsersRefCounts.Remove(userId); watchedUserStates.Remove(userId); - StopWatchingUserInternal(userId); + StopWatchingUserInternal(userId).FireAndForget(); }); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 95e3cb0236..527b1ba243 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match return; } - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); } /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 0b06a16d98..eb387b2664 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.LocalUser != null); if (client.LocalUser.State == MultiplayerUserState.Results) - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); } protected override string ScreenTitle => "Multiplayer"; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index bbac86fd2d..16c6a46a9c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -618,7 +618,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer updateGameplayState(); if (client.LocalUser.State == MultiplayerUserState.Ready) - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); break; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index a001863780..56120120d5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { loadingDisplay.Show(); - client.ChangeState(MultiplayerUserState.ReadyForGameplay); + client.ChangeState(MultiplayerUserState.ReadyForGameplay).FireAndForget(); } // This will pause the clock, pending the gameplay started callback from the server. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 200e6a715d..fb9343c519 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -296,7 +296,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // On a manual exit, set the player back to idle unless gameplay has finished. // Of note, this doesn't cover exiting using alt-f4 or menu home option. if (multiplayerClient.Room.State != MultiplayerRoomState.Open) - multiplayerClient.ChangeState(MultiplayerUserState.Idle); + multiplayerClient.ChangeState(MultiplayerUserState.Idle).FireAndForget(); return base.OnBackButton(); } From 4e76bd0f240e5cd8350e33f5753b253e0ca05033 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Oct 2025 13:58:20 +0900 Subject: [PATCH 066/308] Play sound when match is available even when queueing in background (#35496) --- .../Matchmaking/Queue/QueueController.cs | 99 +++++++++++++------ 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 40ac0e5777..3b9fc145d6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.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.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -32,11 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue [Resolved] private INotificationOverlay? notifications { get; set; } - [Resolved] - private IPerformFromScreenRunner? performer { get; set; } - - private ProgressNotification? backgroundNotification; - private Notification? readyNotification; + private BackgroundQueueNotification? backgroundNotification; private bool isBackgrounded; protected override void LoadComplete() @@ -118,27 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue 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 IntroScreen())); - - closeNotifications(); - return true; - }, - CancelRequested = () => - { - client.MatchmakingLeaveQueue().FireAndForget(); - - closeNotifications(); - return true; - } - }); + notifications?.Post(backgroundNotification = new BackgroundQueueNotification()); } private void closeNotifications() @@ -146,13 +124,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue if (backgroundNotification != null) { backgroundNotification.State = ProgressNotificationState.Cancelled; - backgroundNotification.Close(false); + backgroundNotification.CloseAll(); + backgroundNotification = null; } - - readyNotification?.Close(false); - - backgroundNotification = null; - readyNotification = null; } protected override void Dispose(bool isDisposing) @@ -168,5 +142,66 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue client.MatchmakingRoomReady -= onMatchmakingRoomReady; } } + + private partial class BackgroundQueueNotification : ProgressNotification + { + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Notification? foundNotification; + private Sample? matchFoundSample; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + Text = "Searching for opponents..."; + + CompletionClickAction = () => + { + client.MatchmakingAcceptInvitation().FireAndForget(); + performer?.PerformFromScreen(s => s.Push(new IntroScreen())); + + Close(false); + return true; + }; + + CancelRequested = () => + { + client.MatchmakingLeaveQueue().FireAndForget(); + return true; + }; + + matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found"); + } + + protected override Notification CreateCompletionNotification() + { + // Playing here means it will play even if notification overlay is hidden. + // + // If we add support for the completion notification to be processed during gameplay, + // this can be moved inside the `MatchFoundNotification` implementation. + matchFoundSample?.Play(); + + return foundNotification = new MatchFoundNotification + { + Activated = CompletionClickAction, + Text = "Your match is ready! Click to join.", + }; + } + + public void CloseAll() + { + foundNotification?.Close(false); + Close(false); + } + + public partial class MatchFoundNotification : ProgressCompletionNotification + { + // for future use. + } + } } } From bd912710f139db7c2fa3c03a297598c11071b473 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 14:46:50 +0900 Subject: [PATCH 067/308] Add quick play helpers to add users/rounds --- .../Matchmaking/MatchmakingRoundList.cs | 23 +++++++++++-------- .../Matchmaking/MatchmakingUserList.cs | 23 +++++++++++-------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs index c34d1771f8..a934b61511 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs @@ -25,16 +25,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking /// 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 }; - } - } + public MatchmakingRound this[int round] => GetOrAdd(round); /// /// The total number of rounds. @@ -42,6 +33,18 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking [IgnoreMember] public int Count => RoundsDictionary.Count; + /// + /// Retrieves or adds a entry to this list. + /// + /// The round. + public MatchmakingRound GetOrAdd(int round) + { + if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score)) + return score; + + return RoundsDictionary[round] = new MatchmakingRound { Round = round }; + } + public IEnumerator GetEnumerator() => RoundsDictionary.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs index 600134de4e..dd8fc72eb9 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs @@ -25,16 +25,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking /// 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 }; - } - } + public MatchmakingUser this[int userId] => GetOrAdd(userId); /// /// The total number of users. @@ -42,6 +33,18 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking [IgnoreMember] public int Count => UserDictionary.Count; + /// + /// Retrieves or adds a entry to this list. + /// + /// The user ID. + public MatchmakingUser GetOrAdd(int userId) + { + if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user)) + return user; + + return UserDictionary[userId] = new MatchmakingUser { UserId = userId }; + } + public IEnumerator GetEnumerator() => UserDictionary.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); From 2d177226fdc14974a9520eecc7c8198247a62ee8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 15:08:32 +0900 Subject: [PATCH 068/308] Add failing test --- .../TestSceneBeatmapSelectPanel.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 02c669aaf5..01f76157f1 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; 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.Overlays; @@ -62,5 +64,41 @@ namespace osu.Game.Tests.Visual.Matchmaking panel.AllowSelection = value; }); } + + [Test] + public void TestFailedBeatmapLookup() + { + AddStep("setup request handle", () => + { + var api = (DummyAPIAccess)API; + var handler = api.HandleRequest; + api.HandleRequest = req => + { + switch (req) + { + case GetBeatmapRequest: + case GetBeatmapsRequest: + req.TriggerFailure(new InvalidOperationException()); + return false; + + default: + return handler?.Invoke(req) ?? false; + } + }; + }); + + AddStep("add panel", () => + { + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + }); + } } } From e9260de56fcda11d4111757851e7ebc714a86022 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 15:15:36 +0900 Subject: [PATCH 069/308] Fix potential nullref if beatmap lookup fails --- .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index 001804a521..aa0329ad94 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -111,7 +111,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { Debug.Assert(card == null); - var beatmap = b.GetResultSafely()!; + APIBeatmap beatmap = b.GetResultSafely() ?? new APIBeatmap + { + BeatmapSet = new APIBeatmapSet + { + Title = "unknown beatmap", + TitleUnicode = "unknown beatmap", + Artist = "unknown artist", + ArtistUnicode = "unknown artist", + } + }; + beatmap.StarRating = Item.StarRating; mainContent.Add(card = new BeatmapCardMatchmaking(beatmap) From 722cfb72d8b82301889554726e93b3ab7b09f103 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 16:07:33 +0900 Subject: [PATCH 070/308] Replace indexers with `GetOrAdd()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Matchmaking/MatchmakingRoomStateTest.cs | 76 +++++++++---------- .../Matchmaking/TestSceneMatchmakingScreen.cs | 10 +-- .../TestScenePlayerPanelOverlay.cs | 2 +- .../Matchmaking/TestSceneResultsScreen.cs | 52 ++++++------- .../Matchmaking/MatchmakingRoomState.cs | 4 +- .../Matchmaking/MatchmakingRoundList.cs | 6 -- .../Matchmaking/MatchmakingUserList.cs | 6 -- .../Match/Results/SubScreenResults.cs | 10 ++- 8 files changed, 78 insertions(+), 88 deletions(-) diff --git a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs index c9219c871a..5f82d22ae8 100644 --- a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs +++ b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs @@ -29,17 +29,17 @@ namespace osu.Game.Tests.Online.Matchmaking 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(8, state.Users.GetOrAdd(1).Points); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Rounds.GetOrAdd(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(6, state.Users.GetOrAdd(2).Points); + Assert.AreEqual(3, state.Users.GetOrAdd(2).Placement); + Assert.AreEqual(3, state.Users.GetOrAdd(2).Rounds.GetOrAdd(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); + Assert.AreEqual(7, state.Users.GetOrAdd(3).Points); + Assert.AreEqual(2, state.Users.GetOrAdd(3).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement); // 2 -> 1 -> 3 @@ -51,17 +51,17 @@ namespace osu.Game.Tests.Online.Matchmaking 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(15, state.Users.GetOrAdd(1).Points); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(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(14, state.Users.GetOrAdd(2).Points); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(2).Rounds.GetOrAdd(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); + Assert.AreEqual(13, state.Users.GetOrAdd(3).Points); + Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement); + Assert.AreEqual(3, state.Users.GetOrAdd(3).Rounds.GetOrAdd(2).Placement); } [Test] @@ -80,21 +80,21 @@ namespace osu.Game.Tests.Online.Matchmaking 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.GetOrAdd(1).Points); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(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(7, state.Users.GetOrAdd(2).Points); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Rounds.GetOrAdd(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.GetOrAdd(3).Points); + Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement); + Assert.AreEqual(4, state.Users.GetOrAdd(3).Rounds.GetOrAdd(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); + Assert.AreEqual(5, state.Users.GetOrAdd(4).Points); + Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement); + Assert.AreEqual(4, state.Users.GetOrAdd(4).Rounds.GetOrAdd(1).Placement); } [Test] @@ -120,8 +120,8 @@ namespace osu.Game.Tests.Online.Matchmaking new SoloScoreInfo { UserID = 2, TotalScore = 1000 }, ], placement_points); - Assert.AreEqual(1, state.Users[1].Placement); - Assert.AreEqual(2, state.Users[2].Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); } [Test] @@ -142,12 +142,12 @@ namespace osu.Game.Tests.Online.Matchmaking 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); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement); + Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement); + Assert.AreEqual(5, state.Users.GetOrAdd(5).Placement); + Assert.AreEqual(6, state.Users.GetOrAdd(6).Placement); } } } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index a598ce9a39..e88b10d30d 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -124,11 +124,11 @@ namespace osu.Game.Tests.Visual.Matchmaking foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next())) { - state.Users[user.UserID].Placement = i++; - state.Users[user.UserID].Points = (8 - i) * 7; - state.Users[user.UserID].Rounds[1].Placement = 1; - state.Users[user.UserID].Rounds[1].TotalScore = 1; - state.Users[user.UserID].Rounds[1].Statistics[HitResult.LargeBonus] = 1; + state.Users.GetOrAdd(user.UserID).Placement = i++; + state.Users.GetOrAdd(user.UserID).Points = (8 - i) * 7; + state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Placement = 1; + state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).TotalScore = 1; + state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Statistics[HitResult.LargeBonus] = 1; } }); } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs index c2b2b95d55..16f15014fb 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs @@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Matchmaking MatchmakingRoomState state = new MatchmakingRoomState(); for (int i = 0; i < room.Users.Count; i++) - state.Users[room.Users[i].UserID].Placement = placements[i]; + state.Users.GetOrAdd(room.Users[i].UserID).Placement = placements[i]; MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs index 4d1a40cc10..9111bbd1c8 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -36,28 +36,28 @@ namespace osu.Game.Tests.Visual.Matchmaking int localUserId = API.LocalUser.Value.OnlineID; // Overall state. - state.Users[localUserId].Placement = 1; - state.Users[localUserId].Points = 8; + state.Users.GetOrAdd(localUserId).Placement = 1; + state.Users.GetOrAdd(localUserId).Points = 8; for (int round = 1; round <= state.CurrentRound; round++) - state.Users[localUserId].Rounds[round].Placement = round; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round; // Highest score. - state.Users[localUserId].Rounds[1].TotalScore = 1000; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000; // Highest accuracy. - state.Users[localUserId].Rounds[2].Accuracy = 0.9995; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995; // Highest combo. - state.Users[localUserId].Rounds[3].MaxCombo = 100; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100; // Most bonus score. - state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50; // Smallest score difference. - state.Users[localUserId].Rounds[5].TotalScore = 1000; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000; // Largest score difference. - state.Users[localUserId].Rounds[6].TotalScore = 1000; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000; MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); @@ -103,36 +103,36 @@ namespace osu.Game.Tests.Visual.Matchmaking 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; + state.Users.GetOrAdd(localUserId).Placement = 1; + state.Users.GetOrAdd(localUserId).Points = 8; + state.Users.GetOrAdd(invalid_user_id).Placement = 2; + state.Users.GetOrAdd(invalid_user_id).Points = 7; for (int round = 1; round <= state.CurrentRound; round++) - state.Users[localUserId].Rounds[round].Placement = round; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round; // Highest score. - state.Users[localUserId].Rounds[1].TotalScore = 1000; - state.Users[invalid_user_id].Rounds[1].TotalScore = 990; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(1).TotalScore = 990; // Highest accuracy. - state.Users[localUserId].Rounds[2].Accuracy = 0.9995; - state.Users[invalid_user_id].Rounds[2].Accuracy = 0.5; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(2).Accuracy = 0.5; // Highest combo. - state.Users[localUserId].Rounds[3].MaxCombo = 100; - state.Users[invalid_user_id].Rounds[3].MaxCombo = 10; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(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; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 25; // Smallest score difference. - state.Users[localUserId].Rounds[5].TotalScore = 1000; - state.Users[invalid_user_id].Rounds[5].TotalScore = 999; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(5).TotalScore = 999; // Largest score difference. - state.Users[localUserId].Rounds[6].TotalScore = 1000; - state.Users[invalid_user_id].Rounds[6].TotalScore = 0; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(6).TotalScore = 0; MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs index 9e1953fc59..b55fa63844 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs @@ -81,10 +81,10 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking foreach (var score in scoreGroup) { - MatchmakingUser mmUser = Users[score.UserID]; + MatchmakingUser mmUser = Users.GetOrAdd(score.UserID); mmUser.Points += placementPoints[placement - 1]; - MatchmakingRound mmRound = mmUser.Rounds[CurrentRound]; + MatchmakingRound mmRound = mmUser.Rounds.GetOrAdd(CurrentRound); mmRound.Placement = placement; mmRound.TotalScore = score.TotalScore; mmRound.Accuracy = score.Accuracy; diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs index a934b61511..fb9a713c10 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs @@ -21,12 +21,6 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking [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] => GetOrAdd(round); - /// /// The total number of rounds. /// diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs index dd8fc72eb9..23a246db5d 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs @@ -21,12 +21,6 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking [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] => GetOrAdd(userId); - /// /// The total number of users. /// diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs index 797519a53c..27afcacf9a 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs @@ -194,19 +194,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results { userStatistics.Clear(); - if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0) + var localUserState = state.Users.GetOrAdd(client.LocalUser!.UserID); + + if (localUserState.Rounds.Count == 0) { placementText.Text = "-"; placementText.Colour = OsuColour.Gray(1f); return; } - int overallPlacement = state.Users[client.LocalUser!.UserID].Placement; + int overallPlacement = localUserState.Placement; placementText.Text = overallPlacement.Ordinalize(CultureInfo.CurrentCulture); placementText.Colour = ColourForPlacement(overallPlacement); - int overallPoints = state.Users[client.LocalUser!.UserID].Points; + int overallPoints = localUserState.Points; addStatistic(overallPlacement, $"Overall position ({overallPoints} points)"); var accuracyOrderedUsers = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average())) @@ -223,7 +225,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results int maxComboPlacement = maxComboOrderedUsers.index + 1; addStatistic(maxComboPlacement, $"Best max combo ({maxComboOrderedUsers.info.maxCombo}x)"); - var bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.MinBy(r => r.Placement); + var bestPlacement = localUserState.Rounds.MinBy(r => r.Placement); addStatistic(bestPlacement!.Placement, $"Best round placement (round {bestPlacement.Round})"); void addStatistic(int position, string text) => userStatistics.Add(new PanelUserStatistic(position, text)); From beb977892ebb47fd044b99568a08421fc4a6a0d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Oct 2025 16:47:02 +0900 Subject: [PATCH 071/308] Use better iconography and colour for queue completion notification --- .../OnlinePlay/Matchmaking/Queue/QueueController.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 3b9fc145d6..80cc6e1bd7 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -7,7 +7,10 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; +using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -200,7 +203,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue public partial class MatchFoundNotification : ProgressCompletionNotification { - // for future use. + protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Icon = FontAwesome.Solid.Bolt; + IconContent.Colour = ColourInfo.GradientVertical(colours.YellowDark, colours.YellowLight); + } } } } From ee7c52465b608af12ca9a8ef9f8b52c0260e0642 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Oct 2025 16:55:14 +0900 Subject: [PATCH 072/308] Allow queue completion notification to show even during gameplay --- osu.Game/Overlays/NotificationOverlay.cs | 7 +++++-- osu.Game/Overlays/NotificationOverlayToastTray.cs | 5 +++++ osu.Game/Overlays/Notifications/Notification.cs | 5 +++++ .../OnlinePlay/Matchmaking/Queue/QueueController.cs | 5 +++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index f56e5e6ac3..7ef2fffeda 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -162,16 +162,17 @@ namespace osu.Game.Overlays private int runningDepth; private readonly Scheduler postScheduler = new Scheduler(); + private readonly Scheduler criticalPostScheduler = new Scheduler(); public override bool IsPresent => // Delegate presence as we need to consider the toast tray in addition to the main overlay. - State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks; + State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks || criticalPostScheduler.HasPendingTasks; private bool processingPosts = true; private double? lastSamplePlayback; - public void Post(Notification notification) => postScheduler.Add(() => + public void Post(Notification notification) => (notification.IsCritical ? criticalPostScheduler : postScheduler).Add(() => { ++runningDepth; @@ -220,6 +221,8 @@ namespace osu.Game.Overlays { base.Update(); + criticalPostScheduler.Update(); + if (processingPosts) postScheduler.Update(); } diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index dd60e303f6..e66b999540 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -91,7 +91,12 @@ namespace osu.Game.Overlays public void FlushAllToasts() { foreach (var notification in toastFlow.ToArray()) + { + if (notification.IsCritical) + continue; + forwardNotification(notification); + } } public void Post(Notification notification) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index ccfd1adb39..99d575da56 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -39,6 +39,11 @@ namespace osu.Game.Overlays.Notifications /// public bool IsImportant { get; init; } = true; + /// + /// Critical notifications show even during gameplay or other scenarios where notifications would usually be suppressed. + /// + public bool IsCritical { get; init; } = false; + /// /// Transient notifications only show as a toast, and do not linger in notification history. /// diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 80cc6e1bd7..468e024a65 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -205,6 +205,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; + public MatchFoundNotification() + { + IsCritical = true; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { From 3afc7b045cf3eeab71be3eef85577387d804f64a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 17:27:33 +0900 Subject: [PATCH 073/308] Remove redundant default value --- osu.Game/Overlays/Notifications/Notification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 99d575da56..8a2a7cee81 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Notifications /// /// Critical notifications show even during gameplay or other scenarios where notifications would usually be suppressed. /// - public bool IsCritical { get; init; } = false; + public bool IsCritical { get; init; } /// /// Transient notifications only show as a toast, and do not linger in notification history. From 4c60df21db701e66876c2afaaba5f6282c447802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Oct 2025 11:50:28 +0100 Subject: [PATCH 074/308] Fix `DrawableDate` not updating Co-authored-by: Dean Herbert --- osu.Game/Graphics/DrawableDate.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 7af4df2d25..e5383bf3a9 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.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Utils; @@ -80,7 +81,7 @@ namespace osu.Game.Graphics public DateTimeOffset TooltipContent => Date; - private class HumanisedDate : IEquatable, ILocalisableStringData + private class HumanisedDate : ILocalisableStringData { public readonly DateTimeOffset Date; @@ -89,11 +90,18 @@ namespace osu.Game.Graphics Date = date; } - public bool Equals(HumanisedDate? other) - => other?.Date != null && Date.Equals(other.Date); - - public bool Equals(ILocalisableStringData? other) - => other is HumanisedDate otherDate && Equals(otherDate); + /// + /// Humanizer formats the relative to the local computer time. + /// Therefore, replacing a instance with another instance of the class with the same + /// should have the effect of replacing and re-formatting the text. + /// Including in equality members would stop this from happening, as + /// has equality-based early guards to prevent redundant text replaces. + /// Thus, instances of these class just compare to any to ensure re-formatting happens correctly. + /// There are "technically" more "correct" ways to do this (like also including the current time into equality checks), + /// but they are simultaneously functionally equivalent to this and overly convoluted. + /// This is a private hack-job of a wrapper around humanizer anyway. + /// + public bool Equals(ILocalisableStringData? other) => false; public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date); From ce96c0b03731d71dff903ded7caf34f2962f43c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Oct 2025 14:24:18 +0100 Subject: [PATCH 075/308] Merge extremely similar setting-enforcing flows in skin editor --- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 64 +++++++------------ 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index e5b0f3f307..83a5d95bb4 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -79,9 +79,6 @@ namespace osu.Game.Overlays.SkinEditor private readonly LayoutValue drawSizeLayout; - private Bindable? configVisibilityMode; - private LeasedBindable? leasedVisibilityMode; - public SkinEditorOverlay(ScalingContainer scalingContainer) { this.scalingContainer = scalingContainer; @@ -94,8 +91,7 @@ namespace osu.Game.Overlays.SkinEditor private void load(OsuConfigManager config) { config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); - - configVisibilityMode = config.GetBindable(OsuSetting.HUDVisibilityMode); + config.BindWith(OsuSetting.HUDVisibilityMode, configVisibilityMode); } protected override void LoadComplete() @@ -103,8 +99,6 @@ namespace osu.Game.Overlays.SkinEditor base.LoadComplete(); externalEditOverlayRegistration = overlayManager?.RegisterBlockingOverlay(externalEditOverlay); - - State.BindValueChanged(_ => saveAndRestoreHUDVisibility()); } public bool OnPressed(KeyBindingPressEvent e) @@ -124,7 +118,7 @@ namespace osu.Game.Overlays.SkinEditor protected override void PopIn() { - globallyDisableBeatmapSkinSetting(); + overrideSkinEditorRelevantSettings(); if (skinEditor != null) { @@ -166,7 +160,7 @@ namespace osu.Game.Overlays.SkinEditor nestedInputManagerDisable?.Dispose(); nestedInputManagerDisable = null; - globallyReenableBeatmapSkinSetting(); + restoreSkinEditorRelevantSettings(); } public void PresentGameplay() => presentGameplay(false); @@ -337,45 +331,33 @@ namespace osu.Game.Overlays.SkinEditor private readonly Bindable beatmapSkins = new Bindable(); private LeasedBindable? leasedBeatmapSkins; - private void globallyDisableBeatmapSkinSetting() - { - if (beatmapSkins.Disabled) - return; + private readonly Bindable configVisibilityMode = new Bindable(); + private LeasedBindable? leasedVisibilityMode; - // The skin editor doesn't work well if beatmap skins are being applied to the player screen. - // To keep things simple, disable the setting game-wide while using the skin editor. - // - // This causes a full reload of the skin, which is pretty ugly. - // TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap. - leasedBeatmapSkins = beatmapSkins.BeginLease(true); - leasedBeatmapSkins.Value = false; + private void overrideSkinEditorRelevantSettings() + { + if (!beatmapSkins.Disabled) + { + // The skin editor doesn't work well if beatmap skins are being applied to the player screen. + // To keep things simple, disable the setting game-wide while using the skin editor. + // + // This causes a full reload of the skin, which is pretty ugly. + // TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap. + leasedBeatmapSkins = beatmapSkins.BeginLease(true); + leasedBeatmapSkins.Value = false; + } + + leasedVisibilityMode = configVisibilityMode.BeginLease(true); + leasedVisibilityMode.Value = HUDVisibilityMode.Always; } - private void globallyReenableBeatmapSkinSetting() + private void restoreSkinEditorRelevantSettings() { leasedBeatmapSkins?.Return(); leasedBeatmapSkins = null; - } - private void saveAndRestoreHUDVisibility() - { - // Make HUD visible while editing skin layout - if (State.Value == Visibility.Visible) - { - leasedVisibilityMode = configVisibilityMode?.BeginLease(true); - - if (leasedVisibilityMode != null) - { - // only when HUD visibility mode is not set to Always. - if (leasedVisibilityMode.Value != HUDVisibilityMode.Always) - leasedVisibilityMode.Value = HUDVisibilityMode.Always; - } - } - else - { - leasedVisibilityMode?.Return(); - leasedVisibilityMode = null; - } + leasedVisibilityMode?.Return(); + leasedVisibilityMode = null; } public new void ToggleVisibility() From f9f7740acbae86fcd4892d7d6633eddc178acbd0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 23:04:50 +0900 Subject: [PATCH 076/308] Add failing test --- .../Matchmaking/TestSceneResultsScreen.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs index c286ca8664..843c20b1e5 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -152,5 +152,32 @@ namespace osu.Game.Tests.Visual.Matchmaking MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); } + + [Test] + public void TestUserWithNoScore() + { + AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(2) + { + User = new APIUser + { + Id = 2, + Username = "Other user" + } + })); + + AddStep("show results with no score", () => + { + var state = new MatchmakingRoomState + { + CurrentRound = 6, + Stage = MatchmakingStage.Ended + }; + + state.Users.GetOrAdd(API.LocalUser.Value.OnlineID).Rounds.GetOrAdd(1).Placement = 1; + state.Users.GetOrAdd(2); + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + } } } From 657bc31539cc459e293808fcb7c0eb7504e4b355 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 22:58:56 +0900 Subject: [PATCH 077/308] Fix potential sources of empty sequence errors --- .../OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs index 6ba6b3f4b0..2f3fb2debb 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs @@ -221,7 +221,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results int accuracyPlacement = accuracyOrderedUsers.index + 1; addStatistic(accuracyPlacement, $"Overall accuracy ({accuracyOrderedUsers.info.avgAcc.FormatAccuracy()})"); - var maxComboOrderedUsers = state.Users.Select(u => (user: u, maxCombo: u.Rounds.Max(r => r.MaxCombo))) + var maxComboOrderedUsers = state.Users.Select(u => (user: u, maxCombo: u.Rounds.Select(r => r.MaxCombo).DefaultIfEmpty(0).Max())) .OrderByDescending(t => t.maxCombo) .Select((t, i) => (info: t, index: i)) .Single(t => t.info.user.UserId == client.LocalUser!.UserID); @@ -229,7 +229,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results addStatistic(maxComboPlacement, $"Best max combo ({maxComboOrderedUsers.info.maxCombo}x)"); var bestPlacement = localUserState.Rounds.MinBy(r => r.Placement); - addStatistic(bestPlacement!.Placement, $"Best round placement (round {bestPlacement.Round})"); + if (bestPlacement != null) + addStatistic(bestPlacement.Placement, $"Best round placement (round {bestPlacement.Round})"); void addStatistic(int position, string text) => userStatistics.Add(new PanelUserStatistic(position, text)); } From 7ff6edeb64adbd5d92444611afb5d10e212bf79d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Oct 2025 15:27:27 +0900 Subject: [PATCH 078/308] Fix quick play "view beatmap" showing incorrect difficulty --- .../Match/BeatmapSelect/BeatmapCardMatchmaking.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs index f727d8f926..1c8194d587 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs @@ -53,6 +53,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + [Resolved] + private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + public BeatmapCardMatchmaking(APIBeatmap beatmap) : base(beatmap.BeatmapSet!, false) { @@ -319,7 +322,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { List items = new List { - new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, DefaultAction) + new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID)) }; foreach (var button in buttonContainer.Buttons) From a435dfe93ed4d6971c8cf329a9777ad0d7a2045c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 22:42:12 +0900 Subject: [PATCH 079/308] Add interop models --- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 10 ++++++++++ .../Online/Multiplayer/IMultiplayerRoomServer.cs | 5 +++++ osu.Game/Online/Multiplayer/MultiplayerClient.cs | 12 ++++++++++++ osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 6 ++++++ .../Online/Multiplayer/OnlineMultiplayerClient.cs | 5 +++++ .../Visual/Multiplayer/TestMultiplayerClient.cs | 5 +++++ 6 files changed, 43 insertions(+) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index adb9b92614..340fb04731 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -149,5 +149,15 @@ namespace osu.Game.Online.Multiplayer /// /// The changed item. Task PlaylistItemChanged(MultiplayerPlaylistItem item); + + /// + /// Signals that a user has requested to skip the beatmap intro. + /// + Task UserVotedToSkip(int userId); + + /// + /// Signals that the vote to skip the beatmap intro has passed. + /// + Task VoteToSkipPassed(); } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 490973faa2..d7834427d0 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -112,6 +112,11 @@ namespace osu.Game.Online.Multiplayer /// The item to remove. Task RemovePlaylistItem(long playlistItemId); + /// + /// Votes to skip the beatmap intro. + /// + Task VoteToSkip(); + /// /// Invites a player to the current room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 44cbbafe72..04162f6b6f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -493,6 +493,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task RemovePlaylistItem(long playlistItemId); + public abstract Task VoteToSkip(); + Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { handleRoomRequest(() => @@ -916,6 +918,16 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + Task IMultiplayerClient.UserVotedToSkip(int userId) + { + throw new NotImplementedException(); + } + + Task IMultiplayerClient.VoteToSkipPassed() + { + throw new NotImplementedException(); + } + /// /// Populates the for a given collection of s. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 499e84ce80..365a25778b 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -49,6 +49,12 @@ namespace osu.Game.Online.Multiplayer [Key(6)] public int? BeatmapId; + /// + /// Whether this user voted to skip the beatmap intro. + /// + [Key(7)] + public bool VotedToSkip; + [IgnoreMember] public APIUser? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 0decff7ab3..e496aea7a2 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -312,6 +312,11 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } + public override Task VoteToSkip() + { + throw new NotImplementedException(); + } + public override Task DisconnectInternal() { if (connector == null) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 5b2876a989..a899912225 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -561,6 +561,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); + public override Task VoteToSkip() + { + throw new NotImplementedException(); + } + protected override Task CreateRoomInternal(MultiplayerRoom room) { Room apiRoom = new Room(room) From ea1798d731f6c0f46fff5c2f54922126c9fb8e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Oct 2025 13:08:20 +0100 Subject: [PATCH 080/308] Fix bad performance when moving mouse to left side of song select forcibly expands group with current selection Calling `HandleItemActivated()` rather than its intended 'parent method' of `Activate()` meant that selection state was not correctly invalidated: https://github.com/ppy/osu/blob/819da1bc38dbc9676f8f1ee3924bfd8d9cb50347/osu.Game/Graphics/Carousel/Carousel.cs#L157 which in turn meant that carousel item Y positions would not be recalculated correctly after the group was expanded, which meant that the items would become - visible, - stuck to the bottom of the expanded group, - one on top of another. Which is not something that's going to perform well. Certified OOP moment. --- 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 5e84ba0722..36b066a308 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -689,7 +689,7 @@ namespace osu.Game.Screens.SelectV2 var groupItem = GetCarouselItems()?.FirstOrDefault(i => CheckModelEquality(i.Model, CurrentGroupedBeatmap.Group)); if (groupItem != null) - HandleItemActivated(groupItem); + Activate(groupItem); } protected override double? GetScrollTarget() From a8251046881481061b5d23ec337a919ea94d6729 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 30 Oct 2025 21:34:42 +0900 Subject: [PATCH 081/308] Add test scene for player jump spamming --- .../TestScenePlayerPanelOverlay.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs index c2b2b95d55..83f3aab264 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs @@ -8,7 +8,9 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; 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; @@ -158,5 +160,64 @@ namespace osu.Game.Tests.Visual.Matchmaking MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); } + + [Test] + public void InteractionSpam() + { + AddStep("join users", () => + { + for (int i = 0; i < 7; i++) + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"User {i}" + } + }); + } + }); + AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid); + AddStep("player jump", () => { MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely(); }); + AddStep("local jumping", () => jumpSpam(false)); + AddWaitStep("wait", 25); + AddStep("group jumping spam", () => jumpSpam(true)); + AddWaitStep("wait", 25); + + AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split); + AddStep("local jumping", () => jumpSpam(false)); + AddWaitStep("wait", 25); + AddStep("group jumping spam", () => jumpSpam(true)); + AddWaitStep("wait", 25); + + AddStep("change to hidden mode", () => list.DisplayStyle = PanelDisplayStyle.Hidden); + AddStep("local jumping", () => jumpSpam(false)); + AddWaitStep("wait", 25); + AddStep("group jumping spam", () => jumpSpam(true)); + AddWaitStep("wait", 25); + } + + private void jumpSpam(bool everyone) + { + for (int i = 0; i < 30; i++) + { + Scheduler.AddDelayed(() => + { + MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely(); + }, i * 150 + RNG.NextDouble(0, 140)); + + if (!everyone) + continue; + + for (int ii = 0; ii < 7; ii++) + { + int iii = ii; + Scheduler.AddDelayed(() => + { + MultiplayerClient.SendUserMatchRequest(iii, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely(); + }, i * 150 + RNG.NextDouble(0, 140)); + } + } + } } } From cf0e5edf34326b2a9d198542d2748ca478de01ff Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 30 Oct 2025 21:35:19 +0900 Subject: [PATCH 082/308] Rework player jump feedback --- .../Matchmaking/Match/PlayerPanel.cs | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 8f8e1430a0..92f3bd3c55 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -113,9 +113,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; private bool hasQuit; - private Sample? jumpSample; - private SampleChannel? jumpSampleChannel; + private enum InteractionSampleType + { + PlayerJump, + PlayerReJump, + OtherPlayerJump, + } + + private Dictionary interactionSamples = new Dictionary(); + private readonly Dictionary interactionSampleChannels = new Dictionary(); private double samplePitch; + private double? lastSamplePlayback; public PlayerPanel(MultiplayerRoomUser user) : base(HoverSampleSet.Button) @@ -248,7 +256,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match // Allow avatar to exist outside of masking for when it jumps around and stuff. AddInternal(avatar.CreateProxy()); - jumpSample = audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump"); + interactionSamples = new Dictionary + { + { InteractionSampleType.PlayerJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump") }, + { InteractionSampleType.PlayerReJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-rejump") }, + { InteractionSampleType.OtherPlayerJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump-other") } + }; } protected override void LoadComplete() @@ -267,7 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match .FadeIn(200); // pick a random pitch to be used by the player for duration of this session - samplePitch = 0.75f + RNG.NextDouble(0f, 0.5f); + samplePitch = 0.75f + RNG.NextDouble(0f, 0.75f); } public PlayerPanelDisplayMode DisplayMode @@ -477,7 +490,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match // only play jump sample if panel is visible if (Alpha > 0) - playJumpSample(); + playJumpSample(isConsecutive); break; } @@ -486,21 +499,42 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } } - private void playJumpSample() + private void playJumpSample(bool rejumping) { - jumpSampleChannel?.Stop(); - jumpSampleChannel = jumpSample?.GetChannel(); + bool isLocalUser = User.OnlineID == client.LocalUser?.UserID; - if (jumpSampleChannel == null) + if (isLocalUser) + playInteractionSample(rejumping ? InteractionSampleType.PlayerReJump : InteractionSampleType.PlayerJump); + else + playInteractionSample(InteractionSampleType.OtherPlayerJump); + } + + private void playInteractionSample(InteractionSampleType sampleType) + { + bool enoughTimePassedSinceLastPlayback = lastSamplePlayback == null || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + if (!enoughTimePassedSinceLastPlayback) + return; + + Sample? targetSample = interactionSamples[sampleType]; + SampleChannel? targetChannel = interactionSampleChannels.GetValueOrDefault(sampleType); + + targetChannel?.Stop(); + targetChannel = targetSample?.GetChannel(); + + if (targetChannel == null) return; float horizontalPos = BoundingBox.Centre.X / Parent!.ToLocalSpace(Parent!.ScreenSpaceDrawQuad).Width; // rescale balance from 0..1 to -1..1 float balance = -1f + horizontalPos * 2f; - jumpSampleChannel.Frequency.Value = samplePitch; - jumpSampleChannel.Balance.Value = balance * OsuGameBase.SFX_STEREO_STRENGTH; - jumpSampleChannel?.Play(); + targetChannel.Frequency.Value = samplePitch; + targetChannel.Balance.Value = balance * OsuGameBase.SFX_STEREO_STRENGTH; + targetChannel.Play(); + + interactionSampleChannels[sampleType] = targetChannel; + + lastSamplePlayback = Time.Current; } protected override void Dispose(bool isDisposing) From 2a01e3d148ea69f86e955e5aac771d1cf23d5aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Oct 2025 14:54:07 +0100 Subject: [PATCH 083/308] Add failing test case --- .../TestSceneBeatmapCarouselArtistGrouping.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 2c3013af12..2390261cdb 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -4,9 +4,12 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -322,5 +325,38 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); AddUntilStep("no beatmap panels visible", () => GetVisiblePanels().Count(), () => Is.Zero); } + + [Test] + public void TestGroupChangedAfterEngagingArtistGrouping() + { + RemoveAllBeatmaps(); + AddStep("add test beatmaps", () => + { + for (int i = 0; i < 5; ++i) + { + var baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3); + + var metadata = new BeatmapMetadata + { + Artist = $"{(char)('A' + i)} artist", + Title = $"{(char)('A' + 4 - i)} title", + }; + + foreach (var b in baseTestBeatmap.Beatmaps) + b.Metadata = metadata; + + Realm.Write(r => r.Add(baseTestBeatmap, update: true)); + BeatmapSets.Add(baseTestBeatmap.Detach()); + } + + SortAndGroupBy(SortMode.Title, GroupMode.Title); + SelectNextSet(); + SelectNextSet(); + WaitForExpandedGroup(1); + + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); + WaitForExpandedGroup(3); + }); + } } } From 73e05e3fae13e2b1e2f94825753125a547d7626b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Oct 2025 14:29:40 +0100 Subject: [PATCH 084/308] Switch active carousel group if current selection no longer exists in the previous group This was primarily written to fix https://github.com/ppy/osu/issues/35538, but also incidentally targets some other scenarios, such as: - When switching from artist filtering to title filtering, selection sometimes would stay at the group under which the selection's artist was filed, rather than moving to the group under which the selection's title is filed (in other words, the group that *the selection is currently under*). - When simply assigning a beatmap to a collection such that it would be moved out of the current group, the selection will now follow to the new collection's group rather than staying at its previous position. Whether this is desired is highly likely to be extremely situational, but I don't want to introduce complications unless it's absolutely necessary. This has a significant performance overhead because `CheckModelEquality()` isn't free, but it doesn't seem horrible in profiling. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 5e84ba0722..5991771d00 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -497,25 +497,35 @@ namespace osu.Game.Screens.SelectV2 // The filter might have changed the set of available groups, which means that the current selection may point to a stale 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) + bool groupStillValid = false; + + if (currentGroupedBeatmap?.Group != null) + { + groupStillValid = grouping.GroupItems.TryGetValue(currentGroupedBeatmap.Group, out var items) + && items.Any(i => CheckModelEquality(i.Model, currentGroupedBeatmap)); + } + + if (groupingRemainsOff || groupStillValid) { // 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. + // If the group no longer exists (or the item no longer exists in the previous group), 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)); + // 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; + groupForReselection = newSelection.Group; + } } // 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. if (groupForReselection != null && grouping.GroupItems.TryGetValue(groupForReselection, out _)) setExpandedGroup(groupForReselection); } From 373162df02421cf835f2ad7ef25d98b092910cbd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Oct 2025 19:56:12 +0900 Subject: [PATCH 085/308] Add support for vote-to-skip in multiplayer --- .../Online/Multiplayer/MultiplayerClient.cs | 34 +++++++++++++++++-- .../Multiplayer/OnlineMultiplayerClient.cs | 12 +++++-- .../Multiplayer/MultiplayerPlayer.cs | 14 +++++++- osu.Game/Screens/Play/Player.cs | 9 +++-- .../Multiplayer/TestMultiplayerClient.cs | 2 +- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 04162f6b6f..9d97f0e830 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -131,6 +131,9 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchmakingItemDeselected; public event Action? MatchRoomStateChanged; + public event Action? UserVotedToSkip; + public event Action? VoteToSkipPassed; + /// /// Whether the is currently connected. /// This is NOT thread safe and usage should be scheduled. @@ -521,6 +524,12 @@ namespace osu.Game.Online.Multiplayer break; } + if (state == MultiplayerRoomState.Playing) + { + foreach (var user in Room.Users) + user.VotedToSkip = false; + } + RoomUpdated?.Invoke(); }); @@ -920,12 +929,33 @@ namespace osu.Game.Online.Multiplayer Task IMultiplayerClient.UserVotedToSkip(int userId) { - throw new NotImplementedException(); + handleRoomRequest(() => + { + 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) + return; + + user.VotedToSkip = true; + + UserVotedToSkip?.Invoke(userId); + }); + + return Task.CompletedTask; } Task IMultiplayerClient.VoteToSkipPassed() { - throw new NotImplementedException(); + handleRoomRequest(() => + { + Debug.Assert(Room != null); + VoteToSkipPassed?.Invoke(); + }); + + return Task.CompletedTask; } /// diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index e496aea7a2..54811c5794 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -70,7 +70,8 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded); 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.UserVotedToSkip), ((IMultiplayerClient)this).UserVotedToSkip); + connection.On(nameof(IMultiplayerClient.VoteToSkipPassed), ((IMultiplayerClient)this).VoteToSkipPassed); connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined); connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft); @@ -80,6 +81,8 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged); connection.On(nameof(IMatchmakingClient.MatchmakingItemSelected), ((IMatchmakingClient)this).MatchmakingItemSelected); connection.On(nameof(IMatchmakingClient.MatchmakingItemDeselected), ((IMatchmakingClient)this).MatchmakingItemDeselected); + + connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested); }; IsConnected.BindTo(connector.IsConnected); @@ -314,7 +317,12 @@ namespace osu.Game.Online.Multiplayer public override Task VoteToSkip() { - throw new NotImplementedException(); + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkip)); } public override Task DisconnectInternal() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 56120120d5..41b8f5f146 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -56,7 +56,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { AllowPause = false, AllowRestart = false, - AllowSkipping = room.AutoSkip, AutomaticallySkipIntro = room.AutoSkip, ShowLeaderboard = true, }) @@ -121,6 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.GameplayStarted += onGameplayStarted; client.ResultsReady += onResultsReady; + client.VoteToSkipPassed += onVoteToSkipPassed; ScoreProcessor.HasCompleted.BindValueChanged(_ => { @@ -219,6 +219,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))).ConfigureAwait(false); } + protected override void RequestIntroSkip() + { + // No base call because we aren't skipping yet. + client.VoteToSkip(); + } + + private void onVoteToSkipPassed() + { + Schedule(PerformIntroSkip); + } + protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(Room.RoomID != null); @@ -242,6 +253,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { client.GameplayStarted -= onGameplayStarted; client.ResultsReady -= onResultsReady; + client.VoteToSkipPassed -= onVoteToSkipPassed; } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 22fb8a3463..9f40fc97da 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -502,7 +502,7 @@ namespace osu.Game.Screens.Play DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) { - RequestSkip = performUserRequestedSkip + RequestSkip = RequestIntroSkip }, skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0) { @@ -701,7 +701,12 @@ namespace osu.Game.Screens.Play return true; } - private void performUserRequestedSkip() + protected virtual void RequestIntroSkip() + { + PerformIntroSkip(); + } + + protected void PerformIntroSkip() { // user requested skip // disable sample playback to stop currently playing samples and perform skip diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index a899912225..242bbe3083 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -563,7 +563,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task VoteToSkip() { - throw new NotImplementedException(); + return Task.CompletedTask; } protected override Task CreateRoomInternal(MultiplayerRoom room) From d0ce74063d21329f5feeaec26c1a87f6c19f12f3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Oct 2025 20:49:27 +0900 Subject: [PATCH 086/308] Skip full intro length --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 2 +- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 41b8f5f146..406f38ea72 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -227,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onVoteToSkipPassed() { - Schedule(PerformIntroSkip); + Schedule(() => PerformIntroSkip(true)); } protected override ResultsScreen CreateResults(ScoreInfo score) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 07ecb5a5fb..abf157df43 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -115,14 +115,14 @@ namespace osu.Game.Screens.Play /// /// Skip forward to the next valid skip point. /// - public void Skip() + public void Skip(bool fullLength = false) { if (GameplayClock.CurrentTime > GameplayStartTime - MINIMUM_SKIP_TIME) return; double skipTarget = GameplayStartTime - MINIMUM_SKIP_TIME; - if (StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000) + if (!fullLength && StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros skipTarget = 0; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9f40fc97da..2927d8a720 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -706,13 +706,13 @@ namespace osu.Game.Screens.Play PerformIntroSkip(); } - protected void PerformIntroSkip() + protected void PerformIntroSkip(bool fullLength = false) { // user requested skip // disable sample playback to stop currently playing samples and perform skip samplePlaybackDisabled.Value = true; - (GameplayClockContainer as MasterGameplayClockContainer)?.Skip(); + (GameplayClockContainer as MasterGameplayClockContainer)?.Skip(fullLength); // return samplePlaybackDisabled.Value to what is defined by the beatmap's current state updateSampleDisabledState(); From b20a41c1e8377271bd96fd345df1479d20fac3c2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 21:39:33 +0900 Subject: [PATCH 087/308] Add simple multiplayer skip overlay --- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 2 +- .../TestSceneMultiplayerSkipOverlay.cs | 73 +++++++++++ .../Multiplayer/MultiplayerPlayer.cs | 2 + .../Multiplayer/MultiplayerSkipOverlay.cs | 114 ++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 14 ++- osu.Game/Screens/Play/SkipOverlay.cs | 17 +-- .../Multiplayer/TestMultiplayerClient.cs | 7 +- 7 files changed, 213 insertions(+), 16 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 276a0c3410..946b625608 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay public Drawable OverlayContent => InternalChild; - public Drawable FadingContent => (OverlayContent as Container)?.Child; + public new Drawable FadingContent => (OverlayContent as Container)?.Child; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs new file mode 100644 index 0000000000..a1b28e2544 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs @@ -0,0 +1,73 @@ +// 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.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public partial class TestSceneMultiplayerSkipOverlay : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add skip overlay", () => + { + GameplayClockContainer gameplayClockContainer; + + var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + + Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0) + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new MultiplayerSkipOverlay(120000) + }, + }; + + gameplayClockContainer.Start(); + }); + + AddStep("set playing state", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Playing)); + } + + [Test] + public void TestSkip() + { + for (int i = 0; i < 4; i++) + { + int i2 = i; + + AddStep($"join user {i2}", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = i2, + Username = $"User {i2}" + }); + + MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing); + }); + } + + AddStep("local user votes", () => MultiplayerClient.VoteToSkip().WaitSafely()); + + for (int i = 0; i < 4; i++) + { + int i2 = i; + AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkip(i2).WaitSafely()); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 406f38ea72..24dfa59ed3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -148,6 +148,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.Room != null); } + protected override SkipOverlay CreateSkipOverlay(double startTime) => new MultiplayerSkipOverlay(startTime); + protected override void StartGameplay() { // We can enter this screen one of two ways: diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs new file mode 100644 index 0000000000..ccda0e8690 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -0,0 +1,114 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Play; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerSkipOverlay : SkipOverlay + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Drawable votedIcon = null!; + private OsuSpriteText countText = null!; + + public MultiplayerSkipOverlay(double startTime) + : base(startTime) + { + } + + [BackgroundDependencyLoader] + private void load() + { + FadingContent.AddRange( + [ + votedIcon = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(50, 0), + Size = new Vector2(20), + Alpha = 0, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Green + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.5f), + Icon = FontAwesome.Solid.Check + } + } + }, + countText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + Position = new Vector2(0.75f, 0), + Font = OsuFont.Default.With(size: 36, weight: FontWeight.Bold) + } + ]); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.UserStateChanged += onUserStateChanged; + client.UserVotedToSkip += onUserVotedToSkip; + + updateText(); + } + + private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) + { + Schedule(updateText); + } + + private void onUserVotedToSkip(int userId) => Schedule(() => + { + updateText(); + + countText.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); + + if (userId == client.LocalUser?.UserID) + { + votedIcon.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); + votedIcon.FadeInFromZero(100); + } + }); + + private void updateText() + { + if (client.Room == null) + return; + + int countTotal = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing); + int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkip); + int countRequired = countTotal / 2 + 1; + + countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2927d8a720..b712a451c5 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Play private BreakTracker breakTracker; - private SkipOverlay skipIntroOverlay; + protected SkipOverlay SkipIntroOverlay { get; private set; } private SkipOverlay skipOutroOverlay; protected ScoreProcessor ScoreProcessor { get; private set; } @@ -500,10 +500,10 @@ namespace osu.Game.Screens.Play }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), - skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) + SkipIntroOverlay = CreateSkipOverlay(DrawableRuleset.GameplayStartTime).With(o => { - RequestSkip = RequestIntroSkip - }, + o.RequestSkip = RequestIntroSkip; + }), skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0) { RequestSkip = () => progressToResults(false), @@ -522,13 +522,15 @@ namespace osu.Game.Screens.Play if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) { - skipIntroOverlay.Expire(); + SkipIntroOverlay.Expire(); skipOutroOverlay.Expire(); } return container; } + protected virtual SkipOverlay CreateSkipOverlay(double startTime) => new SkipOverlay(startTime); + private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { updateGameplayState(); @@ -1158,7 +1160,7 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Reset(startClock: true); if (Configuration.AutomaticallySkipIntro) - skipIntroOverlay.SkipWhenReady(); + SkipIntroOverlay.SkipWhenReady(); } public override void OnSuspending(ScreenTransitionEvent e) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index be8517d9a0..700ea2e532 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -38,20 +38,21 @@ namespace osu.Game.Screens.Play private readonly double startTime; public Action RequestSkip; + + protected FadeContainer FadingContent { get; private set; } + private Button button; private ButtonContainer buttonContainer; private Circle remainingTimeBox; - private FadeContainer fadeContainer; private double displayTime; - private bool isClickable; private bool skipQueued; [Resolved] private IGameplayClock gameplayClock { get; set; } - internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; + internal bool IsButtonVisible => FadingContent.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// @@ -77,7 +78,7 @@ namespace osu.Game.Screens.Play InternalChild = buttonContainer = new ButtonContainer { RelativeSizeAxes = Axes.Both, - Child = fadeContainer = new FadeContainer + Child = FadingContent = new FadeContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -107,13 +108,13 @@ namespace osu.Game.Screens.Play public override void Hide() { base.Hide(); - fadeContainer.Hide(); + FadingContent.Hide(); } public override void Show() { base.Show(); - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); } protected override void LoadComplete() @@ -136,7 +137,7 @@ namespace osu.Game.Screens.Play RequestSkip?.Invoke(); }; - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); } /// @@ -183,7 +184,7 @@ namespace osu.Game.Screens.Play protected override bool OnMouseMove(MouseMoveEvent e) { if (isClickable && !e.HasAnyButtonPressed) - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); return base.OnMouseMove(e); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 242bbe3083..fcee7e5b44 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -563,7 +563,12 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task VoteToSkip() { - return Task.CompletedTask; + return UserVoteToSkip(api.LocalUser.Value.OnlineID); + } + + public async Task UserVoteToSkip(int userId) + { + await ((IMultiplayerClient)this).UserVotedToSkip(userId); } protected override Task CreateRoomInternal(MultiplayerRoom room) From 6f94b1ab6d21e0dba43c78a801624611c838981b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 21:40:40 +0900 Subject: [PATCH 088/308] Move property reset into GameplayStarted() --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 9d97f0e830..3df12e16ea 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -524,12 +524,6 @@ namespace osu.Game.Online.Multiplayer break; } - if (state == MultiplayerRoomState.Playing) - { - foreach (var user in Room.Users) - user.VotedToSkip = false; - } - RoomUpdated?.Invoke(); }); @@ -857,6 +851,10 @@ namespace osu.Game.Online.Multiplayer handleRoomRequest(() => { Debug.Assert(Room != null); + + foreach (var user in Room.Users) + user.VotedToSkip = false; + GameplayStarted?.Invoke(); }); From bdcc0ee937113dd5a80ae6e7920688dd7a6de028 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 21:42:29 +0900 Subject: [PATCH 089/308] Apply suggestions from review --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 2 +- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 1 + osu.Game/Screens/Play/Player.cs | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 24dfa59ed3..214a7d6403 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void RequestIntroSkip() { // No base call because we aren't skipping yet. - client.VoteToSkip(); + client.VoteToSkip().FireAndForget(); } private void onVoteToSkipPassed() diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index abf157df43..c9db6009d0 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -115,6 +115,7 @@ namespace osu.Game.Screens.Play /// /// Skip forward to the next valid skip point. /// + /// true to skip as close to gameplay as possible, or false to skip only to the next valid skip point. public void Skip(bool fullLength = false) { if (GameplayClock.CurrentTime > GameplayStartTime - MINIMUM_SKIP_TIME) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b712a451c5..6158118c78 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -708,6 +708,10 @@ namespace osu.Game.Screens.Play PerformIntroSkip(); } + /// + /// Skip forward to the next valid skip point. + /// + /// true to skip as close to gameplay as possible, or false to skip only to the next valid skip point. protected void PerformIntroSkip(bool fullLength = false) { // user requested skip From a9ca4634fc583ececc0fd3c3bbfa224c8a8daf59 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 21:48:24 +0900 Subject: [PATCH 090/308] Resolve CI inspections --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index fcee7e5b44..83b2da000f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -568,7 +568,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public async Task UserVoteToSkip(int userId) { - await ((IMultiplayerClient)this).UserVotedToSkip(userId); + await ((IMultiplayerClient)this).UserVotedToSkip(userId).ConfigureAwait(false); } protected override Task CreateRoomInternal(MultiplayerRoom room) From 14cdc40f0fef4cab84b0893f2dee324d9bd1db55 Mon Sep 17 00:00:00 2001 From: Marvefect Date: Sun, 2 Nov 2025 02:04:48 +0300 Subject: [PATCH 091/308] Added Tooltip --- .../Profile/Header/Components/MainDetails.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 10bb69f0f5..c337299673 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -158,6 +158,7 @@ namespace osu.Game.Overlays.Profile.Header.Components medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0"; ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0"; + ppInfo.ContentTooltipText = getPPInfoTooltipText(user); foreach (var scoreRankInfo in scoreRankInfos) scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; @@ -234,6 +235,29 @@ namespace osu.Game.Overlays.Profile.Header.Components return result ?? default; } + private static LocalisableString getPPInfoTooltipText(APIUser? user) + { + var variants = user?.Statistics?.Variants; + + LocalisableString? result = null; + + if (variants?.Count > 0) + { + foreach (var variant in variants) + { + var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.PP.ToLocalisableString("#,##0")}"); + + if (result == null) + result = variantText; + else + result = LocalisableString.Interpolate($"{result}\n{variantText}"); + + } + } + + return result ?? default; + } + private partial class ScoreRankInfo : CompositeDrawable { private readonly OsuSpriteText rankCount; From 65fb5311ea36755f2d7c56bd1d037f67cf2142ff Mon Sep 17 00:00:00 2001 From: Marvefect Date: Sun, 2 Nov 2025 02:27:39 +0300 Subject: [PATCH 092/308] Removed unneccesary blank space, reran dotnet format --- osu.Game/Overlays/Profile/Header/Components/MainDetails.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index c337299673..e0f3b0a3e5 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -251,7 +251,6 @@ namespace osu.Game.Overlays.Profile.Header.Components result = variantText; else result = LocalisableString.Interpolate($"{result}\n{variantText}"); - } } From 2413e981083cfca8e24fe97c67ef8fdf3a5c88f8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 2 Nov 2025 11:58:09 +0900 Subject: [PATCH 093/308] Fix file and class name mismatch --- .../Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs | 2 +- osu.Game/Screens/Menu/MainMenu.cs | 2 +- osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs | 4 ++-- .../Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs index 5193d58ee6..07d0fe6ed9 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("load screen", () => LoadScreen(new IntroScreen())); + AddStep("load screen", () => LoadScreen(new ScreenIntro())); } [Test] diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c4ba3145b5..2296213dd6 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -482,7 +482,7 @@ namespace osu.Game.Screens.Menu private void loadSongSelect() => this.Push(new SoloSongSelect()); - private void joinOrLeaveMatchmakingQueue() => this.Push(new OnlinePlay.Matchmaking.Intro.IntroScreen()); + private void joinOrLeaveMatchmakingQueue() => this.Push(new OnlinePlay.Matchmaking.Intro.ScreenIntro()); private partial class MobileDisclaimerDialog : PopupDialog { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs index b3fff7dc00..093d9f6117 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro /// /// A brief intro animation that introduces matchmaking to the user. /// - public partial class IntroScreen : OsuScreen + public partial class ScreenIntro : OsuScreen { public override bool DisallowExternalBeatmapRulesetChanges => false; @@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro protected override BackgroundScreen CreateBackground() => new MatchmakingIntroBackgroundScreen(colourProvider); - public IntroScreen() + public ScreenIntro() { ValidForResume = false; } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 468e024a65..353f5ac24f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue CompletionClickAction = () => { client.MatchmakingAcceptInvitation().FireAndForget(); - performer?.PerformFromScreen(s => s.Push(new IntroScreen())); + performer?.PerformFromScreen(s => s.Push(new ScreenIntro())); Close(false); return true; From 1ab017d4e201afcc9cd4cebda6370ecb478b3cfa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 2 Nov 2025 12:44:01 +0900 Subject: [PATCH 094/308] Fix quick play notification not setting "accepted" state --- .../OnlinePlay/Matchmaking/Queue/QueueController.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 353f5ac24f..f72f26f26e 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -119,7 +119,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue if (backgroundNotification != null) return; - notifications?.Post(backgroundNotification = new BackgroundQueueNotification()); + notifications?.Post(backgroundNotification = new BackgroundQueueNotification(this)); } private void closeNotifications() @@ -154,9 +154,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue [Resolved] private MultiplayerClient client { get; set; } = null!; + private readonly QueueController controller; + private Notification? foundNotification; private Sample? matchFoundSample; + public BackgroundQueueNotification(QueueController controller) + { + this.controller = controller; + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { @@ -165,6 +172,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue CompletionClickAction = () => { client.MatchmakingAcceptInvitation().FireAndForget(); + controller.CurrentState.Value = ScreenQueue.MatchmakingScreenState.AcceptedWaitingForRoom; + performer?.PerformFromScreen(s => s.Push(new ScreenIntro())); Close(false); From 89b443bccc172f709839962d9d9523151c10985f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8D=E4=BA=88?= Date: Mon, 3 Nov 2025 20:29:46 +0800 Subject: [PATCH 095/308] Add GitHub link button to the wiki overlay header (#35595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Github link button to wiki overlay header * Localize jump link string * Mark ILinkHandler dependency as nullable * Make the button actually look like it does on the website * Use existing web string instead of inventing a new one * Bind value change callback more reliably --------- Co-authored-by: Bartłomiej Dach --- osu.Game/Overlays/Wiki/WikiHeader.cs | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/osu.Game/Overlays/Wiki/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index d64d6b934a..a5129eaefd 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -5,13 +5,20 @@ 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.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; +using osuTK; namespace osu.Game.Overlays.Wiki { @@ -19,11 +26,15 @@ namespace osu.Game.Overlays.Wiki { public static LocalisableString IndexPageString => LayoutStrings.HeaderHelpIndex; + private const string github_wiki_base = @"https://github.com/ppy/osu-wiki/blob/master/wiki"; + public readonly Bindable WikiPageData = new Bindable(); public Action ShowIndexPage; public Action ShowParentPage; + private readonly Bindable githubPath = new Bindable(); + public WikiHeader() { TabControl.AddItem(IndexPageString); @@ -35,6 +46,9 @@ namespace osu.Game.Overlays.Wiki private void onWikiPageChange(ValueChangedEvent e) { + // Clear the path beforehand in case we got an error page. + githubPath.Value = null; + if (e.NewValue == null) return; @@ -42,6 +56,7 @@ namespace osu.Game.Overlays.Wiki Current.Value = null; TabControl.AddItem(IndexPageString); + githubPath.Value = $"{github_wiki_base}/{e.NewValue.Path}/{e.NewValue.Locale}.md"; if (e.NewValue.Path == WikiOverlay.INDEX_PATH) { @@ -56,6 +71,27 @@ namespace osu.Game.Overlays.Wiki Current.Value = e.NewValue.Title; } + protected override Drawable CreateTabControlContent() + { + return new FillFlowContainer + { + Height = 40, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new ShowOnGitHubButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(32), + TargetPath = { BindTarget = githubPath }, + }, + }, + }; + } + private void onCurrentChange(ValueChangedEvent e) { if (e.NewValue == TabControl.Items.LastOrDefault()) @@ -83,5 +119,39 @@ namespace osu.Game.Overlays.Wiki Icon = OsuIcon.Wiki; } } + + private partial class ShowOnGitHubButton : RoundedButton + { + public override LocalisableString TooltipText => WikiStrings.ShowEditLink; + + public readonly Bindable TargetPath = new Bindable(); + + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] ILinkHandler linkHandler) + { + Width = 42; + + Add(new SpriteIcon + { + Size = new Vector2(12), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Brands.Github, + }); + + Action = () => linkHandler?.HandleLink(TargetPath.Value); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + TargetPath.BindValueChanged(e => + { + this.FadeTo(e.NewValue != null ? 1 : 0); + Enabled.Value = e.NewValue != null; + }, true); + } + } } } From 73f1849365717e0f3144db8154f533f2f8b9fd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Nov 2025 01:46:09 +0100 Subject: [PATCH 096/308] Fix signalr connector connection failure logging eating exception stack trace (#35598) As seen in https://discord.com/channels/188630481301012481/1097318920991559880/1434899538123952128, wherein precisely zero useful detail can be gleaned (and nothing is reported to sentry either). --- osu.Game/Online/PersistentEndpointClientConnector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs index 9e7543ce2b..2674c29103 100644 --- a/osu.Game/Online/PersistentEndpointClientConnector.cs +++ b/osu.Game/Online/PersistentEndpointClientConnector.cs @@ -150,7 +150,7 @@ namespace osu.Game.Online // compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Online/BanchoClient.cs#L539 retryDelay = Math.Min(120000, (int)(retryDelay * 1.5)); - Logger.Log($"{ClientName} connect attempt failed: {exception.Message}. Next attempt in {thisDelay / 1000:N0} seconds.", LoggingTarget.Network); + Logger.Log($"{ClientName} connect attempt failed. Next attempt in {thisDelay / 1000:N0} seconds.\n{exception}", LoggingTarget.Network); await Task.Delay(thisDelay, cancellationToken).ConfigureAwait(false); } From 645d27bb3245e6324e203412963940b584eee34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Nov 2025 02:47:33 +0100 Subject: [PATCH 097/308] Add tiered colours for global rank (#35597) * Add new API property backing for tiered rank * Slightly refactor `ProfileValueDisplay` for direct access to things that will need direct access * Extract separate component for global rank display * Add tiered colours for global rank --- .../Online/TestSceneGlobalRankDisplay.cs | 67 +++++++++ .../Header/Components/GlobalRankDisplay.cs | 137 ++++++++++++++++++ .../Profile/Header/Components/MainDetails.cs | 59 ++------ .../Header/Components/ProfileValueDisplay.cs | 19 +-- .../Header/Components/TotalPlayTime.cs | 6 +- osu.Game/Users/UserRankPanel.cs | 19 ++- osu.Game/Users/UserStatistics.cs | 3 + 7 files changed, 233 insertions(+), 77 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs create mode 100644 osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs new file mode 100644 index 0000000000..07fe8c6172 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs @@ -0,0 +1,67 @@ +// 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.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Tests.Visual.UserInterface; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneGlobalRankDisplay : ThemeComparisonTestScene + { + public TestSceneGlobalRankDisplay() + : base(false) + { + } + + protected override Drawable CreateContent() => new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Full, + Padding = new MarginPadding(20), + Spacing = new Vector2(40), + ChildrenEnumerable = new int?[] { 64, 423, 1453, 3468, 18_367, 48_342, 178_432, 375_231, 897_783, null }.Select(createDisplay) + }; + + private GlobalRankDisplay createDisplay(int? rank) => new GlobalRankDisplay + { + UserStatistics = + { + Value = new UserStatistics + { + GlobalRank = rank, + GlobalRankPercent = rank / 1_000_000f, + Variants = + [ + new UserStatistics.Variant + { + VariantType = UserStatistics.RulesetVariant.FourKey, + GlobalRank = rank / 3, + }, + new UserStatistics.Variant + { + VariantType = UserStatistics.RulesetVariant.SevenKey, + GlobalRank = 2 * rank / 3, + } + ] + }, + }, + HighestRank = + { + Value = rank == null + ? null + : new APIUser.UserRankHighest + { + Rank = rank.Value / 2, + UpdatedAt = DateTimeOffset.Now.AddMonths(-3), + } + } + }; + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs new file mode 100644 index 0000000000..3560986925 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class GlobalRankDisplay : CompositeDrawable + { + public Bindable UserStatistics = new Bindable(); + public Bindable HighestRank = new Bindable(); + + private ProfileValueDisplay info = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public GlobalRankDisplay() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = info = new ProfileValueDisplay(big: true) + { + Title = UsersStrings.ShowRankGlobalSimple + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + UserStatistics.BindValueChanged(_ => updateState()); + HighestRank.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + info.Content.Text = UserStatistics.Value?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + info.Content.TooltipText = getGlobalRankTooltipText(); + + var tier = getRankingTier(); + info.Content.Colour = tier == null ? colourProvider.Content2 : OsuColour.ForRankingTier(tier.Value); + info.Content.Font = info.Content.Font.With(weight: tier == null || tier == RankingTier.Iron ? FontWeight.Regular : FontWeight.Bold); + } + + /// + private RankingTier? getRankingTier() + { + var stats = UserStatistics.Value; + + int? rank = stats?.GlobalRank; + float? percent = stats?.GlobalRankPercent; + + if (rank == null || percent == null) + return null; + + if (rank <= 100) + return RankingTier.Lustrous; + + if (percent < 0.0005) + return RankingTier.Radiant; + + if (percent < 0.0025) + return RankingTier.Rhodium; + + if (percent < 0.005) + return RankingTier.Platinum; + + if (percent < 0.025) + return RankingTier.Gold; + + if (percent < 0.05) + return RankingTier.Silver; + + if (percent < 0.25) + return RankingTier.Bronze; + + if (percent < 0.5) + return RankingTier.Iron; + + return null; + } + + private LocalisableString getGlobalRankTooltipText() + { + var rankHighest = HighestRank.Value; + var variants = UserStatistics.Value?.Variants; + + LocalisableString? result = null; + + if (variants?.Count > 0) + { + foreach (var variant in variants) + { + if (variant.GlobalRank != null) + { + var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}"); + + if (result == null) + result = variantText; + else + result = LocalisableString.Interpolate($"{result}\n{variantText}"); + } + } + } + + if (rankHighest != null) + { + var rankHighestText = UsersStrings.ShowRankHighest( + rankHighest.Rank.ToLocalisableString("\\##,##0"), + rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy")); + + if (result == null) + result = rankHighestText; + else + result = LocalisableString.Interpolate($"{result}\n{rankHighestText}"); + } + + return result ?? default; + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index e0f3b0a3e5..029de96c41 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private readonly Dictionary scoreRankInfos = new Dictionary(); private ProfileValueDisplay medalInfo = null!; private ProfileValueDisplay ppInfo = null!; - private ProfileValueDisplay detailGlobalRank = null!; + private GlobalRankDisplay detailGlobalRank = null!; private ProfileValueDisplay detailCountryRank = null!; private RankGraph rankGraph = null!; @@ -64,10 +64,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { new[] { - detailGlobalRank = new ProfileValueDisplay(true) - { - Title = UsersStrings.ShowRankGlobalSimple, - }, + detailGlobalRank = new GlobalRankDisplay(), Empty(), detailCountryRank = new ProfileValueDisplay(true) { @@ -156,60 +153,22 @@ namespace osu.Game.Overlays.Profile.Header.Components { var user = data?.User; - medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0"; - ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0"; - ppInfo.ContentTooltipText = getPPInfoTooltipText(user); + medalInfo.Content.Text = user?.Achievements?.Length.ToString() ?? "0"; + ppInfo.Content.Text = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0"; + ppInfo.Content.TooltipText = getPPInfoTooltipText(user); foreach (var scoreRankInfo in scoreRankInfos) scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; - detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - detailGlobalRank.ContentTooltipText = getGlobalRankTooltipText(user); + detailGlobalRank.HighestRank.Value = user?.RankHighest; + detailGlobalRank.UserStatistics.Value = user?.Statistics; - detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - detailCountryRank.ContentTooltipText = getCountryRankTooltipText(user); + detailCountryRank.Content.Text = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + detailCountryRank.Content.TooltipText = getCountryRankTooltipText(user); rankGraph.Statistics.Value = user?.Statistics; } - private static LocalisableString getGlobalRankTooltipText(APIUser? user) - { - var rankHighest = user?.RankHighest; - var variants = user?.Statistics?.Variants; - - LocalisableString? result = null; - - if (variants?.Count > 0) - { - foreach (var variant in variants) - { - if (variant.GlobalRank != null) - { - var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}"); - - if (result == null) - result = variantText; - else - result = LocalisableString.Interpolate($"{result}\n{variantText}"); - } - } - } - - if (rankHighest != null) - { - var rankHighestText = UsersStrings.ShowRankHighest( - rankHighest.Rank.ToLocalisableString("\\##,##0"), - rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy")); - - if (result == null) - result = rankHighestText; - else - result = LocalisableString.Interpolate($"{result}\n{rankHighestText}"); - } - - return result ?? default; - } - private static LocalisableString getCountryRankTooltipText(APIUser? user) { var variants = user?.Statistics?.Variants; diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileValueDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileValueDisplay.cs index b2c23458b1..db384ed9d7 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileValueDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileValueDisplay.cs @@ -14,22 +14,13 @@ namespace osu.Game.Overlays.Profile.Header.Components public partial class ProfileValueDisplay : CompositeDrawable { private readonly OsuSpriteText title; - private readonly ContentText content; public LocalisableString Title { set => title.Text = value; } - public LocalisableString Content - { - set => content.Text = value; - } - - public LocalisableString ContentTooltipText - { - set => content.TooltipText = value; - } + public ContentText Content { get; } public ProfileValueDisplay(bool big = false, int minimumWidth = 60) { @@ -44,9 +35,9 @@ namespace osu.Game.Overlays.Profile.Header.Components { Font = OsuFont.GetFont(size: 12) }, - content = new ContentText + Content = new ContentText { - Font = OsuFont.GetFont(size: big ? 30 : 20, weight: FontWeight.Light), + Font = OsuFont.GetFont(size: big ? 30 : 20, weight: big ? FontWeight.Regular : FontWeight.Light), }, new Container // Add a minimum size to the FillFlowContainer { @@ -60,10 +51,10 @@ namespace osu.Game.Overlays.Profile.Header.Components private void load(OverlayColourProvider colourProvider) { title.Colour = colourProvider.Content1; - content.Colour = colourProvider.Content2; + Content.Colour = colourProvider.Content2; } - private partial class ContentText : OsuSpriteText, IHasTooltip + public partial class ContentText : OsuSpriteText, IHasTooltip { public LocalisableString TooltipText { get; set; } } diff --git a/osu.Game/Overlays/Profile/Header/Components/TotalPlayTime.cs b/osu.Game/Overlays/Profile/Header/Components/TotalPlayTime.cs index a3c22d61d2..3cc7bc15e8 100644 --- a/osu.Game/Overlays/Profile/Header/Components/TotalPlayTime.cs +++ b/osu.Game/Overlays/Profile/Header/Components/TotalPlayTime.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Profile.Header.Components InternalChild = info = new ProfileValueDisplay(minimumWidth: 140) { Title = UsersStrings.ShowStatsPlayTime, - ContentTooltipText = "0 hours", + Content = { TooltipText = "0 hours", } }; User.BindValueChanged(updateTime, true); @@ -35,8 +35,8 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateTime(ValueChangedEvent user) { int? playTime = user.NewValue?.User.Statistics?.PlayTime; - info.ContentTooltipText = (playTime ?? 0) / 3600 + " hours"; - info.Content = formatTime(playTime); + info.Content.TooltipText = (playTime ?? 0) / 3600 + " hours"; + info.Content.Text = formatTime(playTime); } private string formatTime(int? secondsNull) diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index ff8adf055c..251c21a89a 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -27,7 +27,7 @@ namespace osu.Game.Users private const int padding = 10; private const int main_content_height = 80; - private ProfileValueDisplay globalRankDisplay = null!; + private GlobalRankDisplay globalRankDisplay = null!; private ProfileValueDisplay countryRankDisplay = null!; private LoadingLayer loadingLayer = null!; @@ -71,8 +71,13 @@ namespace osu.Game.Users var statistics = statisticsProvider?.GetStatisticsFor(ruleset.Value); loadingLayer.State.Value = statistics == null ? Visibility.Visible : Visibility.Hidden; - globalRankDisplay.Content = statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; - countryRankDisplay.Content = statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; + + // TODO: implement highest rank tooltip + // `RankHighest` resides in `APIUser`, but `api.LocalUser` doesn't update + // maybe move to `UserStatistics` in api, so `UserStatisticsWatcher` can update the value + globalRankDisplay.UserStatistics.Value = statistics; + + countryRankDisplay.Content.Text = statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; } protected override Drawable CreateLayout() @@ -187,13 +192,7 @@ namespace osu.Game.Users { new Drawable[] { - globalRankDisplay = new ProfileValueDisplay(true) - { - Title = UsersStrings.ShowRankGlobalSimple, - // TODO: implement highest rank tooltip - // `RankHighest` resides in `APIUser`, but `api.LocalUser` doesn't update - // maybe move to `UserStatistics` in api, so `UserStatisticsWatcher` can update the value - }, + globalRankDisplay = new GlobalRankDisplay(), countryRankDisplay = new ProfileValueDisplay(true) { Title = UsersStrings.ShowRankCountrySimple, diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 687dd52594..65bea41e20 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -40,6 +40,9 @@ namespace osu.Game.Users [JsonProperty(@"global_rank")] public int? GlobalRank; + [JsonProperty(@"global_rank_percent")] + public float? GlobalRankPercent; + [JsonProperty(@"country_rank")] public int? CountryRank; From f4049c7ec18fa22904e7c57ce8c725f7824136bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Oct 2025 22:48:49 +0900 Subject: [PATCH 098/308] Suffix introp methods with "Intro" --- .../Multiplayer/TestSceneMultiplayerSkipOverlay.cs | 4 ++-- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 4 ++-- .../Online/Multiplayer/IMultiplayerRoomServer.cs | 2 +- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 14 +++++++------- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 2 +- .../Online/Multiplayer/OnlineMultiplayerClient.cs | 8 ++++---- .../OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 8 ++++---- .../Multiplayer/MultiplayerSkipOverlay.cs | 2 +- .../Visual/Multiplayer/TestMultiplayerClient.cs | 8 ++++---- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs index a1b28e2544..059af2484d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs @@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - AddStep("local user votes", () => MultiplayerClient.VoteToSkip().WaitSafely()); + AddStep("local user votes", () => MultiplayerClient.VoteToSkipIntro().WaitSafely()); for (int i = 0; i < 4; i++) { int i2 = i; - AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkip(i2).WaitSafely()); + AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkipIntro(i2).WaitSafely()); } } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 340fb04731..c91128401d 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -153,11 +153,11 @@ namespace osu.Game.Online.Multiplayer /// /// Signals that a user has requested to skip the beatmap intro. /// - Task UserVotedToSkip(int userId); + Task UserVotedToSkipIntro(int userId); /// /// Signals that the vote to skip the beatmap intro has passed. /// - Task VoteToSkipPassed(); + Task VoteToSkipIntroPassed(); } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index d7834427d0..169d5d1b83 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -115,7 +115,7 @@ namespace osu.Game.Online.Multiplayer /// /// Votes to skip the beatmap intro. /// - Task VoteToSkip(); + Task VoteToSkipIntro(); /// /// Invites a player to the current room. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 3df12e16ea..af2655f0f4 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -132,7 +132,7 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchRoomStateChanged; public event Action? UserVotedToSkip; - public event Action? VoteToSkipPassed; + public event Action? VoteToSkipIntroPassed; /// /// Whether the is currently connected. @@ -496,7 +496,7 @@ namespace osu.Game.Online.Multiplayer public abstract Task RemovePlaylistItem(long playlistItemId); - public abstract Task VoteToSkip(); + public abstract Task VoteToSkipIntro(); Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { @@ -853,7 +853,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(Room != null); foreach (var user in Room.Users) - user.VotedToSkip = false; + user.VotedToSkipIntro = false; GameplayStarted?.Invoke(); }); @@ -925,7 +925,7 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.UserVotedToSkip(int userId) + Task IMultiplayerClient.UserVotedToSkipIntro(int userId) { handleRoomRequest(() => { @@ -937,7 +937,7 @@ namespace osu.Game.Online.Multiplayer if (user == null) return; - user.VotedToSkip = true; + user.VotedToSkipIntro = true; UserVotedToSkip?.Invoke(userId); }); @@ -945,12 +945,12 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.VoteToSkipPassed() + Task IMultiplayerClient.VoteToSkipIntroPassed() { handleRoomRequest(() => { Debug.Assert(Room != null); - VoteToSkipPassed?.Invoke(); + VoteToSkipIntroPassed?.Invoke(); }); return Task.CompletedTask; diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 365a25778b..d19386c98d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online.Multiplayer /// Whether this user voted to skip the beatmap intro. /// [Key(7)] - public bool VotedToSkip; + public bool VotedToSkipIntro; [IgnoreMember] public APIUser? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 54811c5794..1319578c06 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -70,8 +70,8 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded); connection.On(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved); connection.On(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged); - connection.On(nameof(IMultiplayerClient.UserVotedToSkip), ((IMultiplayerClient)this).UserVotedToSkip); - connection.On(nameof(IMultiplayerClient.VoteToSkipPassed), ((IMultiplayerClient)this).VoteToSkipPassed); + connection.On(nameof(IMultiplayerClient.UserVotedToSkipIntro), ((IMultiplayerClient)this).UserVotedToSkipIntro); + connection.On(nameof(IMultiplayerClient.VoteToSkipIntroPassed), ((IMultiplayerClient)this).VoteToSkipIntroPassed); connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined); connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft); @@ -315,14 +315,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } - public override Task VoteToSkip() + public override Task VoteToSkipIntro() { if (!IsConnected.Value) return Task.CompletedTask; Debug.Assert(connection != null); - return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkip)); + return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkipIntro)); } public override Task DisconnectInternal() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 214a7d6403..26535f269c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -120,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.GameplayStarted += onGameplayStarted; client.ResultsReady += onResultsReady; - client.VoteToSkipPassed += onVoteToSkipPassed; + client.VoteToSkipIntroPassed += onVoteToSkipIntroPassed; ScoreProcessor.HasCompleted.BindValueChanged(_ => { @@ -224,10 +224,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void RequestIntroSkip() { // No base call because we aren't skipping yet. - client.VoteToSkip().FireAndForget(); + client.VoteToSkipIntro().FireAndForget(); } - private void onVoteToSkipPassed() + private void onVoteToSkipIntroPassed() { Schedule(() => PerformIntroSkip(true)); } @@ -255,7 +255,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { client.GameplayStarted -= onGameplayStarted; client.ResultsReady -= onResultsReady; - client.VoteToSkipPassed -= onVoteToSkipPassed; + client.VoteToSkipIntroPassed -= onVoteToSkipIntroPassed; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index ccda0e8690..68c6fbe7c5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; int countTotal = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing); - int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkip); + int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkipIntro); int countRequired = countTotal / 2 + 1; countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 83b2da000f..38070d953e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -561,14 +561,14 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); - public override Task VoteToSkip() + public override Task VoteToSkipIntro() { - return UserVoteToSkip(api.LocalUser.Value.OnlineID); + return UserVoteToSkipIntro(api.LocalUser.Value.OnlineID); } - public async Task UserVoteToSkip(int userId) + public async Task UserVoteToSkipIntro(int userId) { - await ((IMultiplayerClient)this).UserVotedToSkip(userId).ConfigureAwait(false); + await ((IMultiplayerClient)this).UserVotedToSkipIntro(userId).ConfigureAwait(false); } protected override Task CreateRoomInternal(MultiplayerRoom room) From 4c81d661aa472d69b74af75c539a96ea66d1bee7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:03:45 +0900 Subject: [PATCH 099/308] Bypass vote for auto-skip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 7 +++++++ .../OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 26535f269c..4cc6f3469d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -223,6 +223,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void RequestIntroSkip() { + // If the room is set up such that the intro is automatically skipped, there's no need to vote on it. + if (Configuration.AutomaticallySkipIntro) + { + base.RequestIntroSkip(); + return; + } + // No base call because we aren't skipping yet. client.VoteToSkipIntro().FireAndForget(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 68c6fbe7c5..747384b220 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void updateText() { - if (client.Room == null) + if (client.Room == null || client.Room.Settings.AutoSkip) return; int countTotal = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing); From c44f701abe08bae1439a3319654df2c9eb991c58 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:06:57 +0900 Subject: [PATCH 100/308] Also update text when users leave --- .../OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 747384b220..927d303988 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -75,12 +75,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); + client.UserLeft += onUserLeft; client.UserStateChanged += onUserStateChanged; client.UserVotedToSkip += onUserVotedToSkip; updateText(); } + private void onUserLeft(MultiplayerRoomUser user) + { + Schedule(updateText); + } + private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) { Schedule(updateText); From 4d706b12ac3b0cc13e44ce6efd8af2d971055195 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:07:18 +0900 Subject: [PATCH 101/308] Fix missing disposal --- .../Multiplayer/MultiplayerSkipOverlay.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 927d303988..f9d394c2b5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -4,6 +4,7 @@ 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.Graphics.Shapes; @@ -116,5 +117,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.UserLeft -= onUserLeft; + client.UserStateChanged -= onUserStateChanged; + client.UserVotedToSkip -= onUserVotedToSkip; + } + } } } From 4ea03d0e0710bb49763518de9fc09785ed0d0d8f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:21:33 +0900 Subject: [PATCH 102/308] Add history footer button to quick play rooms --- .../ScreenMatchmaking.HistoryFooterButton.cs | 40 +++++++++++++++++++ .../Matchmaking/Match/ScreenMatchmaking.cs | 11 ++++- 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.cs diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.cs new file mode 100644 index 0000000000..94e19ab7b9 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.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 osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Footer; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class ScreenMatchmaking + { + private partial class HistoryFooterButton : ScreenFooterButton + { + [Resolved] + private OsuGame? game { get; set; } + + private readonly MultiplayerRoom room; + + public HistoryFooterButton(MultiplayerRoom room) + { + this.room = room; + + Action = openRoomHistory; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Text = "History"; + Icon = FontAwesome.Solid.Globe; + AccentColour = colours.Lime1; + } + + private void openRoomHistory() + => game?.OpenUrlExternally($@"/multiplayer/rooms/{room.RoomID}"); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 527b1ba243..160fdd7405 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.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.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Allocation; @@ -29,6 +30,7 @@ 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.Matchmaking.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Users; @@ -164,7 +166,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Size = new Vector2(700, 130), + Size = new Vector2(600, 130), Margin = new MarginPadding { Bottom = row_padding } } ] @@ -326,6 +328,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match return false; } + public override IReadOnlyList CreateFooterButtons() => + [ + new HistoryFooterButton(room) + ]; + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); @@ -463,7 +470,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match // This component is added to the screen footer which is only about 50px high. // Therefore, it's given a large absolute size to give the context menu enough space to display correctly. - Size = new Vector2(700); + Size = new Vector2(600); InternalChild = new OsuContextMenuContainer { From 78f639d7600f9bce7513494cd9854bc98ac2e17b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:29:51 +0900 Subject: [PATCH 103/308] Attempt to clean up chat size definition --- .../Matchmaking/Match/ScreenMatchmaking.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 160fdd7405..972f0b4adb 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -49,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match /// private const float row_padding = 10; + private static readonly Vector2 chat_size = new Vector2(550, 130); + public override bool? ApplyModTrackAdjustments => true; public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -106,8 +108,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Size = new Vector2(700, 130), - Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING }, + Size = chat_size, + Margin = new MarginPadding + { + Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING, + Bottom = row_padding + }, Alpha = 0 }; } @@ -164,9 +170,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match [ new Container { + Name = "Chat Area Space", Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Size = new Vector2(600, 130), + Size = new Vector2(550, 130), Margin = new MarginPadding { Bottom = row_padding } } ] @@ -470,7 +477,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match // This component is added to the screen footer which is only about 50px high. // Therefore, it's given a large absolute size to give the context menu enough space to display correctly. - Size = new Vector2(600); + Size = new Vector2(chat_size.X); InternalChild = new OsuContextMenuContainer { From 7da051b144a438e23a27e09a5f608d067de2ea73 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 1 Nov 2025 17:45:32 +0900 Subject: [PATCH 104/308] Add test --- .../Matchmaking/TestScenePlayerPanel.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index 21567daabe..0c78038179 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -27,7 +27,13 @@ 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("join other player to room", () => MultiplayerClient.AddUser(new APIUser + { + Id = 2, + Username = "peppy", + })); + + AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(2) { User = new APIUser { @@ -85,9 +91,9 @@ namespace osu.Game.Tests.Visual.Matchmaking UserDictionary = { { - 1, new MatchmakingUser + 2, new MatchmakingUser { - UserId = 1, + UserId = 2, Placement = 1, Points = ++points } @@ -100,7 +106,7 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestJump() { - AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely()); + AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(2, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely()); } [Test] @@ -108,5 +114,14 @@ namespace osu.Game.Tests.Visual.Matchmaking { AddToggleStep("toggle quit", quit => panel.HasQuit = quit); } + + [Test] + public void TestDownloadProgress() + { + AddStep("set download progress 20%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.2f))); + AddStep("set download progress 50%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.5f))); + AddStep("set download progress 90%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.9f))); + AddStep("set locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.LocallyAvailable())); + } } } From 23cb7f3b238653af3371b363f5f545c4a32f4680 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 1 Nov 2025 18:08:19 +0900 Subject: [PATCH 105/308] Add download progress bars to quick play users --- .../Online/Multiplayer/MultiplayerClient.cs | 3 + .../Matchmaking/Match/PlayerPanel.cs | 160 ++++++++++-------- 2 files changed, 97 insertions(+), 66 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 44cbbafe72..df16022e59 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -131,6 +131,8 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchmakingItemDeselected; public event Action? MatchRoomStateChanged; + public event Action? BeatmapAvailabilityChanged; + /// /// Whether the is currently connected. /// This is NOT thread safe and usage should be scheduled. @@ -770,6 +772,7 @@ namespace osu.Game.Online.Multiplayer user.BeatmapAvailability = beatmapAvailability; + BeatmapAvailabilityChanged?.Invoke(user, beatmapAvailability); RoomUpdated?.Invoke(); }); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index e2455eb020..01238bdd9c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; @@ -27,6 +28,7 @@ using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; @@ -107,6 +109,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private BufferedContainer backgroundQuitTarget = null!; private BufferedContainer avatarQuitTarget = null!; + private Box downloadProgressBar = null!; + private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; private bool hasQuit; @@ -158,80 +162,91 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Child = mainContent = new Container + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Children = new[] + mainContent = new Container { - quitText = new OsuSpriteText + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "QUIT", - Font = OsuFont.Default.With(weight: "Bold", size: 70), - Rotation = -22.5f, - Colour = OsuColour.Gray(0.3f), - Blending = BlendingParameters.Additive - }, - avatarPositionTarget = new Container - { - Origin = Anchor.Centre, - Size = avatar_size, - Child = avatarJumpTarget = new Container + quitText = new OsuSpriteText { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "QUIT", + Font = OsuFont.Default.With(weight: "Bold", size: 70), + Rotation = -22.5f, + Colour = OsuColour.Gray(0.3f), + Blending = BlendingParameters.Additive + }, + avatarPositionTarget = new Container + { + Origin = Anchor.Centre, + Size = avatar_size, + Child = avatarJumpTarget = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Child = avatar = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + // Needs to be re-buffered as the avatar is proxied outside of the parent buffered container. + Child = avatarQuitTarget = new BufferedContainer + { + FrameBufferScale = new Vector2(1.5f), + RelativeSizeAxes = Axes.Both, + Child = 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 + { + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomCentre, + Blending = BlendingParameters.Additive, + Margin = new MarginPadding(4), + Text = "-", + Font = OsuFont.Style.Title.With(size: 55), + }, + username = new OsuSpriteText + { + Alpha = 0, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.Both, - Child = avatar = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - // Needs to be re-buffered as the avatar is proxied outside of the parent buffered container. - Child = avatarQuitTarget = new BufferedContainer - { - FrameBufferScale = new Vector2(1.5f), - RelativeSizeAxes = Axes.Both, - Child = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = Vector2.One - } - } - }, + Text = User.Username, + Font = OsuFont.Style.Heading1, + }, + scoreText = new OsuSpriteText + { + Alpha = 0, + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Style.Heading2, + Text = "0 pts" } - }, - rankText = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomCentre, - Blending = BlendingParameters.Additive, - Margin = new MarginPadding(4), - Text = "-", - Font = OsuFont.Style.Title.With(size: 55), - }, - username = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = User.Username, - Font = OsuFont.Style.Heading1, - }, - scoreText = new OsuSpriteText - { - Alpha = 0, - Margin = new MarginPadding(10), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Font = OsuFont.Style.Heading2, - Text = "0 pts" } + }, + downloadProgressBar = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Size = new Vector2(0.5f, 4f), + Colour = colourProvider?.Content2 ?? colours.Gray3 } } } @@ -250,6 +265,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match client.MatchRoomStateChanged += onRoomStateChanged; client.MatchEvent += onMatchEvent; + client.BeatmapAvailabilityChanged += onBeatmapAvailabilityChanged; onRoomStateChanged(client.Room!.MatchState); @@ -472,6 +488,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } } + private void onBeatmapAvailabilityChanged(MultiplayerRoomUser user, BeatmapAvailability availability) => Scheduler.Add(() => + { + if (availability.State == DownloadState.Downloading) + { + downloadProgressBar.FadeIn(200, Easing.OutPow10); + downloadProgressBar.ResizeWidthTo(availability.DownloadProgress ?? 0, 200, Easing.OutPow10); + } + else + downloadProgressBar.FadeOut(200, Easing.OutPow10); + }); + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -480,6 +507,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { client.MatchRoomStateChanged -= onRoomStateChanged; client.MatchEvent -= onMatchEvent; + client.BeatmapAvailabilityChanged -= onBeatmapAvailabilityChanged; } } From 88dd458394c631a71f04343f8fc5708a105e9f98 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Nov 2025 11:36:48 +0900 Subject: [PATCH 106/308] Apply suggestions from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 01238bdd9c..a8555822e6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -245,7 +245,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, - Size = new Vector2(0.5f, 4f), Colour = colourProvider?.Content2 ?? colours.Gray3 } } @@ -491,12 +490,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private void onBeatmapAvailabilityChanged(MultiplayerRoomUser user, BeatmapAvailability availability) => Scheduler.Add(() => { if (availability.State == DownloadState.Downloading) - { downloadProgressBar.FadeIn(200, Easing.OutPow10); - downloadProgressBar.ResizeWidthTo(availability.DownloadProgress ?? 0, 200, Easing.OutPow10); - } else downloadProgressBar.FadeOut(200, Easing.OutPow10); + + downloadProgressBar.ResizeWidthTo(availability.DownloadProgress ?? 0, 200, Easing.OutPow10); }); protected override void Dispose(bool isDisposing) From a8020dea7c80b9c8f3c5b9ce271ba7637f43912a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Nov 2025 12:51:53 +0100 Subject: [PATCH 107/308] Bring back size spec in a better way --- osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index a8555822e6..7b09a3565c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -245,6 +245,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, + Size = new Vector2(0, 4), Colour = colourProvider?.Content2 ?? colours.Gray3 } } From f8331e0b2859d0d849cbf9f39dfa722f7def33c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Nov 2025 12:56:03 +0100 Subject: [PATCH 108/308] Apply one more missed rename --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- .../OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index af2655f0f4..6f98264d23 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -131,7 +131,7 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchmakingItemDeselected; public event Action? MatchRoomStateChanged; - public event Action? UserVotedToSkip; + public event Action? UserVotedToSkipIntro; public event Action? VoteToSkipIntroPassed; /// @@ -939,7 +939,7 @@ namespace osu.Game.Online.Multiplayer user.VotedToSkipIntro = true; - UserVotedToSkip?.Invoke(userId); + UserVotedToSkipIntro?.Invoke(userId); }); return Task.CompletedTask; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index f9d394c2b5..35e85c3273 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.UserLeft += onUserLeft; client.UserStateChanged += onUserStateChanged; - client.UserVotedToSkip += onUserVotedToSkip; + client.UserVotedToSkipIntro += onUserVotedToSkipIntro; updateText(); } @@ -93,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Schedule(updateText); } - private void onUserVotedToSkip(int userId) => Schedule(() => + private void onUserVotedToSkipIntro(int userId) => Schedule(() => { updateText(); @@ -126,7 +126,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { client.UserLeft -= onUserLeft; client.UserStateChanged -= onUserStateChanged; - client.UserVotedToSkip -= onUserVotedToSkip; + client.UserVotedToSkipIntro -= onUserVotedToSkipIntro; } } } From a7e4aa8b1250e7d8467f19ec99a800e6e16a0a27 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 4 Nov 2025 21:27:07 +0500 Subject: [PATCH 109/308] Clamp notification avatar width --- osu.Game/Overlays/Notifications/UserAvatarNotification.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs index 7dbecbf11e..fcc1d59dde 100644 --- a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Notifications protected override void Update() { base.Update(); - IconContent.Width = IconContent.DrawHeight; + IconContent.Width = Math.Min(78, IconContent.DrawHeight); } } } From d98cb9ca45c9ac3ae1e44e9d341d66365d0c1806 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Nov 2025 16:42:32 +0900 Subject: [PATCH 110/308] Correctly link to room history --- .../Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.cs index 94e19ab7b9..f46c0611c5 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.HistoryFooterButton.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } private void openRoomHistory() - => game?.OpenUrlExternally($@"/multiplayer/rooms/{room.RoomID}"); + => game?.OpenUrlExternally($@"/multiplayer/rooms/{room.RoomID}/events"); } } } From 6a6c7ad3ba106f06414f4ce5bba6fb6585ea1aee Mon Sep 17 00:00:00 2001 From: Loreos7 <86934170+Loreos7@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:56:07 +0300 Subject: [PATCH 111/308] Move `Delete...` button to `CommonStrings` --- osu.Game/Localisation/CommonStrings.cs | 5 +++++ osu.Game/Localisation/SongSelectStrings.cs | 5 ----- osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index c8630f9332..324cb424b5 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -199,6 +199,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Mapper => new TranslatableString(getKey(@"mapper"), @"Mapper"); + /// + /// "Delete..." + /// + public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete..."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index 5f940f8a56..c20715fb4c 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -139,11 +139,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ClearAllLocalScores => new TranslatableString(getKey(@"clear_all_local_scores"), @"Clear all local scores"); - /// - /// "Delete..." - /// - public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete..."); - /// /// "Restore all hidden" /// diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs index ae06522b30..c93afe24a5 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(beatmap.BeatmapSet != null); addHeader(SongSelectStrings.ForAllDifficulties, beatmap.BeatmapSet.ToString()); - addButton(SongSelectStrings.Delete, FontAwesome.Solid.Trash, () => SongSelect?.Delete(beatmap.BeatmapSet), colours.Red1); + addButton(CommonStrings.Delete, FontAwesome.Solid.Trash, () => SongSelect?.Delete(beatmap.BeatmapSet), colours.Red1); addHeader(SongSelectStrings.ForSelectedDifficulty, beatmap.DifficultyName); diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index cc55286431..3046155a5e 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -268,7 +268,7 @@ namespace osu.Game.Screens.SelectV2 if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem(SongSelectStrings.RestoreAllHidden, MenuItemType.Standard, () => songSelect?.RestoreAllHidden(beatmapSet))); - items.Add(new OsuMenuItem(SongSelectStrings.Delete, MenuItemType.Destructive, () => songSelect?.Delete(beatmapSet))); + items.Add(new OsuMenuItem(CommonStrings.Delete, MenuItemType.Destructive, () => songSelect?.Delete(beatmapSet))); return items.ToArray(); } } From 20904de276d67939a339fa7b020643192c5e662a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Nov 2025 22:47:21 +0900 Subject: [PATCH 112/308] 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 3ca35b958c..1c3e1a3a9e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From dbefba57ce0b890ac423ae4319873a197efb87b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Nov 2025 08:06:52 +0100 Subject: [PATCH 113/308] Fix pressing Enter on song select with IME active advancing to gameplay instead of confirming choice (#35619) Closes https://github.com/ppy/osu/issues/35568. --- osu.Game/Graphics/UserInterface/SearchTextBox.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index a2e0ab6482..17d714d029 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -54,7 +54,13 @@ namespace osu.Game.Graphics.UserInterface { case Key.KeypadEnter: case Key.Enter: - return false; + // even if committing per se is not allowed for this textbox, + // the commit flow is also responsible for terminating any active IME. + // ensure that the Enter press terminates IME correctly + // and is also handled if it needs to be, so that it doesn't leak to some other non-focused drawable and cause breakage. + bool wasImeComposing = ImeCompositionActive; + FinalizeImeComposition(true); + return wasImeComposing; } } From 4a22ef88ce6ad68831d448047ae2b8deb44995dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Nov 2025 13:14:25 +0100 Subject: [PATCH 114/308] Adjust global rank colour tiers See https://github.com/ppy/osu-web/pull/12522. --- osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs | 2 +- .../Overlays/Profile/Header/Components/GlobalRankDisplay.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs index 07fe8c6172..beabf6711c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneGlobalRankDisplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Online Direction = FillDirection.Full, Padding = new MarginPadding(20), Spacing = new Vector2(40), - ChildrenEnumerable = new int?[] { 64, 423, 1453, 3468, 18_367, 48_342, 178_432, 375_231, 897_783, null }.Select(createDisplay) + ChildrenEnumerable = new int?[] { 64, 423, 1_453, 3_468, 8_367, 48_342, 78_432, 375_231, 897_783, null }.Select(createDisplay) }; private GlobalRankDisplay createDisplay(int? rank) => new GlobalRankDisplay diff --git a/osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs index 3560986925..f48d467d87 100644 --- a/osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/GlobalRankDisplay.cs @@ -75,19 +75,19 @@ namespace osu.Game.Overlays.Profile.Header.Components if (percent < 0.0005) return RankingTier.Radiant; - if (percent < 0.0025) + if (percent < 0.0015) return RankingTier.Rhodium; if (percent < 0.005) return RankingTier.Platinum; - if (percent < 0.025) + if (percent < 0.015) return RankingTier.Gold; if (percent < 0.05) return RankingTier.Silver; - if (percent < 0.25) + if (percent < 0.15) return RankingTier.Bronze; if (percent < 0.5) From 55ae7e8bb81717adcf85fe9d267cbc226090f117 Mon Sep 17 00:00:00 2001 From: "Giovanni D." <37423957+GioSDA@users.noreply.github.com> Date: Thu, 6 Nov 2025 07:01:00 -0600 Subject: [PATCH 115/308] Fix timing of beatmap break overlay (#35566) Issue was bisected to [this commit](https://github.com/ppy/osu/pull/29616/commits/6f1664f0a60fc08995d737e40272b61742fbe580) This change in the commit outlined is what caused the issue: ```diff BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, - Breaks = working.Beatmap.Breaks + BreakTracker = breakTracker, }, ``` `BreakTracker` always initializes breaks as `new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION);` leaving room at the end to account for the fade before resuming gameplay. Because of this, changing the `BreakOverlay` to use a `BreakTracker` instead of the original beatmap breaks caused each break to be `BREAK_FADE_DURATION` shorter than it was originally - which in this case is 325ms - leading to the discrepancy between the background fadeout and the overlay fadeout. Since the current behavior is 'correct', aligning the overlay with the rest of the beatmap such as background fadeout, I changed the timing to account for the shorter duration instead of revert the overlay initialization. --- osu.Game/Screens/Play/BreakOverlay.cs | 7 +++---- osu.Game/Screens/Play/LetterboxOverlay.cs | 4 +--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 2ae66a6dc4..234daece5e 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -165,7 +165,6 @@ namespace osu.Game.Screens.Play private void updateDisplay(ValueChangedEvent period) { - FinishTransforms(true); Scheduler.CancelDelayedTasks(); if (period.NewValue == null) @@ -180,12 +179,12 @@ namespace osu.Game.Screens.Play remainingTimeAdjustmentBox .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) - .Delay(b.Duration - BREAK_FADE_DURATION) + .Delay(b.Duration) .ResizeWidthTo(0); remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod); - remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); + remainingTimeCounter.CountTo(b.Duration + BREAK_FADE_DURATION).CountTo(0, b.Duration + BREAK_FADE_DURATION); remainingTimeCounter.MoveToX(-50) .MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint); @@ -193,7 +192,7 @@ namespace osu.Game.Screens.Play info.MoveToX(50) .MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint); - using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) + using (BeginDelayedSequence(b.Duration)) { fadeContainer.FadeOut(BREAK_FADE_DURATION); breakArrows.Hide(BREAK_FADE_DURATION); diff --git a/osu.Game/Screens/Play/LetterboxOverlay.cs b/osu.Game/Screens/Play/LetterboxOverlay.cs index 21fc6cf19c..f5c762ccf2 100644 --- a/osu.Game/Screens/Play/LetterboxOverlay.cs +++ b/osu.Game/Screens/Play/LetterboxOverlay.cs @@ -61,8 +61,6 @@ namespace osu.Game.Screens.Play private void updateDisplay(ValueChangedEvent period) { - FinishTransforms(true); - if (period.NewValue == null) return; @@ -71,7 +69,7 @@ namespace osu.Game.Screens.Play using (BeginAbsoluteSequence(b.Start)) { fadeContainer.FadeInFromZero(BreakOverlay.BREAK_FADE_DURATION); - using (BeginDelayedSequence(b.Duration - BreakOverlay.BREAK_FADE_DURATION)) + using (BeginDelayedSequence(b.Duration)) fadeContainer.FadeOut(BreakOverlay.BREAK_FADE_DURATION); } } From 933fbd274d0444b843c915c0791f5032456d09c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Nov 2025 15:21:26 +0100 Subject: [PATCH 116/308] Fix incorrect handling of user verification failure response (#35629) `VerificationFailureResponse.RequiredSessionVerificationMethod` not being nullable means that if it was missing in the verification response, it would not be `null` but default to `TimedOneTimePassword` instead, therefore showing TOTP-related error messages to users that never enabled it rather than the user-facing message they were supposed to. Most easily tested on a local full-stack environment with ```diff diff --git a/app/Libraries/SessionVerification/MailState.php b/app/Libraries/SessionVerification/MailState.php index 305a2794ec0..3c2d15f335b 100644 --- a/app/Libraries/SessionVerification/MailState.php +++ b/app/Libraries/SessionVerification/MailState.php @@ -14,7 +14,7 @@ use Carbon\CarbonImmutable; class MailState { - private const KEY_VALID_DURATION = 600; + private const KEY_VALID_DURATION = 10; public readonly CarbonImmutable $expiresAt; public readonly string $key; ``` applied so that you don't have to wait 10 minutes to trigger the failure. --- osu.Game/Online/API/Requests/VerifySessionRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/VerifySessionRequest.cs b/osu.Game/Online/API/Requests/VerifySessionRequest.cs index d8f622348b..88652bce7f 100644 --- a/osu.Game/Online/API/Requests/VerifySessionRequest.cs +++ b/osu.Game/Online/API/Requests/VerifySessionRequest.cs @@ -44,7 +44,7 @@ namespace osu.Game.Online.API.Requests private class VerificationFailureResponse { [JsonProperty("method")] - public SessionVerificationMethod RequiredSessionVerificationMethod { get; set; } + public SessionVerificationMethod? RequiredSessionVerificationMethod { get; set; } } } } From 8c28d2613046a51286b29db73435f92f4a95fa35 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 15:40:48 +0900 Subject: [PATCH 117/308] Document `-1` as a special "random" playlist item --- osu.Game/Online/Matchmaking/IMatchmakingClient.cs | 4 ++++ osu.Game/Online/Matchmaking/IMatchmakingServer.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Matchmaking/IMatchmakingClient.cs b/osu.Game/Online/Matchmaking/IMatchmakingClient.cs index 70e1ce0b5d..be05e3ca0d 100644 --- a/osu.Game/Online/Matchmaking/IMatchmakingClient.cs +++ b/osu.Game/Online/Matchmaking/IMatchmakingClient.cs @@ -43,11 +43,15 @@ namespace osu.Game.Online.Matchmaking /// /// The user has raised a candidate playlist item to be played. /// + /// The notifying user. + /// The playlist item candidate raised, or -1 as a special value that indicates a random selection. Task MatchmakingItemSelected(int userId, long playlistItemId); /// /// The user has removed a candidate playlist item. /// + /// The notifying user. + /// The playlist item candidate removed, or -1 as a special value that indicates a random selection. Task MatchmakingItemDeselected(int userId, long playlistItemId); } } diff --git a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs index 66fd8c36da..7641c57fe9 100644 --- a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs +++ b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs @@ -45,7 +45,7 @@ namespace osu.Game.Online.Matchmaking /// /// Raise a candidate playlist item to be played in the current round. /// - /// The playlist item. + /// The playlist item, or -1 to indicate a random selection. Task MatchmakingToggleSelection(long playlistItemId); /// From 3c215f6574919573e49ff6742086bd9742c22732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Nov 2025 04:01:11 +0100 Subject: [PATCH 118/308] Fix retro skin changing when creating copy for skin editor (#35630) RFC, lowest effort solution for https://github.com/ppy/osu/issues/34979. The `SkinImporter` conditional *is* hella ugly, but anything less ugly will require taking a hammer to structures. Maybe passing version via the import flow, maybe even trying to make the `EnsureMutableSkin()` flow somehow attempt to read the `skin.ini` that's in resources. No idea. Properties from `skin.ini` that were defaults or that lazer can't (won't ever?) understand snipped. --- osu.Game/Skinning/RetroSkin.cs | 18 ++++++++++++++++++ osu.Game/Skinning/SkinImporter.cs | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/RetroSkin.cs b/osu.Game/Skinning/RetroSkin.cs index abeab9ab17..20214dfb67 100644 --- a/osu.Game/Skinning/RetroSkin.cs +++ b/osu.Game/Skinning/RetroSkin.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Extensions; using osu.Game.IO; +using osuTK.Graphics; namespace osu.Game.Skinning { @@ -40,6 +41,23 @@ namespace osu.Game.Skinning new NamespacedResourceStore(resources.Resources, "Skins/Retro") ) { + Configuration.ConfigDictionary[@"SliderBallFlip"] = "0"; + Configuration.ConfigDictionary[@"SliderBallFrames"] = "10"; + Configuration.ConfigDictionary[@"AllowSliderBallTint"] = "0"; + Configuration.ConfigDictionary[@"CursorTrailRotate"] = "0"; + Configuration.ConfigDictionary[@"Version"] = "1"; + + Configuration.CustomComboColours = + [ + new Color4(255, 150, 0, 255), + new Color4(5, 240, 5, 255), + new Color4(5, 5, 240, 255), + new Color4(240, 5, 5, 255) + ]; + + Configuration.ConfigDictionary[@"HitCircleOverlap"] = "3"; + Configuration.ConfigDictionary[@"ScoreOverlap"] = "3"; + Configuration.ConfigDictionary[@"ComboOverlap"] = "3"; } public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 3a50fb9f9a..6290e3439a 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -177,9 +177,10 @@ namespace osu.Game.Skinning if (existingFile == null) { - // skins without a skin.ini are supposed to import using the "latest version" spec. + // skins without a skin.ini are supposed to import using the "latest version" spec, unless we're making a copy of the retro skin which specifies 1.0. // see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298 - newLines.Add(FormattableString.Invariant($"Version: {SkinConfiguration.LATEST_VERSION}")); + decimal version = item.InstantiationInfo == typeof(RetroSkin).GetInvariantInstantiationInfo() ? 1.0M : SkinConfiguration.LATEST_VERSION; + newLines.Add(FormattableString.Invariant($"Version: {version}")); // In the case a skin doesn't have a skin.ini yet, let's create one. writeNewSkinIni(); From 1fbe1bd6c9b1b4dae23e11378fc9edef67d89337 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 7 Nov 2025 00:38:42 +0900 Subject: [PATCH 119/308] Fix selected item callback being lost --- .../Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 1d3153915f..4057f2097b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect AllowSelection = allowSelection, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = ItemSelected, + Action = i => ItemSelected?.Invoke(i), }; panelGridContainer.Add(panel); From b354fa44720da3a010f138248a4d694a74658aed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Oct 2025 19:38:15 +0900 Subject: [PATCH 120/308] Implement random beatmap card --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 17 + .../TestSceneBeatmapSelectPanel.cs | 48 +- .../BeatmapSelect/BeatmapCardMatchmaking.cs | 490 +++--------------- .../BeatmapCardMatchmakingBeatmapContent.cs | 347 +++++++++++++ .../BeatmapCardMatchmakingContent.cs | 153 ++++++ .../BeatmapCardMatchmakingRandomContent.cs | 77 +++ .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 19 +- .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 67 +-- .../BeatmapSelect/SubScreenBeatmapSelect.cs | 14 + 9 files changed, 749 insertions(+), 483 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 4271742b1b..15989dd47d 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -197,6 +197,23 @@ namespace osu.Game.Tests.Visual.Matchmaking }); } + [Test] + public void TestPresentRandomItem() + { + AddStep("present random item panel", () => + { + grid.TransferCandidatePanelsToRollContainer(pickRandomItems(4).candidateItems.Append(-1).ToArray(), duration: 0); + grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); + grid.PlayRollAnimation(-1, duration: 0); + + Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(-1), 500); + }); + + AddWaitStep("wait for animation", 5); + + AddStep("reveal beatmap", () => grid.RevealRandomItem(new MultiplayerPlaylistItem())); + } + private (long[] candidateItems, long finalItem) pickRandomItems(int count) { long[] candidateItems = items.Select(it => it.ID).ToArray(); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 01f76157f1..79eb8f4443 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -2,6 +2,7 @@ // 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; @@ -21,6 +22,24 @@ namespace osu.Game.Tests.Visual.Matchmaking [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + 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 = 0, + StarRating = i / 10.0, + })).ToArray(); + + JoinRoom(room); + }); + } + [Test] public void TestBeatmapPanel() { @@ -58,11 +77,7 @@ namespace osu.Game.Tests.Visual.Matchmaking 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; - }); + AddToggleStep("allow selection", value => panel!.AllowSelection = value); } [Test] @@ -100,5 +115,28 @@ namespace osu.Game.Tests.Visual.Matchmaking }; }); } + + [Test] + public void TestRandomPanel() + { + BeatmapSelectPanel? panel = null; + + AddStep("add panel", () => + { + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem { ID = -1 }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + }); + + AddToggleStep("allow selection", value => panel!.AllowSelection = value); + + AddStep("reveal beatmap", () => panel!.DisplayItem(new MultiplayerPlaylistItem())); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs index 1c8194d587..737649a352 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs @@ -2,465 +2,105 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -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.Graphics; +using osu.Game.Database; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osu.Game.Overlays.BeatmapSet; -using osu.Game.Resources.Localisation.Web; -using osuTK; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class BeatmapCardMatchmaking : BeatmapCard + public partial class BeatmapCardMatchmaking : OsuClickableContainer { - private readonly APIBeatmap beatmap; - - protected override Drawable IdleContent => idleBottomContent; - protected override Drawable DownloadInProgressContent => downloadProgressBar; - + public const float WIDTH = 345; public const float HEIGHT = 80; - [Cached] - private readonly BeatmapCardContent content; - - private BeatmapCardThumbnail thumbnail = null!; - private CollapsibleButtonContainer buttonContainer = null!; - - private FillFlowContainer idleBottomContent = null!; - private BeatmapCardDownloadProgressBar downloadProgressBar = null!; - - public AvatarOverlay SelectionOverlay = null!; - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - [Resolved] - private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + private readonly List users = new List(); - public BeatmapCardMatchmaking(APIBeatmap beatmap) - : base(beatmap.BeatmapSet!, false) - { - this.beatmap = beatmap; - content = new BeatmapCardContent(HEIGHT); - } + private Container contentContainer = null!; + private Drawable flashLayer = null!; + private BeatmapCardMatchmakingContent? content; - [BackgroundDependencyLoader] - private void load(OsuColour colours) + public BeatmapCardMatchmaking() { Width = WIDTH; Height = HEIGHT; + } - FillFlowContainer leftIconArea = null!; - FillFlowContainer titleBadgeArea = null!; - GridContainer artistContainer = null!; - - Child = content.With(c => + [BackgroundDependencyLoader] + private void load() + { + Children = new[] { - c.MainContent = new Container + contentContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + flashLayer = new Box { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Alpha = 0 + } + }; + } + + public void AddUser(APIUser user) + { + users.Add(user); + content?.SelectionOverlay.AddUser(user); + } + + public void RemoveUser(APIUser user) + { + users.Remove(user); + content?.SelectionOverlay.RemoveUser(user.Id); + } + + public void DisplayItem(MultiplayerPlaylistItem item) + { + Task.Run(loadBeatmap); + + async Task loadBeatmap() + { + APIBeatmap? beatmap = await beatmapLookupCache.GetBeatmapAsync(item.BeatmapID).ConfigureAwait(false); + + beatmap ??= new APIBeatmap + { + BeatmapSet = new APIBeatmapSet { - thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet, keepLoaded: true) - { - 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, keepBackgroundLoaded: true) - { - 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[] - { - 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(4), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(6, 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.9f), - }, - new TruncatingSpriteText - { - Text = beatmap.DifficultyName, - Font = OsuFont.Style.Caption1.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 } - } - } - }, - SelectionOverlay = new AvatarOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - } - } - } + Title = "unknown beatmap", + TitleUnicode = "unknown beatmap", + Artist = "unknown artist", + ArtistUnicode = "unknown artist", } }; - c.Expanded.BindTarget = Expanded; - }); - if (BeatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); + beatmap.StarRating = item.StarRating; - 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 } - }; + loadContent(new BeatmapCardMatchmakingBeatmapContent(beatmap)); } } - private LocalisableString createArtistText() + public void DisplayRandom() => loadContent(new BeatmapCardMatchmakingRandomContent()); + + private void loadContent(BeatmapCardMatchmakingContent newContent) => Schedule(() => { - var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); - return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); - } + bool flashNewContent = content != null; - protected override void UpdateState() - { - base.UpdateState(); + contentContainer.Child = content = newContent; - bool showDetails = IsHovered; + foreach (var user in users) + newContent.SelectionOverlay.AddUser(user); - buttonContainer.ShowDetails.Value = showDetails; - thumbnail.Dimmed.Value = showDetails; - } - - public override MenuItem[] ContextMenuItems - { - get - { - List items = new List - { - new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID)) - }; - - foreach (var button in buttonContainer.Buttons) - { - if (button.Enabled.Value) - items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick())); - } - - return items.ToArray(); - } - } - - public partial class AvatarOverlay : CompositeDrawable - { - private readonly Container avatars; - - private Sample? userAddedSample; - private double? lastSamplePlayback; - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - public AvatarOverlay() - { - AutoSizeAxes = Axes.Both; - - InternalChild = avatars = new Container - { - AutoSizeAxes = Axes.X, - Height = SelectionAvatar.AVATAR_SIZE, - }; - - Padding = new MarginPadding { Vertical = 5 }; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); - } - - public bool AddUser(APIUser user) - { - if (avatars.Any(a => a.User.Id == user.Id)) - return false; - - var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value)); - - avatars.Add(avatar); - - if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) - { - userAddedSample?.Play(); - lastSamplePlayback = Time.Current; - } - - updateAvatarLayout(); - - avatar.FinishTransforms(); - - return true; - } - - public bool RemoveUser(int id) - { - if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) - return false; - - avatar.PopOutAndExpire(); - avatars.ChangeChildDepth(avatar, float.MaxValue); - - updateAvatarLayout(); - - return true; - } - - private void updateAvatarLayout() - { - const double stagger = 30; - const float spacing = 4; - - double delay = 0; - float x = 0; - - for (int i = avatars.Count - 1; i >= 0; i--) - { - var avatar = avatars[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 const float AVATAR_SIZE = 30; - - public APIUser User { get; } - - public bool Expired { get; private set; } - - private readonly MatchmakingAvatar avatar; - - public SelectionAvatar(APIUser user, bool isOwnUser) - { - User = user; - Size = new Vector2(AVATAR_SIZE); - - InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - avatar.ScaleTo(0) - .ScaleTo(1, 500, Easing.OutElasticHalf) - .FadeIn(200); - } - - public void PopOutAndExpire() - { - avatar.ScaleTo(0, 400, Easing.OutExpo); - - this.FadeOut(100).Expire(); - Expired = true; - } - } - } + if (flashNewContent) + flashLayer.FadeOutFromOne(1000, Easing.In); + }); } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs new file mode 100644 index 0000000000..a5478b5035 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs @@ -0,0 +1,347 @@ +// 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.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +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; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Online; +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 BeatmapCardMatchmakingBeatmapContent : BeatmapCardMatchmakingContent, IHasContextMenu + { + public override AvatarOverlay SelectionOverlay => selectionOverlay; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + + private readonly IBindable downloadState = new Bindable(); + private readonly IBindableNumber downloadProgress = new BindableDouble(); + private readonly Bindable favouriteState = new Bindable(); + private readonly APIBeatmapSet beatmapSet; + private readonly APIBeatmap beatmap; + + private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + private AvatarOverlay selectionOverlay = null!; + + public BeatmapCardMatchmakingBeatmapContent(APIBeatmap beatmap) + { + this.beatmap = beatmap; + + beatmapSet = beatmap.BeatmapSet!; + favouriteState.Value = new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + FillFlowContainer leftIconArea; + FillFlowContainer titleBadgeArea; + GridContainer artistContainer; + + InternalChildren = new Drawable[] + { + new BeatmapDownloadTracker(beatmap.BeatmapSet!) + { + State = { BindTarget = downloadState }, + Progress = { BindTarget = downloadProgress }, + }, + thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true) + { + Name = @"Left (icon) area", + Size = new Vector2(BeatmapCardMatchmaking.HEIGHT), + Padding = new MarginPadding { Right = BeatmapCard.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, keepBackgroundLoaded: true) + { + X = BeatmapCardMatchmaking.HEIGHT - BeatmapCard.CORNER_RADIUS, + Width = BeatmapCard.WIDTH - BeatmapCardMatchmaking.HEIGHT + BeatmapCard.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 = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)), + 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[] + { + new Container + { + Masking = true, + CornerRadius = BeatmapCard.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(4), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6, 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.9f), + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Style.Caption1.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 = downloadState }, + Progress = { BindTarget = downloadProgress } + } + } + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + } + }; + + 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 } + }; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + downloadState.BindValueChanged(_ => updateState(), true); + + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + thumbnail.Dimmed.Value = showDetails; + + bool showProgress = downloadState.Value == DownloadState.Downloading || downloadState.Value == DownloadState.Importing; + + idleBottomContent.FadeTo(showProgress ? 0 : 1, 340, Easing.OutQuint); + downloadProgressBar.FadeTo(showProgress ? 1 : 0, 340, Easing.OutQuint); + } + + public MenuItem[] ContextMenuItems + { + get + { + List items = new List + { + new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID)) + }; + + 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/BeatmapCardMatchmakingContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.cs new file mode 100644 index 0000000000..8314174a4c --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.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 System.Linq; +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; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public abstract partial class BeatmapCardMatchmakingContent : CompositeDrawable + { + public abstract AvatarOverlay SelectionOverlay { get; } + + protected BeatmapCardMatchmakingContent() + { + RelativeSizeAxes = Axes.Both; + } + + public partial class AvatarOverlay : CompositeDrawable + { + private readonly Container avatars; + + private Sample? userAddedSample; + private double? lastSamplePlayback; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public AvatarOverlay() + { + AutoSizeAxes = Axes.Both; + + InternalChild = avatars = new Container + { + AutoSizeAxes = Axes.X, + Height = SelectionAvatar.AVATAR_SIZE, + }; + + Padding = new MarginPadding { Vertical = 5 }; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); + } + + public bool AddUser(APIUser user) + { + if (avatars.Any(a => a.User.Id == user.Id)) + return false; + + var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value)); + + avatars.Add(avatar); + + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) + { + userAddedSample?.Play(); + lastSamplePlayback = Time.Current; + } + + updateAvatarLayout(); + + avatar.FinishTransforms(); + + return true; + } + + public bool RemoveUser(int id) + { + if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) + return false; + + avatar.PopOutAndExpire(); + avatars.ChangeChildDepth(avatar, float.MaxValue); + + updateAvatarLayout(); + + return true; + } + + private void updateAvatarLayout() + { + const double stagger = 30; + const float spacing = 4; + + double delay = 0; + float x = 0; + + for (int i = avatars.Count - 1; i >= 0; i--) + { + var avatar = avatars[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 const float AVATAR_SIZE = 30; + + public APIUser User { get; } + + public bool Expired { get; private set; } + + private readonly MatchmakingAvatar avatar; + + public SelectionAvatar(APIUser user, bool isOwnUser) + { + User = user; + Size = new Vector2(AVATAR_SIZE); + + InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + avatar.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + } + + public void PopOutAndExpire() + { + avatar.ScaleTo(0, 400, Easing.OutExpo); + + this.FadeOut(100).Expire(); + Expired = true; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs new file mode 100644 index 0000000000..515456abe1 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs @@ -0,0 +1,77 @@ +// 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.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class BeatmapCardMatchmakingRandomContent : BeatmapCardMatchmakingContent + { + public override AvatarOverlay SelectionOverlay => selectionOverlay; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private AvatarOverlay selectionOverlay = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = + [ + new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(32), + Icon = FontAwesome.Solid.Random, + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Random", + } + ] + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + } + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 4057f2097b..cb7cfae4f6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -69,6 +69,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Masking = true, }, }; + + // Special item denoting a random selection. + AddItem(new MultiplayerPlaylistItem { ID = -1 }); } [BackgroundDependencyLoader] @@ -116,14 +119,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect 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)) @@ -135,6 +130,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect panel.RemoveUser(user); } + public void RevealRandomItem(MultiplayerPlaylistItem item) + { + if (!panelLookup.TryGetValue(-1, out var panel)) + return; + + panel.DisplayItem(item); + } + public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) { Debug.Assert(candidateItemIds.Length >= 1); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index aa0329ad94..cbd8480da4 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -2,10 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Diagnostics; using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,7 +10,6 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps.Drawables.Cards; -using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -37,13 +33,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private Container scaleContainer = null!; private Drawable lighting = null!; - private Container border = null!; - private Container mainContent = null!; - - private readonly List users = new List(); - - private BeatmapCardMatchmaking? card; + private BeatmapCardMatchmaking card = null!; public BeatmapSelectPanel(MultiplayerPlaylistItem item) { @@ -52,7 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } [BackgroundDependencyLoader] - private void load(BeatmapLookupCache lookupCache, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { InternalChild = scaleContainer = new Container { @@ -61,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Origin = Anchor.Centre, Children = new[] { - mainContent = new Container + new Container { Masking = true, CornerRadius = BeatmapCard.CORNER_RADIUS, @@ -69,6 +60,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect RelativeSizeAxes = Axes.Both, Children = new[] { + card = new BeatmapCardMatchmaking + { + Action = () => + { + if (AllowSelection) + Action?.Invoke(Item); + }, + }, lighting = new Box { Blending = BlendingParameters.Additive, @@ -107,48 +106,26 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect }, } }; - lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => - { - Debug.Assert(card == null); - APIBeatmap beatmap = b.GetResultSafely() ?? new APIBeatmap - { - BeatmapSet = new APIBeatmapSet - { - Title = "unknown beatmap", - TitleUnicode = "unknown beatmap", - Artist = "unknown artist", - ArtistUnicode = "unknown artist", - } - }; - - beatmap.StarRating = Item.StarRating; - - mainContent.Add(card = new BeatmapCardMatchmaking(beatmap) - { - Depth = float.MaxValue, - Action = () => - { - if (AllowSelection) - Action?.Invoke(Item); - }, - }); - - foreach (var user in users) - card.SelectionOverlay.AddUser(user); - })); + if (Item.ID == -1) + card.DisplayRandom(); + else + card.DisplayItem(Item); } public void AddUser(APIUser user) { - users.Add(user); - card?.SelectionOverlay.AddUser(user); + card.AddUser(user); } public void RemoveUser(APIUser user) { - users.Remove(user); - card?.SelectionOverlay.RemoveUser(user.Id); + card.RemoveUser(user); + } + + public void DisplayItem(MultiplayerPlaylistItem item) + { + card.DisplayItem(item); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index 4b34125517..de83258764 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect @@ -58,6 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect client.MatchmakingItemSelected += onItemSelected; client.MatchmakingItemDeselected += onItemDeselected; + client.SettingsChanged += onSettingsChanged; } private void onItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => @@ -80,6 +82,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect beatmapSelectGrid.SetUserSelection(user, itemId, false); } + private void onSettingsChanged(MultiplayerRoomSettings settings) + { + if (client.Room!.MatchState is not MatchmakingRoomState matchmakingState) + return; + + if (matchmakingState.CandidateItem != -1 || client.Room!.CurrentPlaylistItem.Expired) + return; + + beatmapSelectGrid.RevealRandomItem(client.Room!.CurrentPlaylistItem); + } + public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); protected override void Dispose(bool isDisposing) @@ -91,6 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect client.ItemAdded -= onItemAdded; client.MatchmakingItemSelected -= onItemSelected; client.MatchmakingItemDeselected -= onItemDeselected; + client.SettingsChanged -= onSettingsChanged; } } } From 34a3b1ba78474ef4cf9a7ebc523ab047dbb028bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 7 Nov 2025 17:59:02 +0900 Subject: [PATCH 121/308] Display mods in quick play beatmap cards --- .../TestSceneBeatmapSelectPanel.cs | 29 ++++++ .../BeatmapSelect/BeatmapCardMatchmaking.cs | 15 +++- .../BeatmapCardMatchmakingBeatmapContent.cs | 89 +++++++++++++------ 3 files changed, 103 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 79eb8f4443..023b9b9743 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -12,6 +12,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.Multiplayer; @@ -138,5 +139,33 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("reveal beatmap", () => panel!.DisplayItem(new MultiplayerPlaylistItem())); } + + [Test] + public void TestBeatmapWithMods() + { + AddStep("add panel", () => + { + BeatmapSelectPanel? panel; + + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem + { + RequiredMods = [new APIMod(new OsuModHardRock()), new APIMod(new OsuModDoubleTime())] + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + + panel.AddUser(new APIUser + { + Id = 2, + Username = "peppy", + }); + }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs index 737649a352..96eb9dd0da 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,6 +12,8 @@ using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { @@ -22,6 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + private readonly List users = new List(); private Container contentContainer = null!; @@ -65,6 +71,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public void DisplayItem(MultiplayerPlaylistItem item) { + Ruleset? ruleset = rulesetStore.GetRuleset(item.RulesetID)?.CreateInstance(); + + if (ruleset == null) + return; + + Mod[] mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); + Task.Run(loadBeatmap); async Task loadBeatmap() @@ -84,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect beatmap.StarRating = item.StarRating; - loadContent(new BeatmapCardMatchmakingBeatmapContent(beatmap)); + loadContent(new BeatmapCardMatchmakingBeatmapContent(beatmap, mods)); } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs index a5478b5035..e6a2dfb055 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs @@ -26,6 +26,8 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect @@ -45,6 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private readonly Bindable favouriteState = new Bindable(); private readonly APIBeatmapSet beatmapSet; private readonly APIBeatmap beatmap; + private readonly Mod[] mods; private BeatmapCardThumbnail thumbnail = null!; private CollapsibleButtonContainer buttonContainer = null!; @@ -52,9 +55,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private BeatmapCardDownloadProgressBar downloadProgressBar = null!; private AvatarOverlay selectionOverlay = null!; - public BeatmapCardMatchmakingBeatmapContent(APIBeatmap beatmap) + public BeatmapCardMatchmakingBeatmapContent(APIBeatmap beatmap, Mod[] mods) { this.beatmap = beatmap; + this.mods = mods; beatmapSet = beatmap.BeatmapSet!; favouriteState.Value = new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount); @@ -193,42 +197,69 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect AlwaysPresent = true, Children = new Drawable[] { - new Container + new GridContainer { - Masking = true, - CornerRadius = BeatmapCard.CORNER_RADIUS, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new Drawable[] + ColumnDimensions = new[] { - new Box + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { - Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f), - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - Padding = new MarginPadding(4), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(6, 0), - Children = new Drawable[] + new Container { - new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true) + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Scale = new Vector2(0.9f), - }, - new TruncatingSpriteText - { - Text = beatmap.DifficultyName, - Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new Box + { + Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f), + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Padding = new MarginPadding(4), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6, 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.9f), + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, } - } + }, + new ModFlowDisplay + { + AutoSizeAxes = Axes.Both, + Scale = new Vector2(0.5f), + Margin = new MarginPadding { Left = 5 }, + Current = { Value = mods } + }, }, } }, From 8d80e2bd2c6b023b09d315fd6c456ae926625563 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 7 Nov 2025 18:35:46 +0900 Subject: [PATCH 122/308] Adjust guard to be based on current stage --- .../Match/BeatmapSelect/SubScreenBeatmapSelect.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index de83258764..e0db69783c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -87,7 +87,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect if (client.Room!.MatchState is not MatchmakingRoomState matchmakingState) return; - if (matchmakingState.CandidateItem != -1 || client.Room!.CurrentPlaylistItem.Expired) + if (matchmakingState.Stage != MatchmakingStage.ServerBeatmapFinalised) + return; + + if (matchmakingState.CandidateItem != -1) return; beatmapSelectGrid.RevealRandomItem(client.Room!.CurrentPlaylistItem); From 04d2ce150ad5ded53dcb67ee2654bd313829ffa8 Mon Sep 17 00:00:00 2001 From: Denis Titovets Date: Fri, 7 Nov 2025 14:46:40 +0300 Subject: [PATCH 123/308] Localise `WASAPI` setting --- osu.Game/Localisation/AudioSettingsStrings.cs | 15 +++++++++++++++ .../Sections/Audio/AudioDevicesSettings.cs | 9 +++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs index 58caea7dd4..37ebdd80e0 100644 --- a/osu.Game/Localisation/AudioSettingsStrings.cs +++ b/osu.Game/Localisation/AudioSettingsStrings.cs @@ -99,6 +99,21 @@ namespace osu.Game.Localisation /// public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied."); + /// + /// "Use experimental audio mode" + /// + public static LocalisableString WasapiLabel => new TranslatableString(getKey(@"wasapi_label"), @"Use experimental audio mode"); + + /// + /// "This will attempt to initialise the audio engine in a lower latency mode." + /// + public static LocalisableString WasapiTooltip => new TranslatableString(getKey(@"wasapi_tooltip"), @"This will attempt to initialise the audio engine in a lower latency mode."); + + /// + /// "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value." + /// + public static LocalisableString WasapiNotice => new TranslatableString(getKey(@"wasapi_notice"), @"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index 5b5617bae0..811f6b606a 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -41,8 +41,8 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { Add(wasapiExperimental = new SettingsCheckbox { - LabelText = "Use experimental audio mode", - TooltipText = "This will attempt to initialise the audio engine in a lower latency mode.", + LabelText = AudioSettingsStrings.WasapiLabel, + TooltipText = AudioSettingsStrings.WasapiTooltip, Current = audio.UseExperimentalWasapi, Keywords = new[] { "wasapi", "latency", "exclusive" } }); @@ -64,10 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio if (wasapiExperimental != null) { if (wasapiExperimental.Current.Value) - { - wasapiExperimental.SetNoticeText( - "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.", true); - } + wasapiExperimental.SetNoticeText(AudioSettingsStrings.WasapiNotice, true); else wasapiExperimental.ClearNoticeText(); } From 680614fbeef34d5d08febd2d3b7dd077de3999d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Nov 2025 15:12:12 +0100 Subject: [PATCH 124/308] Fix messages from blocked users being visible in public channels (#35645) * Add failing test coverage for blocking users not removing their messages from public channels * Fix messages from blocked users being visible in public channels Closes https://github.com/ppy/osu/issues/35633. It appears that the expectation from web here is that messages from blocked users should be excised client-side. Compare: https://github.com/ppy/osu-web/blob/12dd504255bddc0cb37701c392c460222b6825db/resources/js/chat/conversation-view.tsx#L104 This implementation won't *restore* the messages after a block and unblock, but I kind of... don't care if I'm honest with you? Making that happen will result in a bunch of complications for no reason, so I'm fine waiting for anyone to complain about it. --- .../Chat/TestSceneChannelManager.cs | 59 +++++++++++++++++++ osu.Game/Online/Chat/ChannelManager.cs | 20 ++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index ef4d4f683a..5c7f0b0a2f 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Chat sentMessages = new List(); silencedUserIds = new List(); + ((DummyAPIAccess)API).LocalUserState.Blocks.Clear(); ((DummyAPIAccess)API).HandleRequest = req => { switch (req) @@ -63,6 +64,10 @@ namespace osu.Game.Tests.Chat silencedUserIds.Clear(); return true; + case GetMessagesRequest getMessages: + getMessages.TriggerSuccess(sentMessages); + return true; + case GetUpdatesRequest updatesRequest: updatesRequest.TriggerSuccess(new GetUpdatesResponse { @@ -161,6 +166,60 @@ namespace osu.Game.Tests.Chat AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands")); } + [Test] + public void TestBlockedUserMessagesAreDeletedFromInitialMessageBatch() + { + Channel channel = null; + + AddStep("create channel", () => channel = createChannel(1, ChannelType.Public)); + AddStep("post a message from blocked user", () => sentMessages.Add(new Message + { + ChannelId = channel.Id, + Content = "i am blocked", + SenderId = 1234 + })); + AddStep("mark user as blocked", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation + { + TargetUser = new APIUser { Username = "blocked", Id = 1234 }, + TargetID = 1234, + })); + + AddStep("join channel and select it", () => + { + channelManager.JoinChannel(channel); + channelManager.CurrentChannel.Value = channel; + }); + AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty); + } + + [Test] + public void TestBlockedUserMessagesAreDeletedImmediatelyOnBlock() + { + Channel channel = null; + + AddStep("create channel", () => channel = createChannel(1, ChannelType.Public)); + + AddStep("join channel and select it", () => + { + channelManager.JoinChannel(channel); + channelManager.CurrentChannel.Value = channel; + }); + AddStep("post a message from blocked user", () => sentMessages.Add(new Message + { + ChannelId = channel.Id, + Content = "i am blocked", + SenderId = 1234 + })); + AddUntilStep("channel has message", () => channel.Messages, () => Is.Not.Empty); + + AddStep("block user", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation + { + TargetUser = new APIUser { Username = "blocked", Id = 1234 }, + TargetID = 1234, + })); + AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty); + } + private void handlePostMessageRequest(PostMessageRequest request) { var message = new Message(++currentMessageId) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index fde6c4db06..eb5d6d1b9c 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -70,6 +71,7 @@ namespace osu.Game.Online.Chat private UserLookupCache users { get; set; } private readonly IBindable apiState = new Bindable(); + private readonly IBindableList localUserBlocks = new BindableList(); private ScheduledDelegate scheduledAck; private IChatClient chatClient = null!; @@ -95,6 +97,9 @@ namespace osu.Game.Online.Chat apiState.BindTo(api.State); apiState.BindValueChanged(_ => SendAck(), true); + + localUserBlocks.BindTo(api.LocalUserState.Blocks); + localUserBlocks.BindCollectionChanged((_, args) => Schedule(() => onBlocksChanged(args))); } /// @@ -311,8 +316,9 @@ namespace osu.Game.Online.Chat private void addMessages(List messages) { var channels = JoinedChannels.ToList(); + var blockedUserIds = localUserBlocks.Select(b => b.TargetID).ToList(); - foreach (var group in messages.GroupBy(m => m.ChannelId)) + foreach (var group in messages.Where(m => !blockedUserIds.Contains(m.SenderId)).GroupBy(m => m.ChannelId)) channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); lastSilenceMessageId ??= messages.LastOrDefault()?.Id; @@ -641,6 +647,18 @@ namespace osu.Game.Online.Chat api.Queue(req); } + private void onBlocksChanged(NotifyCollectionChangedEventArgs args) + { + if (args.Action != NotifyCollectionChangedAction.Add) + return; + + foreach (APIRelation newBlock in args.NewItems!) + { + foreach (var channel in joinedChannels) + channel.RemoveMessagesFromUser(newBlock.TargetID); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 7b55b9e4f285483d4aa3a9563d64d6394c8ecda7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 9 Nov 2025 02:07:13 +0300 Subject: [PATCH 125/308] Change path thickness to 1px Looks better with the new path rendering --- osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs index f23cf5129b..76de6c4724 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; - PathRadius = 0.5f; + PathRadius = 1f; } [BackgroundDependencyLoader] From 1df640898fe2a43c9e2ce5956ea1e27ee7685d9d Mon Sep 17 00:00:00 2001 From: Loreos7 <86934170+Loreos7@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:48:19 +0300 Subject: [PATCH 126/308] Use proper string key --- osu.Game/Localisation/CommonStrings.cs | 2 +- osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 324cb424b5..22fc2bb242 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -202,7 +202,7 @@ namespace osu.Game.Localisation /// /// "Delete..." /// - public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete..."); + public static LocalisableString DeleteEllipsis => new TranslatableString(getKey(@"delete_ellipsis"), @"Delete..."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs index c93afe24a5..afbe2450d6 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(beatmap.BeatmapSet != null); addHeader(SongSelectStrings.ForAllDifficulties, beatmap.BeatmapSet.ToString()); - addButton(CommonStrings.Delete, FontAwesome.Solid.Trash, () => SongSelect?.Delete(beatmap.BeatmapSet), colours.Red1); + addButton(CommonStrings.DeleteEllipsis, FontAwesome.Solid.Trash, () => SongSelect?.Delete(beatmap.BeatmapSet), colours.Red1); addHeader(SongSelectStrings.ForSelectedDifficulty, beatmap.DifficultyName); diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 3046155a5e..71da530e18 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -268,7 +268,7 @@ namespace osu.Game.Screens.SelectV2 if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem(SongSelectStrings.RestoreAllHidden, MenuItemType.Standard, () => songSelect?.RestoreAllHidden(beatmapSet))); - items.Add(new OsuMenuItem(CommonStrings.Delete, MenuItemType.Destructive, () => songSelect?.Delete(beatmapSet))); + items.Add(new OsuMenuItem(CommonStrings.DeleteEllipsis, MenuItemType.Destructive, () => songSelect?.Delete(beatmapSet))); return items.ToArray(); } } From cd6c9405fe0c7dfa21fa69a591a71ab75706d7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Nov 2025 07:43:59 +0100 Subject: [PATCH 127/308] Fix legacy skin drum roll head circle being underneath ticks (#35647) Closes https://github.com/ppy/osu/issues/35321. --- .../Objects/Drawables/DrawableDrumRoll.cs | 33 +++++++++++++++++-- .../Skinning/Legacy/LegacyDrumRoll.cs | 16 ++------- .../Legacy/TaikoLegacySkinTransformer.cs | 6 ++++ .../TaikoSkinComponents.cs | 1 + 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 547d0afe4a..f4dc1f18bd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private int rollingHits; private readonly Container tickContainer; + private SkinnableDrawable headPiece; private Color4 colourIdle; private Color4 colourEngaged; @@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both, - Depth = float.MinValue + Depth = -1, }); } @@ -79,7 +80,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void RecreatePieces() { + if (headPiece != null) + Content.Remove(headPiece, true); + base.RecreatePieces(); + + Content.Add(headPiece = createHeadPiece()); + updateColour(); Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE; } @@ -122,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollBody), _ => new ElongatedCirclePiece()); + private SkinnableDrawable createHeadPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollHead), _ => Empty()) + { + RelativeSizeAxes = Axes.Y, + Depth = -2, + }; + public override bool OnPressed(KeyBindingPressEvent e) => false; private void onNewResult(DrawableHitObject obj, JudgementResult result) @@ -174,7 +187,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private void updateColour(double fadeDuration = 0) { Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); - (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); + + if (fadeDuration == 0) + { + // fade duration is 0 when calling via `RecreatePieces()`. + // in this case we want to apply the colour *without* using transforms. + // using transforms may result in the application of colour being undone via `DrawableHitObject.UpdateState()` clearing transforms. + if (MainPiece.Drawable is IHasAccentColour mainPieceWithAccentColour) + mainPieceWithAccentColour.AccentColour = newColour; + + if (headPiece.Drawable is IHasAccentColour headPieceWithAccentColour) + headPieceWithAccentColour.AccentColour = newColour; + } + else + { + (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); + (headPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); + } } public partial class StrongNestedHit : DrawableStrongNestedHit diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs index 78be0ef643..34339b185d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,14 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { get { - // the reason why this calculation is so involved is that the head & tail sprites have different sizes/radii. - // therefore naively taking the SSDQs of them and making a quad out of them results in a trapezoid shape and not a box. - var headCentre = headCircle.ScreenSpaceDrawQuad.Centre; + var headCentre = (body.ScreenSpaceDrawQuad.TopLeft + body.ScreenSpaceDrawQuad.BottomLeft) / 2; var tailCentre = (tailCircle.ScreenSpaceDrawQuad.TopLeft + tailCircle.ScreenSpaceDrawQuad.BottomLeft) / 2; - float headRadius = headCircle.ScreenSpaceDrawQuad.Height / 2; - float tailRadius = tailCircle.ScreenSpaceDrawQuad.Height / 2; - float radius = Math.Max(headRadius, tailRadius); + float radius = body.ScreenSpaceDrawQuad.Height / 2; var rectangle = new RectangleF(headCentre.X, headCentre.Y, tailCentre.X - headCentre.X, 0).Inflate(radius); return new Quad(rectangle.TopLeft, rectangle.TopRight, rectangle.BottomLeft, rectangle.BottomRight); @@ -37,8 +32,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => ScreenSpaceDrawQuad.Contains(screenSpacePos); - private LegacyCirclePiece headCircle = null!; - private Sprite body = null!; private Sprite tailCircle = null!; @@ -66,10 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy RelativeSizeAxes = Axes.Both, Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge), }, - headCircle = new LegacyCirclePiece - { - RelativeSizeAxes = Axes.Y, - }, }; AccentColour = colours.YellowDark; @@ -101,7 +90,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); - headCircle.AccentColour = colour; body.Colour = colour; tailCircle.Colour = colour; } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index b5c767c2be..73d32a7933 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -103,6 +103,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { switch (taikoComponent.Component) { + case TaikoSkinComponents.DrumRollHead: + if (GetTexture("taiko-roll-middle") != null) + return new LegacyCirclePiece(); + + return null; + case TaikoSkinComponents.DrumRollBody: if (GetTexture("taiko-roll-middle") != null) return new LegacyDrumRoll(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 28133ffcb2..31342b30c4 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Taiko InputDrum, CentreHit, RimHit, + DrumRollHead, DrumRollBody, DrumRollTick, Swell, From 013de9f85d9b7652f04f329643bbee48f1fe69b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8D=E4=BA=88?= Date: Mon, 10 Nov 2025 17:08:00 +0800 Subject: [PATCH 128/308] Add circular progress display to back-to-top button (#35625) * Show circular progress on ScrollBackButton of OverlayScrollContainer * Adjust standardization of position progress --- osu.Game/Overlays/OverlayScrollContainer.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 957008d823..a197748687 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -14,6 +15,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -36,6 +38,7 @@ namespace osu.Game.Overlays public ScrollBackButton Button { get; private set; } private readonly Bindable lastScrollTarget = new Bindable(); + private readonly Bindable progress = new Bindable(); [BackgroundDependencyLoader] private void load() @@ -46,7 +49,8 @@ namespace osu.Game.Overlays Origin = Anchor.BottomRight, Margin = new MarginPadding(20), Action = scrollBack, - LastScrollTarget = { BindTarget = lastScrollTarget } + LastScrollTarget = { BindTarget = lastScrollTarget }, + Progress = { BindTarget = progress }, }); } @@ -54,6 +58,10 @@ namespace osu.Game.Overlays { base.UpdateAfterChildren(); + // Map current position to standardized progress + float height = AvailableContent - DrawHeight; + progress.Value = height == 0 ? 1 : Math.Round(Math.Clamp(Current / height, 0, 1), 3); + if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight) { Button.State = Visibility.Hidden; @@ -110,9 +118,11 @@ namespace osu.Game.Overlays private readonly Container content; private readonly Box background; + private readonly CircularProgress currentCircularProgress; private readonly SpriteIcon spriteIcon; public Bindable LastScrollTarget = new Bindable(); + public Bindable Progress = new Bindable(); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); @@ -145,6 +155,11 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, + currentCircularProgress = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = 0.1f, + }, spriteIcon = new SpriteIcon { Anchor = Anchor.Centre, @@ -164,6 +179,7 @@ namespace osu.Game.Overlays IdleColour = colourProvider.Background6; HoverColour = colourProvider.Background5; flashColour = colourProvider.Light1; + currentCircularProgress.Colour = colourProvider.Highlight1; scrollToTopSample = audio.Samples.Get(@"UI/scroll-to-top"); scrollToPreviousSample = audio.Samples.Get(@"UI/scroll-to-previous"); @@ -173,6 +189,8 @@ namespace osu.Game.Overlays { base.LoadComplete(); + Progress.BindValueChanged(p => currentCircularProgress.Progress = p.NewValue, true); + LastScrollTarget.BindValueChanged(target => { spriteIcon.ScaleTo(target.NewValue != null ? new Vector2(1f, -1f) : Vector2.One, fade_duration, Easing.OutQuint); From c56c52882476231eaf70512e100367bb8f9de900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Nov 2025 11:24:59 +0100 Subject: [PATCH 129/308] Add button for reporting issues to general settings Clicking the button opens the browser, on the "new topic" page inside the help forum. Web can now correctly read the build number of the client since https://github.com/ppy/osu-web/pull/12478 so I see no reason not to. Minimal effort implementation. Stemmed from discussion in https://discord.com/channels/90072389919997952/299846395031060480/1437368033734561792. Not really interested in putting more effort into this at this point, if this is not considered acceptable then just close the PR and this can be revisited more properly at a later date. --- osu.Game/Localisation/GeneralSettingsStrings.cs | 10 ++++++++++ osu.Game/Overlays/Settings/Sections/GeneralSection.cs | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 20db5983fd..7e4ee94286 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -79,6 +79,16 @@ namespace osu.Game.Localisation /// public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ"); + /// + /// "Report an issue" + /// + public static LocalisableString ReportIssue => new TranslatableString(getKey(@"report_issue"), @"Report an issue"); + + /// + /// "Report a problem with the game to the developers." + /// + public static LocalisableString ReportIssueTooltip => new TranslatableString(getKey(@"report_issue_tooltip"), @"Report a problem with the game to the developers."); + /// /// "Check with your package manager / provider for other release streams." /// diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index 2aa1008b1d..848fbd9c7a 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -45,6 +45,12 @@ namespace osu.Game.Overlays.Settings.Sections BackgroundColour = colours.YellowDark, Action = () => game?.ShowWiki(@"Help_centre/Upgrading_to_lazer") }, + new SettingsButton + { + Text = GeneralSettingsStrings.ReportIssue, + TooltipText = GeneralSettingsStrings.ReportIssueTooltip, + Action = () => game?.OpenUrlExternally(@"https://osu.ppy.sh/community/forums/topics/create?forum_id=5") + }, new LanguageSettings(), new UpdateSettings(), }; From 4c72a60ee275e281204028452fe6794dbd38dcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Nov 2025 06:18:40 +0100 Subject: [PATCH 130/308] Delay seeking the current track when dragging now playing overlay progress bar until commit (#35677) RFC. Written to address https://osu.ppy.sh/community/forums/topics/2150023. Few other things we might want to happen here: - pause the track when starting the drag - figure out what to do when a drag is held while the track changes in the background (which was impossible to happen before this) but I want to see the reaction to this first. --- .../Graphics/UserInterface/ProgressBar.cs | 14 ++++++++++- osu.Game/Overlays/NowPlayingOverlay.cs | 23 +++++++++++-------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ProgressBar.cs b/osu.Game/Graphics/UserInterface/ProgressBar.cs index 8f383c76db..dcf96f04c0 100644 --- a/osu.Game/Graphics/UserInterface/ProgressBar.cs +++ b/osu.Game/Graphics/UserInterface/ProgressBar.cs @@ -13,6 +13,8 @@ namespace osu.Game.Graphics.UserInterface { public partial class ProgressBar : SliderBar { + public bool Seeking { get; private set; } + public Action OnSeek; private readonly Box fill; @@ -75,6 +77,16 @@ namespace osu.Game.Graphics.UserInterface fill.Width = value * UsableWidth; } - protected override void OnUserChange(double value) => OnSeek?.Invoke(value); + protected override void OnUserChange(double value) + { + Seeking = true; + } + + protected override bool Commit() + { + OnSeek?.Invoke(CurrentNumber.Value); + Seeking = false; + return base.Commit(); + } } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index a58aa27e24..84c279476f 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -304,18 +304,21 @@ namespace osu.Game.Overlays var track = musicController.CurrentTrack; - if (!track.IsDummyDevice) + if (!progressBar.Seeking) { - progressBar.EndTime = track.Length; - progressBar.CurrentTime = track.CurrentTime; + if (!track.IsDummyDevice) + { + progressBar.EndTime = track.Length; + progressBar.CurrentTime = track.CurrentTime; - playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; - } - else - { - progressBar.CurrentTime = 0; - progressBar.EndTime = 1; - playButton.Icon = FontAwesome.Regular.PlayCircle; + playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; + } + else + { + progressBar.CurrentTime = 0; + progressBar.EndTime = 1; + playButton.Icon = FontAwesome.Regular.PlayCircle; + } } } From 4f783f8c41a11248b9535c80b3649349623455d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Nov 2025 06:20:42 +0100 Subject: [PATCH 131/308] Fix attempting to select beatmap which was just externally edited in song select crashing (#35676) Closes https://github.com/ppy/osu/issues/35651. The reproduction steps provided in the issue are too complex even. In my testing all you need to do is go into editor, replace the background via external editing, and exit out to song select; you'll immediately see loss of selection on the carousel, the set panel still using the old background, and eventually a crash when you attempt to re-select any of the difficulties of the edited set. `HandleItemsChanged()` - an optimisation aiming to reduce the number of redundant re-filters due to minor changes to realm models that aren't visible to the user anyway - ignoring changes to `BeatmapInfo.ID` after re-entering song select post-external edit meant that song select would retain stale beatmap models that no longer existed in the realm database, thus failing refetch attempts via `GetWorkingBeatmap()` or https://github.com/ppy/osu/blob/8f6f859c15bdfc9f10d5754c254fca7b9dd9bc9b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs#L56-L57 --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index edee63c0fa..ee504eefc8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -459,6 +459,8 @@ namespace osu.Game.Screens.SelectV2 // - Background user tag population runs and causes a realm update. // We don't display user tags so want to ignore this. bool equalForDisplayPurposes = + // covers import-as-update flows, such as updating the beatmap with the latest online versions, or external editing inside editor + oldBeatmap.ID == newBeatmap.ID && // covers metadata changes oldBeatmap.Hash == newBeatmap.Hash && // sanity check From e1baa0362239ae63ab1618d387f67a6355a70a3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Nov 2025 18:00:15 +0900 Subject: [PATCH 132/308] 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 8917bc9339..6f0543935b 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 7e219e4b1d..adab5435ea 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 5763b7dbe96cf2ce8fcf5499d7b73eff3129c610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Nov 2025 10:24:11 +0100 Subject: [PATCH 133/308] Fix skin layout deserialisation eating exceptions without logging Because I just wasted 30 minutes trying to debug why a skin provided by a user in an issue thread was failing to deserialise, only to realise halfway through that the deserialisation error I was seeing was *from the fallback path and thus a complete red herring*. --- osu.Game/Skinning/Skin.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 07902106ef..fe0ce5afbc 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -228,8 +228,9 @@ namespace osu.Game.Skinning // First attempt to deserialise using the new SkinLayoutInfo format layout = JsonConvert.DeserializeObject(jsonContent); } - catch + catch (Exception ex) { + Logger.Log($"Deserialising skin layout to {nameof(SkinLayoutInfo)} failed. Falling back to {nameof(SerialisedDrawableInfo)}[].\nDetails: {ex}"); } // If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list. From cb9d9734d692e40750c597bc5d939dabbb70a39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Nov 2025 12:29:39 +0100 Subject: [PATCH 134/308] Move realm collection writes off of update thread (#35681) Probably closes https://github.com/ppy/osu/issues/35650. Realm slow, episode 23894. I can't reproduce freezes as big as the video in the issue is showing but 'realm slow' is 99% the culprit, because affected user's database is not small. --- osu.Game/Collections/CollectionDropdown.cs | 5 +++-- osu.Game/Collections/CollectionToggleMenuItem.cs | 5 +++-- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 4 ++-- osu.Game/Screens/SelectV2/CollectionDropdown.cs | 5 +++-- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 5 +++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index 1e47aff3ec..2f9e94fef7 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -263,11 +264,11 @@ namespace osu.Game.Collections { Debug.Assert(collection != null); - collection.PerformWrite(c => + Task.Run(() => collection.PerformWrite(c => { if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); - }); + })); } protected override Drawable CreateContent() => (Content)base.CreateContent(); diff --git a/osu.Game/Collections/CollectionToggleMenuItem.cs b/osu.Game/Collections/CollectionToggleMenuItem.cs index 5ad06a72c0..e0e278e9a3 100644 --- a/osu.Game/Collections/CollectionToggleMenuItem.cs +++ b/osu.Game/Collections/CollectionToggleMenuItem.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading.Tasks; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; @@ -10,7 +11,7 @@ namespace osu.Game.Collections public class CollectionToggleMenuItem : ToggleMenuItem { public CollectionToggleMenuItem(Live collection, IBeatmapInfo beatmap) - : base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => + : base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => Task.Run(() => { collection.PerformWrite(c => { @@ -19,7 +20,7 @@ namespace osu.Game.Collections else c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash); }); - }) + })) { State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash)); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index c410cb7d69..f0e024663d 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -328,7 +328,7 @@ namespace osu.Game.Screens.Select.Carousel return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s => { - liveCollection.PerformWrite(c => + Task.Run(() => liveCollection.PerformWrite(c => { foreach (var b in beatmapSet.Beatmaps) { @@ -346,7 +346,7 @@ namespace osu.Game.Screens.Select.Carousel break; } } - }); + })); }) { State = { Value = state } diff --git a/osu.Game/Screens/SelectV2/CollectionDropdown.cs b/osu.Game/Screens/SelectV2/CollectionDropdown.cs index a333be5776..9f1950ac5f 100644 --- a/osu.Game/Screens/SelectV2/CollectionDropdown.cs +++ b/osu.Game/Screens/SelectV2/CollectionDropdown.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -237,11 +238,11 @@ namespace osu.Game.Screens.SelectV2 { Debug.Assert(collection != null); - collection.PerformWrite(c => + Task.Run(() => collection.PerformWrite(c => { if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); - }); + })); } protected override Drawable CreateContent() => (Content)base.CreateContent(); diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 792fa90c4e..91645d261c 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -296,7 +297,7 @@ namespace osu.Game.Screens.SelectV2 return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s => { - liveCollection.PerformWrite(c => + Task.Run(() => liveCollection.PerformWrite(c => { foreach (var b in beatmapSet.Beatmaps) { @@ -314,7 +315,7 @@ namespace osu.Game.Screens.SelectV2 break; } } - }); + })); }) { State = { Value = state } From 72507b80c784d03d1a72a102e953f93d2c74e7cf Mon Sep 17 00:00:00 2001 From: Kawaritai <72053972+Kawaritai@users.noreply.github.com> Date: Wed, 12 Nov 2025 06:51:55 +1100 Subject: [PATCH 135/308] Add window sizes in dropdown menu options --- .../Sections/Graphics/LayoutSettings.cs | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index f40a4c941f..0028d21376 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -36,6 +36,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private Bindable scalingMode = null!; private Bindable sizeFullscreen = null!; + private Bindable sizeWindowed = null!; + private readonly BindableWithCurrent currentResolution = new BindableWithCurrent(); private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) }); private readonly IBindable fullscreenCapability = new Bindable(FullscreenCapability.Capable); @@ -70,6 +72,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingMode = osuConfig.GetBindable(OsuSetting.Scaling); sizeFullscreen = config.GetBindable(FrameworkSetting.SizeFullscreen); + sizeWindowed = config.GetBindable(FrameworkSetting.WindowedSize); scalingSizeX = osuConfig.GetBindable(OsuSetting.ScalingSizeX); scalingSizeY = osuConfig.GetBindable(OsuSetting.ScalingSizeY); scalingPositionX = osuConfig.GetBindable(OsuSetting.ScalingPositionX); @@ -105,7 +108,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = GraphicsSettingsStrings.Resolution, ShowsDefaultIndicator = false, ItemSource = resolutions, - Current = sizeFullscreen + Current = currentResolution }, minimiseOnFocusLossCheckbox = new SettingsCheckbox { @@ -196,6 +199,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { updateDisplaySettingsVisibility(); updateScreenModeWarning(); + updateCurrentResolutionBinding(); }, true); currentDisplay.BindValueChanged(display => Schedule(() => @@ -206,15 +210,41 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics return; } + var buffer = new Bindable(currentResolution.Value); + currentResolution.Current = buffer; + resolutions.ReplaceRange(1, resolutions.Count - 1, display.NewValue.DisplayModes .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) .OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width)) .Select(m => m.Size) .Distinct()); + updateCurrentResolutionBinding(); + updateDisplaySettingsVisibility(); }), true); + sizeWindowed.BindValueChanged(size => + { + if (windowModeDropdown.Current.Value != WindowMode.Windowed) + return; + + if (window?.WindowState == Framework.Platform.WindowState.Normal && + size.NewValue == new Size(9999, 9999) + ) + { + window.WindowState = Framework.Platform.WindowState.Maximised; + return; + } + + if (window?.WindowState == Framework.Platform.WindowState.Maximised && + size.NewValue != new Size(9999, 9999) + ) + { + window.WindowState = Framework.Platform.WindowState.Normal; + } + }); + scalingMode.BindValueChanged(_ => { scalingSettings.ClearTransforms(); @@ -223,8 +253,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics updateScalingModeVisibility(); }); - - // initial update bypasses transforms updateScalingModeVisibility(); void updateScalingModeVisibility() @@ -248,6 +276,20 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics } } + private void updateCurrentResolutionBinding() + { + switch (windowModeDropdown.Current.Value) + { + case WindowMode.Fullscreen: + currentResolution.Current = sizeFullscreen; + break; + + case WindowMode.Windowed: + currentResolution.Current = sizeWindowed; + break; + } + } + private void onDisplaysChanged(IEnumerable displays) { Scheduler.AddOnce(d => @@ -260,7 +302,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private void updateDisplaySettingsVisibility() { - resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen; + resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 + && (windowModeDropdown.Current.Value == WindowMode.Fullscreen || + windowModeDropdown.Current.Value == WindowMode.Windowed); displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1; minimiseOnFocusLossCheckbox.CanBeShown.Value = RuntimeInfo.IsDesktop && windowModeDropdown.Current.Value == WindowMode.Fullscreen; safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero; From 4265e72180785fd82af0966381000d5f1ba5dee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Nov 2025 06:10:24 +0100 Subject: [PATCH 136/308] Improve loading time of collection grouping mode (#35693) Supersedes / closes https://github.com/ppy/osu/pull/35687. Implements idea from https://github.com/ppy/osu/pull/35687#issuecomment-3520613982, except without the additional record, because there's no need for it. Co-authored-by: WitherFlower --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 675bb455a5..b8dd65823f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -209,7 +209,7 @@ namespace osu.Game.Screens.SelectV2 case GroupMode.Collections: { var collections = GetCollections(); - return getGroupsBy(b => defineGroupByCollection(b, collections), items); + return defineGroupsByCollection(items, collections); } case GroupMode.MyMaps: @@ -396,29 +396,56 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(0, source).Yield(); } - private IEnumerable defineGroupByCollection(BeatmapInfo beatmap, List collections) + private List defineGroupsByCollection(List carouselItems, List allCollections) { - bool anyCollections = false; + Dictionary groupMappings = new Dictionary(); + // this is a pre-built mapping of MD5s to a list of collections in which this MD5 is found in. + // the reason to pre-build this is that `BeatmapCollection.BeatmapMD5Hashes` is a list and therefore a naive implementation would be slow, + // particularly in edge cases where most beatmaps are in more than one collection. + Dictionary> md5ToCollectionsMap = new Dictionary>(); - for (int i = 0; i < collections.Count; i++) + for (int i = 0; i < allCollections.Count; i++) { - var collection = collections[i]; + var collection = allCollections[i]; + // NOTE: the ordering of the incoming collection list is significant and needs to be preserved. + // the fallback to ordering by name cannot be relied on. + // see xmldoc of `BeatmapCarousel.GetAllCollections()`. + var groupDefinition = new GroupDefinition(i, collection.Name); + groupMappings[groupDefinition] = new GroupMapping(groupDefinition, []); - if (collection.BeatmapMD5Hashes.Contains(beatmap.MD5Hash)) + foreach (string md5 in collection.BeatmapMD5Hashes) { - // NOTE: the ordering of the incoming collection list is significant and needs to be preserved. - // the fallback to ordering by name cannot be relied on. - // see xmldoc of `BeatmapCarousel.GetAllCollections()`. - yield return new GroupDefinition(i, collection.Name); + if (!md5ToCollectionsMap.TryGetValue(md5, out var collections)) + md5ToCollectionsMap[md5] = collections = new List(); - anyCollections = true; + collections.Add(groupDefinition); } } - if (anyCollections) - yield break; + var notInCollection = new GroupDefinition(int.MaxValue, "Not in collection"); + groupMappings[notInCollection] = new GroupMapping(notInCollection, []); - yield return new GroupDefinition(int.MaxValue, "Not in collection"); + foreach (var item in carouselItems) + { + var beatmap = (BeatmapInfo)item.Model; + + // as a side note, even reading the `MD5Hash` off a realm model is slow if done enough times, + // so it definitely helps that thanks to the mapping it needs to only be retrieved once + if (md5ToCollectionsMap.TryGetValue(beatmap.MD5Hash, out var collections)) + { + foreach (var collection in collections) + groupMappings[collection].ItemsInGroup.Add(item); + } + else + groupMappings[notInCollection].ItemsInGroup.Add(item); + } + + return groupMappings.Values + // safety against potentially empty eagerly-initialised groups + // (could happen if user has a collection with MD5s of maps that aren't locally available) + .Where(mapping => mapping.ItemsInGroup.Count > 0) + .OrderBy(mapping => mapping.Group!.Order) + .ToList(); } private IEnumerable defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername) From b64abbf1f521bb0a422b18e4193fd981b16b9a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Nov 2025 15:21:14 +0100 Subject: [PATCH 137/308] Alleviate song select post-filter update thread hitches by caching a model-to-carousel-item mapping (#35628) --- osu.Game/Beatmaps/BeatmapInfo.cs | 6 +++ osu.Game/Graphics/Carousel/Carousel.cs | 51 +++++++++++-------- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 44 +++++++++------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 6 +++ 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index a6b40a26de..1f4d370d13 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -157,6 +157,12 @@ namespace osu.Game.Beatmaps public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b); + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return ID.GetHashCode(); + } + public bool AudioEquals(BeatmapInfo? other) => other != null && BeatmapSet != null && other.BeatmapSet != null diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 4a40862a6f..be1c013478 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -785,32 +785,20 @@ namespace osu.Game.Graphics.Carousel // We are performing two important operations here: // - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions. // - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use. + FindCarouselItemsForSelection(ref currentKeyboardSelection, ref currentSelection, carouselItems); + for (int i = 0; i < count; i++) { var item = carouselItems[i]; - - bool isKeyboardSelection = CheckModelEquality(item.Model, currentKeyboardSelection.Model!); - bool isSelection = CheckModelEquality(item.Model, currentSelection.Model!); - - // while we don't know the Y position of the item yet, as it's about to be updated, - // consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing - // at the correct item to avoid redundant local equality checks. - // the Y positions will be filled in after they're computed. - if (isKeyboardSelection) - currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, null, i); - - if (isSelection) - currentSelection = new Selection(currentSelection.Model, item, null, i); - updateItemYPosition(item, ref lastVisible, ref yPos); - - if (isKeyboardSelection) - currentKeyboardSelection = currentKeyboardSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 }; - - if (isSelection) - currentSelection = currentSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 }; } + if (currentKeyboardSelection.CarouselItem is CarouselItem currentKeyboardSelectionItem) + currentKeyboardSelection = currentKeyboardSelection with { YPosition = currentKeyboardSelectionItem.CarouselYPosition + currentKeyboardSelectionItem.DrawHeight / 2 }; + + if (currentSelection.CarouselItem is CarouselItem currentSelectionItem) + currentSelection = currentSelection with { YPosition = currentSelectionItem.CarouselYPosition + currentSelectionItem.DrawHeight / 2 }; + // Update the total height of all items (to make the scroll container scrollable through the full height even though // most items are not displayed / loaded). Scroll.SetLayoutHeight(yPos + visibleHalfHeight); @@ -821,6 +809,27 @@ namespace osu.Game.Graphics.Carousel Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); } + protected virtual void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList items) + { + for (int i = 0; i < items.Count; i++) + { + var item = items[i]; + + bool isKeyboardSelection = CheckModelEquality(item.Model, keyboardSelection.Model!); + bool isSelection = CheckModelEquality(item.Model, selection.Model!); + + // while we don't know the Y position of the item yet, as it's about to be updated, + // consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing + // at the correct item to avoid redundant local equality checks. + // the Y positions will be filled in after they're computed. + if (isKeyboardSelection) + keyboardSelection = new Selection(keyboardSelection.Model, item, null, i); + + if (isSelection) + selection = new Selection(selection.Model, item, null, i); + } + } + #endregion #region Display handling @@ -1081,7 +1090,7 @@ namespace osu.Game.Graphics.Carousel /// A related carousel item representation for the model. May be null if selection is not present as an item, or if has not been run yet. /// The Y position of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet. /// The index of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet. - private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null); + protected record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null); private record DisplayRange(int First, int Last) { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index ee504eefc8..58874e79d1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -485,6 +485,15 @@ namespace osu.Game.Screens.SelectV2 } } + protected override void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList items) + { + if (keyboardSelection.Model != null && grouping.ItemMap.TryGetValue(keyboardSelection.Model, out var keyboardSelectionItem)) + keyboardSelection = keyboardSelection with { CarouselItem = keyboardSelectionItem.item, Index = keyboardSelectionItem.index }; + + if (selection.Model != null && grouping.ItemMap.TryGetValue(selection.Model, out var selectionItem)) + selection = selection with { CarouselItem = selectionItem.item, Index = selectionItem.index }; + } + protected override void HandleFilterCompleted() { base.HandleFilterCompleted(); @@ -499,14 +508,7 @@ namespace osu.Game.Screens.SelectV2 // The filter might have changed the set of available groups, which means that the current selection may point to a stale group. // Check whether that is the case. bool groupingRemainsOff = currentGroupedBeatmap?.Group == null && grouping.GroupItems.Count == 0; - - bool groupStillValid = false; - - if (currentGroupedBeatmap?.Group != null) - { - groupStillValid = grouping.GroupItems.TryGetValue(currentGroupedBeatmap.Group, out var items) - && items.Any(i => CheckModelEquality(i.Model, currentGroupedBeatmap)); - } + bool groupStillValid = currentGroupedBeatmap?.Group != null && grouping.ItemMap.ContainsKey(currentGroupedBeatmap); if (groupingRemainsOff || groupStillValid) { @@ -699,9 +701,8 @@ namespace osu.Game.Screens.SelectV2 if (CheckModelEquality(ExpandedGroup, CurrentGroupedBeatmap.Group)) return; - var groupItem = GetCarouselItems()?.FirstOrDefault(i => CheckModelEquality(i.Model, CurrentGroupedBeatmap.Group)); - if (groupItem != null) - Activate(groupItem); + if (grouping.ItemMap.TryGetValue(CurrentGroupedBeatmap.Group, out var groupItem)) + Activate(groupItem.item); } protected override double? GetScrollTarget() @@ -712,9 +713,13 @@ namespace osu.Game.Screens.SelectV2 // attempt a fallback to other possibly expanded panels (set first, then group) if (target == null) { - var items = GetCarouselItems(); - var targetItem = items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedBeatmapSet)) - ?? items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedGroup)); + CarouselItem? targetItem = null; + + if (ExpandedBeatmapSet != null && grouping.ItemMap.TryGetValue(ExpandedBeatmapSet, out var setItem)) + targetItem = setItem.item; + + if (targetItem == null && ExpandedGroup != null && grouping.ItemMap.TryGetValue(ExpandedGroup, out var groupItem)) + targetItem = groupItem.item; target = targetItem?.CarouselYPosition; } @@ -924,9 +929,6 @@ namespace osu.Game.Screens.SelectV2 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); - if (x is StarDifficultyGroupDefinition starX && y is StarDifficultyGroupDefinition starY) return starX.Equals(starY); @@ -936,6 +938,14 @@ namespace osu.Game.Screens.SelectV2 if (x is RankedStatusGroupDefinition statusX && y is RankedStatusGroupDefinition statusY) return statusX.Equals(statusY); + // NOTE: this branch must be AFTER all branches that compare `GroupDefinition` subtypes! + // this is an optimisation measure. any subclass of `GroupDefinition` will pass the `is GroupDefinition` check, + // and testing a subclass of `GroupDefinition` against any other `GroupDefinition` (or subclass thereof) + // will result in a casting cascade of `Equals(GroupDefinition) -> Equals(object) -> Equals(GroupDefinitionSubClass)` + // (that last one only if the type check passes) + if (x is GroupDefinition groupX && y is GroupDefinition groupY) + return groupX.Equals(groupY); + return base.CheckModelEquality(x, y); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index b8dd65823f..280db188ef 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 /// public int BeatmapItemsCount { get; private set; } + public IDictionary ItemMap => itemMap; + /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// @@ -36,6 +38,7 @@ namespace osu.Game.Screens.SelectV2 /// public IDictionary> GroupItems => groupMap; + private Dictionary itemMap = new Dictionary(); private Dictionary> setMap = new Dictionary>(); private Dictionary> groupMap = new Dictionary>(); @@ -49,6 +52,7 @@ namespace osu.Game.Screens.SelectV2 return await Task.Run(() => { // preallocate space for the new mappings using last known estimates + var newItemMap = new Dictionary(itemMap.Count); var newSetMap = new Dictionary>(setMap.Count); var newGroupMap = new Dictionary>(groupMap.Count); @@ -127,6 +131,7 @@ namespace osu.Game.Screens.SelectV2 { newItems.Add(i); + newItemMap[i.Model] = (i, newItems.Count - 1); currentGroupItems?.Add(i); currentSetItems?.Add(i); @@ -136,6 +141,7 @@ namespace osu.Game.Screens.SelectV2 cancellationToken.ThrowIfCancellationRequested(); + Interlocked.Exchange(ref itemMap, newItemMap); Interlocked.Exchange(ref setMap, newSetMap); Interlocked.Exchange(ref groupMap, newGroupMap); BeatmapItemsCount = displayedBeatmapsCount; From 435cd272eaa3f291921e804255f33a3c53d92da4 Mon Sep 17 00:00:00 2001 From: Kawaritai <72053972+Kawaritai@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:48:32 +1100 Subject: [PATCH 138/308] Separate fullscreen/windowed dropdowns. Center window on size change. --- .../Sections/Graphics/LayoutSettings.cs | 83 +++++++++---------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 0028d21376..f1211e3a60 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -37,9 +37,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private Bindable scalingMode = null!; private Bindable sizeFullscreen = null!; private Bindable sizeWindowed = null!; - private readonly BindableWithCurrent currentResolution = new BindableWithCurrent(); - private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) }); + private readonly BindableList resolutionsFullscreen = new BindableList(new[] { new Size(9999, 9999) }); + private readonly BindableList resolutionsWindowed = new BindableList(); private readonly IBindable fullscreenCapability = new Bindable(FullscreenCapability.Capable); [Resolved] @@ -50,12 +50,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private IWindow? window; - private SettingsDropdown resolutionDropdown = null!; + private SettingsDropdown resolutionFullscreenDropdown = null!; + private SettingsDropdown resolutionWindowedDropdown = null!; private SettingsDropdown displayDropdown = null!; private SettingsDropdown windowModeDropdown = null!; private SettingsCheckbox minimiseOnFocusLossCheckbox = null!; private SettingsCheckbox safeAreaConsiderationsCheckbox = null!; + private Bindable windowedPositionX = null!; + private Bindable windowedPositionY = null!; private Bindable scalingPositionX = null!; private Bindable scalingPositionY = null!; private Bindable scalingSizeX = null!; @@ -73,6 +76,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingMode = osuConfig.GetBindable(OsuSetting.Scaling); sizeFullscreen = config.GetBindable(FrameworkSetting.SizeFullscreen); sizeWindowed = config.GetBindable(FrameworkSetting.WindowedSize); + windowedPositionX = config.GetBindable(FrameworkSetting.WindowedPositionX); + windowedPositionY = config.GetBindable(FrameworkSetting.WindowedPositionY); scalingSizeX = osuConfig.GetBindable(OsuSetting.ScalingSizeX); scalingSizeY = osuConfig.GetBindable(OsuSetting.ScalingSizeY); scalingPositionX = osuConfig.GetBindable(OsuSetting.ScalingPositionX); @@ -103,12 +108,19 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics Items = window?.Displays, Current = currentDisplay, }, - resolutionDropdown = new ResolutionSettingsDropdown + resolutionFullscreenDropdown = new ResolutionSettingsDropdown { LabelText = GraphicsSettingsStrings.Resolution, ShowsDefaultIndicator = false, - ItemSource = resolutions, - Current = currentResolution + ItemSource = resolutionsFullscreen, + Current = sizeFullscreen + }, + resolutionWindowedDropdown = new ResolutionSettingsDropdown + { + LabelText = GraphicsSettingsStrings.Resolution, + ShowsDefaultIndicator = false, + ItemSource = resolutionsWindowed, + Current = sizeWindowed }, minimiseOnFocusLossCheckbox = new SettingsCheckbox { @@ -199,27 +211,31 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { updateDisplaySettingsVisibility(); updateScreenModeWarning(); - updateCurrentResolutionBinding(); }, true); currentDisplay.BindValueChanged(display => Schedule(() => { if (display.NewValue == null) { - resolutions.Clear(); + resolutionsFullscreen.Clear(); + resolutionsWindowed.Clear(); return; } - var buffer = new Bindable(currentResolution.Value); - currentResolution.Current = buffer; + var buffer = new Bindable(sizeWindowed.Value); + resolutionWindowedDropdown.Current = buffer; - resolutions.ReplaceRange(1, resolutions.Count - 1, display.NewValue.DisplayModes - .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) - .OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width)) - .Select(m => m.Size) - .Distinct()); + var newResolutions = display.NewValue.DisplayModes + .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) + .OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width)) + .Select(m => m.Size) + .Distinct() + .ToList(); - updateCurrentResolutionBinding(); + resolutionsFullscreen.ReplaceRange(1, resolutionsFullscreen.Count - 1, newResolutions); + resolutionsWindowed.ReplaceRange(0, resolutionsWindowed.Count, newResolutions); + + resolutionWindowedDropdown.Current = sizeWindowed; updateDisplaySettingsVisibility(); }), true); @@ -229,20 +245,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (windowModeDropdown.Current.Value != WindowMode.Windowed) return; - if (window?.WindowState == Framework.Platform.WindowState.Normal && - size.NewValue == new Size(9999, 9999) - ) - { - window.WindowState = Framework.Platform.WindowState.Maximised; - return; - } - - if (window?.WindowState == Framework.Platform.WindowState.Maximised && - size.NewValue != new Size(9999, 9999) - ) + if (window?.WindowState == Framework.Platform.WindowState.Maximised) { window.WindowState = Framework.Platform.WindowState.Normal; } + + windowedPositionX.Value = 0.5; + windowedPositionY.Value = 0.5; }); scalingMode.BindValueChanged(_ => @@ -276,20 +285,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics } } - private void updateCurrentResolutionBinding() - { - switch (windowModeDropdown.Current.Value) - { - case WindowMode.Fullscreen: - currentResolution.Current = sizeFullscreen; - break; - - case WindowMode.Windowed: - currentResolution.Current = sizeWindowed; - break; - } - } - private void onDisplaysChanged(IEnumerable displays) { Scheduler.AddOnce(d => @@ -302,9 +297,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private void updateDisplaySettingsVisibility() { - resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 - && (windowModeDropdown.Current.Value == WindowMode.Fullscreen || - windowModeDropdown.Current.Value == WindowMode.Windowed); + resolutionFullscreenDropdown.CanBeShown.Value = windowModeDropdown.Current.Value == WindowMode.Fullscreen && resolutionsFullscreen.Count > 1; + resolutionWindowedDropdown.CanBeShown.Value = windowModeDropdown.Current.Value == WindowMode.Windowed && resolutionsWindowed.Count > 1; + displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1; minimiseOnFocusLossCheckbox.CanBeShown.Value = RuntimeInfo.IsDesktop && windowModeDropdown.Current.Value == WindowMode.Fullscreen; safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero; From 02b88de76e48cc940e77456920ca6ea51e740541 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 14 Nov 2025 19:20:56 +0900 Subject: [PATCH 139/308] Add SFX to the matchmaking roulette random reveal --- .../Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index cb7cfae4f6..967e2777b7 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -43,6 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private readonly Sample?[] spinSamples = new Sample?[5]; private static readonly int[] spin_sample_sequence = [0, 1, 2, 3, 4, 2, 3, 4]; + private Sample? randomRevealSample; private Sample? resultSample; private Sample? swooshSample; private double? lastSamplePlayback; @@ -80,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect for (int i = 0; i < spinSamples.Length; i++) spinSamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Selection/roulette-{i}"); + randomRevealSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/random-reveal"); resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result"); swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); } @@ -136,6 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect return; panel.DisplayItem(item); + randomRevealSample?.Play(); } public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) From 1e91dde92ecfbe9c6c0b92ab48e9a80f149dcfcf Mon Sep 17 00:00:00 2001 From: Kawaritai <72053972+Kawaritai@users.noreply.github.com> Date: Sat, 15 Nov 2025 05:43:22 +1100 Subject: [PATCH 140/308] Separate bindables and centering logic for windowed resolution changes. --- .../Sections/Graphics/LayoutSettings.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index f1211e3a60..99d47aab1f 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -40,6 +40,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private readonly BindableList resolutionsFullscreen = new BindableList(new[] { new Size(9999, 9999) }); private readonly BindableList resolutionsWindowed = new BindableList(); + private readonly Bindable windowedResolution = new Bindable(); private readonly IBindable fullscreenCapability = new Bindable(FullscreenCapability.Capable); [Resolved] @@ -84,6 +85,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY); scalingBackgroundDim = osuConfig.GetBindable(OsuSetting.ScalingBackgroundDim); + windowedResolution.Value = sizeWindowed.Value; + if (window != null) { currentDisplay.BindTo(window.CurrentDisplayBindable); @@ -120,7 +123,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = GraphicsSettingsStrings.Resolution, ShowsDefaultIndicator = false, ItemSource = resolutionsWindowed, - Current = sizeWindowed + Current = windowedResolution }, minimiseOnFocusLossCheckbox = new SettingsCheckbox { @@ -222,7 +225,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics return; } - var buffer = new Bindable(sizeWindowed.Value); + var buffer = new Bindable(windowedResolution.Value); resolutionWindowedDropdown.Current = buffer; var newResolutions = display.NewValue.DisplayModes @@ -235,16 +238,18 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics resolutionsFullscreen.ReplaceRange(1, resolutionsFullscreen.Count - 1, newResolutions); resolutionsWindowed.ReplaceRange(0, resolutionsWindowed.Count, newResolutions); - resolutionWindowedDropdown.Current = sizeWindowed; + resolutionWindowedDropdown.Current = windowedResolution; updateDisplaySettingsVisibility(); }), true); - sizeWindowed.BindValueChanged(size => + windowedResolution.BindValueChanged(size => { - if (windowModeDropdown.Current.Value != WindowMode.Windowed) + if (size.NewValue == sizeWindowed.Value || windowModeDropdown.Current.Value != WindowMode.Windowed) return; + sizeWindowed.Value = size.NewValue; + if (window?.WindowState == Framework.Platform.WindowState.Maximised) { window.WindowState = Framework.Platform.WindowState.Normal; @@ -254,6 +259,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowedPositionY.Value = 0.5; }); + sizeWindowed.BindValueChanged(size => + { + if (size.NewValue != windowedResolution.Value) + windowedResolution.Value = size.NewValue; + }); + scalingMode.BindValueChanged(_ => { scalingSettings.ClearTransforms(); From bd4ed49c067467347429480c8ee25725bd20bff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Nov 2025 08:19:08 +0100 Subject: [PATCH 141/308] Fix several issues with incorrect sample playback (#35685) * Add failing test coverage for layered hit samples not playing in mania when beatmap is converted Adding the `osu.Game.Rulesets.Osu` reference to the mania test project is required so that `HitObjectSampleTest` base logic doesn't die on https://github.com/ppy/osu/blob/f0aeeeea966f06add12cf2bca3dd48dac8573e82/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs#L88-L91 * Fix layered hit sounds not playing on converted beatmaps in mania Compare https://github.com/peppy/osu-stable-reference/blob/f9e58b4864a10f801393199e7652b2192c7342c3/osu!/GameplayElements/HitObjects/HitObject.cs#L476-L477. In case of converted beatmaps, the last condition there (`BeatmapManager.Current.PlayMode != PlayModes.OsuMania`) fails, and thus layered hitsounds are allowed to play. * Add failing test coverage for mania beatmap conversion assigning wrong samples to spinners * Fix mania beatmap conversion assigning wrong samples to spinners A spinner is never `IHasRepeats`. It was a dead condition, leading to the hitobject generating fallback `NodeSamples`, which in particular feature a silent tail which stable doesn't do. Noticeably, stable also appears to force the head of the generated hold note to have no addition sounds: https://github.com/peppy/osu-stable-reference/blob/f9e58b4864a10f801393199e7652b2192c7342c3/osu!/GameplayElements/HitObjects/Mania/SpinnerMania.cs#L86-L89 * Add failing test coverage for file hit sample not falling back to plain samples if file missing * Allow `FileHitSampleInfo` to fall back to standard samples if the file is not found (or not allowed to be looked up) I'm honestly not 100% as to how closely this matches stable because I reached the point wherein I'd rather not look at stable code anymore, so as long as this passes tests I'm fine to wait for someone else to report new breakage. * Use alternative workaround for lack of osu! ruleset assembly in mania test project * Fix encode stability test failures --- .../ManiaBeatmapSampleConversionTest.cs | 1 + .../convert-beatmap-custom-sample-bank.osu | 10 ++++++++++ ...er-convert-samples-expected-conversion.json | 16 ++++++++++++++++ .../Beatmaps/spinner-convert-samples.osu | 18 ++++++++++++++++++ .../TestSceneManiaHitObjectSamples.cs | 14 ++++++++++++++ .../Patterns/Legacy/SpinnerPatternGenerator.cs | 6 +++++- .../Legacy/ManiaLegacySkinTransformer.cs | 6 ++++-- .../Gameplay/TestSceneHitObjectSamples.cs | 16 ++++++++++++++++ .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 8 +++----- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 4 +--- 11 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/convert-beatmap-custom-sample-bank.osu create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/spinner-convert-samples-expected-conversion.json create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/spinner-convert-samples.osu diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs index b4f084a07c..823538919b 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase("mania-samples")] [TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407 [TestCase("slider-convert-samples")] + [TestCase("spinner-convert-samples")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/convert-beatmap-custom-sample-bank.osu b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/convert-beatmap-custom-sample-bank.osu new file mode 100644 index 0000000000..bccaf49023 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/convert-beatmap-custom-sample-bank.osu @@ -0,0 +1,10 @@ +osu file format v14 + +[General] +Mode: 0 + +[TimingPoints] +0,300,4,0,2,100,1,0 + +[HitObjects] +444,320,1000,5,2,0:0:0:0: diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/spinner-convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/spinner-convert-samples-expected-conversion.json new file mode 100644 index 0000000000..6a4ce67ec1 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/spinner-convert-samples-expected-conversion.json @@ -0,0 +1,16 @@ +{ + "Mappings": [{ + "StartTime": 1000.0, + "Objects": [{ + "StartTime": 1000.0, + "EndTime": 8000.0, + "Column": 0, + "PlaySlidingSamples": false, + "NodeSamples": [ + ["Gameplay/soft-hitnormal"], + ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"] + ], + "Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"], + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/spinner-convert-samples.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/spinner-convert-samples.osu new file mode 100644 index 0000000000..b68c5cc055 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/spinner-convert-samples.osu @@ -0,0 +1,18 @@ +osu file format v14 + +[General] +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:5 +OverallDifficulty:5 +ApproachRate:5 +SliderMultiplier:1.4 +SliderTickRate:1 + +[TimingPoints] +0,500,4,2,0,100,1,0 + +[HitObjects] +256,192,1000,8,4,8000,0:2:0:0: diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs index 36ecbdb098..bbac75f74f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs @@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests AssertBeatmapLookup(expected_sample); AssertNoLookup(unwanted_sample); } + + [Test] + public void TestConvertHitObjectCustomSampleBank() + { + const string beatmap_sample = "normal-hitwhistle2"; + const string user_skin_sample = "normal-hitnormal"; + + SetupSkins(beatmap_sample, user_skin_sample); + + CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu"); + + AssertBeatmapLookup(beatmap_sample); + AssertUserLookup(user_skin_sample); + } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SpinnerPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SpinnerPatternGenerator.cs index 39896d3e13..f2ca2888c7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SpinnerPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SpinnerPatternGenerator.cs @@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy Duration = endTime - HitObject.StartTime, Column = column, Samples = HitObject.Samples, - NodeSamples = (HitObject as IHasRepeats)?.NodeSamples + NodeSamples = + [ + HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(), + HitObject.Samples + ] }; } else diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index f0d8430f71..addb96d2c3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private readonly Lazy hasKeyTexture; private readonly ManiaBeatmap beatmap; + private readonly bool isBeatmapConverted; public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap) : base(skin) { this.beatmap = (ManiaBeatmap)beatmap; + isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo); isLegacySkin = new Lazy(() => GetConfig(SkinConfiguration.LegacySetting.Version) != null); hasKeyTexture = new Lazy(() => @@ -196,8 +198,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy public override ISample GetSample(ISampleInfo sampleInfo) { - // layered hit sounds never play in mania - if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered) + // layered hit sounds never play in mania-native beatmaps (but do play on converts) + if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted) return new SampleVirtual(); return base.GetSample(sampleInfo); diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index c9f5f50232..20d63b9bb4 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -126,6 +126,22 @@ namespace osu.Game.Tests.Gameplay AssertBeatmapLookup(expected_sample); } + /// + /// Tests that a hitobject which specifies a specific sample file which doesn't exist (or isn't allowed to be looked up) + /// falls back to a normal sample. + /// + [Test] + public void TestFileSampleFallsBackToNormal() + { + const string expected_sample = "normal-hitnormal"; + + SetupSkins(null, expected_sample); + + CreateTestWithBeatmap("file-beatmap-sample.osu"); + + AssertUserLookup(expected_sample); + } + /// /// Tests that a default hitobject and control point causes . /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index cfca40104f..24976717c1 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -544,7 +544,7 @@ namespace osu.Game.Beatmaps.Formats if (!banksOnly) { int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))); - string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; + string sampleFilename = samples.FirstOrDefault(s => s is ConvertHitObjectParser.FileHitSampleInfo)?.LookupNames.First() ?? string.Empty; int volume = samples.FirstOrDefault()?.Volume ?? 100; // We want to ignore custom sample banks and volume when not encoding to the mania game mode, diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 243f79d906..0a6ef82b77 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -550,7 +550,6 @@ namespace osu.Game.Rulesets.Objects.Legacy } else { - // Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume)); } @@ -680,14 +679,13 @@ namespace osu.Game.Rulesets.Objects.Legacy public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); } - private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable + public class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable { public readonly string Filename; public FileHitSampleInfo(string filename, int volume) // Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin. - // Note that this does not change the lookup names, as they are overridden locally. - : base(string.Empty, customSampleBank: 1, volume: volume) + : base(HIT_NORMAL, SampleControlPoint.DEFAULT_BANK, customSampleBank: 1, volume: volume) { Filename = filename; } @@ -696,7 +694,7 @@ namespace osu.Game.Rulesets.Objects.Legacy { Filename, Path.ChangeExtension(Filename, null) - }; + }.Concat(base.LookupNames); public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newEditorAutoBank = default, Optional newCustomSampleBank = default, Optional newIsLayered = default) diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 1f491be7e3..85c436e9c8 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -86,9 +86,7 @@ namespace osu.Game.Tests.Beatmaps currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader); // populate ruleset for beatmap converters that require it to be present. - var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID); - - Debug.Assert(ruleset != null); + var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID) ?? new RulesetInfo { OnlineID = currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID }; currentTestBeatmap.BeatmapInfo.Ruleset = ruleset; }); From 1c30cb8371a23c40465825fc3ef6ee922e511213 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Nov 2025 20:22:21 +0900 Subject: [PATCH 142/308] 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 6b4b91d14d..9925cf217e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From ce5e54c9d27b17d460d99e774de502f9480fb710 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 17 Nov 2025 13:33:59 +0900 Subject: [PATCH 143/308] Fix various screens not registering themselves as `IPreviewTrackOwner` --- .../Visual/Components/TestScenePreviewTrackManager.cs | 9 ++++++--- osu.Game/Audio/IPreviewTrackOwner.cs | 3 +++ .../Graphics/Containers/OsuFocusedOverlayContainer.cs | 1 - .../Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 1 - osu.Game/Screens/Play/SoloSpectatorScreen.cs | 1 - 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs index b334616125..3cce378247 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs @@ -220,10 +220,13 @@ namespace osu.Game.Tests.Visual.Components protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); if (registerAsOwner) - dependencies.CacheAs(this); - return dependencies; + { + // Automatically handled by interface caching. + return base.CreateChildDependencies(parent); + } + + return new DependencyContainer(); } } diff --git a/osu.Game/Audio/IPreviewTrackOwner.cs b/osu.Game/Audio/IPreviewTrackOwner.cs index 8ab93257a5..e9653aad22 100644 --- a/osu.Game/Audio/IPreviewTrackOwner.cs +++ b/osu.Game/Audio/IPreviewTrackOwner.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; + namespace osu.Game.Audio { /// @@ -10,6 +12,7 @@ namespace osu.Game.Audio /// s can cancel the currently playing through the /// global if they're the owner of the playing . /// + [Cached] public interface IPreviewTrackOwner { } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 1945b2f0dd..3c530a3ace 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -15,7 +15,6 @@ using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { - [Cached(typeof(IPreviewTrackOwner))] public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler { protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 893bc4eb5c..c3648a7edf 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -44,7 +44,6 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge { - [Cached(typeof(IPreviewTrackOwner))] public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap { private readonly Room room; diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 75f8da707c..e54cde4b0a 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -30,7 +30,6 @@ using osuTK; namespace osu.Game.Screens.Play { - [Cached(typeof(IPreviewTrackOwner))] public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner { [Resolved] From 8b778e8106b509f4affabdb6b7682bf5dbc658bc Mon Sep 17 00:00:00 2001 From: maarvin Date: Mon, 17 Nov 2025 06:11:07 +0100 Subject: [PATCH 144/308] Split quickplay beatmap & "random" panel into separate classes (V2) (#35701) * Load all beatmaps in bulk for SubScreenBeatmapSelect * Fix tests no longer working due to drawable changes * Remove test that no longer makes sense * Split matchmaking panel into subclasses for each panel type * Adjust tests to match new structure * Add `ConfigureAwait` * Display loading spinner while beatmaps are being fetched * Fix test failure * Load playlist items directly in `LoadComplete` * Convert `MatchmakingSelectPanel` card content classes into nested classes * Wait for panels to be loaded before operating on them * Add ConfigureAwait() --------- Co-authored-by: Dan Balasescu --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 60 ++- .../TestSceneBeatmapSelectPanel.cs | 55 +-- .../BeatmapSelect/BeatmapCardMatchmaking.cs | 119 ------ .../BeatmapCardMatchmakingBeatmapContent.cs | 378 ----------------- .../BeatmapCardMatchmakingContent.cs | 153 ------- .../BeatmapCardMatchmakingRandomContent.cs | 77 ---- .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 91 +++-- .../BeatmapSelect/MatchmakingPlaylistItem.cs | 14 + .../MatchmakingSelectPanel.CardContent.cs | 156 +++++++ ...tchmakingSelectPanel.CardContentBeatmap.cs | 381 ++++++++++++++++++ ...atchmakingSelectPanel.CardContentRandom.cs | 80 ++++ ...lectPanel.cs => MatchmakingSelectPanel.cs} | 134 +++--- .../MatchmakingSelectPanelBeatmap.cs | 40 ++ .../MatchmakingSelectPanelRandom.cs | 60 +++ .../BeatmapSelect/SubScreenBeatmapSelect.cs | 81 +++- 15 files changed, 963 insertions(+), 916 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingPlaylistItem.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/{BeatmapSelectPanel.cs => MatchmakingSelectPanel.cs} (56%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 15989dd47d..0e5e5b8aae 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -2,17 +2,20 @@ // 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.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.OnlinePlay; using osuTK; @@ -21,7 +24,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene { - private MultiplayerPlaylistItem[] items = null!; + private MatchmakingPlaylistItem[] items = null!; private BeatmapSelectGrid grid = null!; @@ -36,24 +39,44 @@ namespace osu.Game.Tests.Visual.Matchmaking .Take(50) .ToArray(); + IEnumerable playlistItems; + if (beatmaps.Length > 0) { - items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + playlistItems = Enumerable.Range(1, 50).Select(i => { - ID = i, - BeatmapID = beatmaps[i % beatmaps.Length].OnlineID, - StarRating = i / 10.0, - }).ToArray(); + var beatmap = beatmaps[i % beatmaps.Length]; + + return new MatchmakingPlaylistItem( + new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = beatmap.OnlineID, + StarRating = i / 10.0, + }, + CreateAPIBeatmap(beatmap), + Array.Empty() + ); + }); } else { - items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - }).ToArray(); + playlistItems = Enumerable.Range(1, 50).Select(i => new MatchmakingPlaylistItem( + new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + }, + CreateAPIBeatmap(), + Array.Empty() + )); } + + foreach (var item in playlistItems) + item.Beatmap.StarRating = item.PlaylistItem.StarRating; + + items = playlistItems.ToArray(); } public override void SetUpSteps() @@ -70,8 +93,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("add items", () => { - foreach (var item in items) - grid.AddItem(item); + grid.AddItems(items); }); AddWaitStep("wait for panels", 3); @@ -85,17 +107,17 @@ namespace osu.Game.Tests.Visual.Matchmaking // test scene is weird. }); - AddStep("add selection 1", () => grid.ChildrenOfType().First().AddUser(new APIUser + AddStep("add selection 1", () => grid.ChildrenOfType().First().AddUser(new APIUser { Id = DummyAPIAccess.DUMMY_USER_ID, Username = "Maarvin", })); - AddStep("add selection 2", () => grid.ChildrenOfType().Skip(5).First().AddUser(new APIUser + 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 + AddStep("add selection 3", () => grid.ChildrenOfType().Skip(10).First().AddUser(new APIUser { Id = 1040328, Username = "smoogipoo", @@ -180,7 +202,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++) { @@ -211,7 +233,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddWaitStep("wait for animation", 5); - AddStep("reveal beatmap", () => grid.RevealRandomItem(new MultiplayerPlaylistItem())); + AddStep("reveal beatmap", () => grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem)); } private (long[] candidateItems, long finalItem) pickRandomItems(int count) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 023b9b9743..9ac64288ed 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; 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.Overlays; @@ -44,14 +42,14 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestBeatmapPanel() { - BeatmapSelectPanel? panel = null; + MatchmakingSelectPanel? panel = null; AddStep("add panel", () => { Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) + Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [])) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -81,53 +79,17 @@ namespace osu.Game.Tests.Visual.Matchmaking AddToggleStep("allow selection", value => panel!.AllowSelection = value); } - [Test] - public void TestFailedBeatmapLookup() - { - AddStep("setup request handle", () => - { - var api = (DummyAPIAccess)API; - var handler = api.HandleRequest; - api.HandleRequest = req => - { - switch (req) - { - case GetBeatmapRequest: - case GetBeatmapsRequest: - req.TriggerFailure(new InvalidOperationException()); - return false; - - default: - return handler?.Invoke(req) ?? false; - } - }; - }); - - AddStep("add panel", () => - { - Child = new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }; - }); - } - [Test] public void TestRandomPanel() { - BeatmapSelectPanel? panel = null; + MatchmakingSelectPanelRandom? panel = null; AddStep("add panel", () => { Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem { ID = -1 }) + Child = panel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -137,7 +99,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddToggleStep("allow selection", value => panel!.AllowSelection = value); - AddStep("reveal beatmap", () => panel!.DisplayItem(new MultiplayerPlaylistItem())); + AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), [])); } [Test] @@ -145,15 +107,12 @@ namespace osu.Game.Tests.Visual.Matchmaking { AddStep("add panel", () => { - BeatmapSelectPanel? panel; + MatchmakingSelectPanel? panel; Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem - { - RequiredMods = [new APIMod(new OsuModHardRock()), new APIMod(new OsuModDoubleTime())] - }) + Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [new OsuModHardRock(), new OsuModDoubleTime()])) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs deleted file mode 100644 index 96eb9dd0da..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs +++ /dev/null @@ -1,119 +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 System.Linq; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Database; -using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public partial class BeatmapCardMatchmaking : OsuClickableContainer - { - public const float WIDTH = 345; - public const float HEIGHT = 80; - - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - - [Resolved] - private RulesetStore rulesetStore { get; set; } = null!; - - private readonly List users = new List(); - - private Container contentContainer = null!; - private Drawable flashLayer = null!; - private BeatmapCardMatchmakingContent? content; - - public BeatmapCardMatchmaking() - { - Width = WIDTH; - Height = HEIGHT; - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new[] - { - contentContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - flashLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0 - } - }; - } - - public void AddUser(APIUser user) - { - users.Add(user); - content?.SelectionOverlay.AddUser(user); - } - - public void RemoveUser(APIUser user) - { - users.Remove(user); - content?.SelectionOverlay.RemoveUser(user.Id); - } - - public void DisplayItem(MultiplayerPlaylistItem item) - { - Ruleset? ruleset = rulesetStore.GetRuleset(item.RulesetID)?.CreateInstance(); - - if (ruleset == null) - return; - - Mod[] mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); - - Task.Run(loadBeatmap); - - async Task loadBeatmap() - { - APIBeatmap? beatmap = await beatmapLookupCache.GetBeatmapAsync(item.BeatmapID).ConfigureAwait(false); - - beatmap ??= new APIBeatmap - { - BeatmapSet = new APIBeatmapSet - { - Title = "unknown beatmap", - TitleUnicode = "unknown beatmap", - Artist = "unknown artist", - ArtistUnicode = "unknown artist", - } - }; - - beatmap.StarRating = item.StarRating; - - loadContent(new BeatmapCardMatchmakingBeatmapContent(beatmap, mods)); - } - } - - public void DisplayRandom() => loadContent(new BeatmapCardMatchmakingRandomContent()); - - private void loadContent(BeatmapCardMatchmakingContent newContent) => Schedule(() => - { - bool flashNewContent = content != null; - - contentContainer.Child = content = newContent; - - foreach (var user in users) - newContent.SelectionOverlay.AddUser(user); - - if (flashNewContent) - flashLayer.FadeOutFromOne(1000, Easing.In); - }); - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs deleted file mode 100644 index e6a2dfb055..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs +++ /dev/null @@ -1,378 +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.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -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; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Beatmaps.Drawables.Cards; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; -using osu.Game.Online; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osu.Game.Overlays.BeatmapSet; -using osu.Game.Resources.Localisation.Web; -using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play.HUD; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public partial class BeatmapCardMatchmakingBeatmapContent : BeatmapCardMatchmakingContent, IHasContextMenu - { - public override AvatarOverlay SelectionOverlay => selectionOverlay; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - [Resolved] - private BeatmapSetOverlay? beatmapSetOverlay { get; set; } - - private readonly IBindable downloadState = new Bindable(); - private readonly IBindableNumber downloadProgress = new BindableDouble(); - private readonly Bindable favouriteState = new Bindable(); - private readonly APIBeatmapSet beatmapSet; - private readonly APIBeatmap beatmap; - private readonly Mod[] mods; - - private BeatmapCardThumbnail thumbnail = null!; - private CollapsibleButtonContainer buttonContainer = null!; - private FillFlowContainer idleBottomContent = null!; - private BeatmapCardDownloadProgressBar downloadProgressBar = null!; - private AvatarOverlay selectionOverlay = null!; - - public BeatmapCardMatchmakingBeatmapContent(APIBeatmap beatmap, Mod[] mods) - { - this.beatmap = beatmap; - this.mods = mods; - - beatmapSet = beatmap.BeatmapSet!; - favouriteState.Value = new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - FillFlowContainer leftIconArea; - FillFlowContainer titleBadgeArea; - GridContainer artistContainer; - - InternalChildren = new Drawable[] - { - new BeatmapDownloadTracker(beatmap.BeatmapSet!) - { - State = { BindTarget = downloadState }, - Progress = { BindTarget = downloadProgress }, - }, - thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true) - { - Name = @"Left (icon) area", - Size = new Vector2(BeatmapCardMatchmaking.HEIGHT), - Padding = new MarginPadding { Right = BeatmapCard.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, keepBackgroundLoaded: true) - { - X = BeatmapCardMatchmaking.HEIGHT - BeatmapCard.CORNER_RADIUS, - Width = BeatmapCard.WIDTH - BeatmapCardMatchmaking.HEIGHT + BeatmapCard.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 = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)), - 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[] - { - 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 Container - { - Masking = true, - CornerRadius = BeatmapCard.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(4), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(6, 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.9f), - }, - new TruncatingSpriteText - { - Text = beatmap.DifficultyName, - Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - } - }, - new ModFlowDisplay - { - AutoSizeAxes = Axes.Both, - Scale = new Vector2(0.5f), - Margin = new MarginPadding { Left = 5 }, - Current = { Value = mods } - }, - }, - } - }, - } - }, - downloadProgressBar = new BeatmapCardDownloadProgressBar - { - RelativeSizeAxes = Axes.X, - Height = 5, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = { BindTarget = downloadState }, - Progress = { BindTarget = downloadProgress } - } - } - }, - selectionOverlay = new AvatarOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - } - } - } - }; - - 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 } - }; - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - downloadState.BindValueChanged(_ => updateState(), true); - - FinishTransforms(true); - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - bool showDetails = IsHovered; - - buttonContainer.ShowDetails.Value = showDetails; - thumbnail.Dimmed.Value = showDetails; - - bool showProgress = downloadState.Value == DownloadState.Downloading || downloadState.Value == DownloadState.Importing; - - idleBottomContent.FadeTo(showProgress ? 0 : 1, 340, Easing.OutQuint); - downloadProgressBar.FadeTo(showProgress ? 1 : 0, 340, Easing.OutQuint); - } - - public MenuItem[] ContextMenuItems - { - get - { - List items = new List - { - new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID)) - }; - - 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/BeatmapCardMatchmakingContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.cs deleted file mode 100644 index 8314174a4c..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.cs +++ /dev/null @@ -1,153 +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.Linq; -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; -using osu.Game.Online.API.Requests.Responses; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public abstract partial class BeatmapCardMatchmakingContent : CompositeDrawable - { - public abstract AvatarOverlay SelectionOverlay { get; } - - protected BeatmapCardMatchmakingContent() - { - RelativeSizeAxes = Axes.Both; - } - - public partial class AvatarOverlay : CompositeDrawable - { - private readonly Container avatars; - - private Sample? userAddedSample; - private double? lastSamplePlayback; - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - public AvatarOverlay() - { - AutoSizeAxes = Axes.Both; - - InternalChild = avatars = new Container - { - AutoSizeAxes = Axes.X, - Height = SelectionAvatar.AVATAR_SIZE, - }; - - Padding = new MarginPadding { Vertical = 5 }; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); - } - - public bool AddUser(APIUser user) - { - if (avatars.Any(a => a.User.Id == user.Id)) - return false; - - var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value)); - - avatars.Add(avatar); - - if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) - { - userAddedSample?.Play(); - lastSamplePlayback = Time.Current; - } - - updateAvatarLayout(); - - avatar.FinishTransforms(); - - return true; - } - - public bool RemoveUser(int id) - { - if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) - return false; - - avatar.PopOutAndExpire(); - avatars.ChangeChildDepth(avatar, float.MaxValue); - - updateAvatarLayout(); - - return true; - } - - private void updateAvatarLayout() - { - const double stagger = 30; - const float spacing = 4; - - double delay = 0; - float x = 0; - - for (int i = avatars.Count - 1; i >= 0; i--) - { - var avatar = avatars[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 const float AVATAR_SIZE = 30; - - public APIUser User { get; } - - public bool Expired { get; private set; } - - private readonly MatchmakingAvatar avatar; - - public SelectionAvatar(APIUser user, bool isOwnUser) - { - User = user; - Size = new Vector2(AVATAR_SIZE); - - InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - avatar.ScaleTo(0) - .ScaleTo(1, 500, Easing.OutElasticHalf) - .FadeIn(200); - } - - public void PopOutAndExpire() - { - avatar.ScaleTo(0, 400, Easing.OutExpo); - - this.FadeOut(100).Expire(); - Expired = true; - } - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs deleted file mode 100644 index 515456abe1..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs +++ /dev/null @@ -1,77 +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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics.Sprites; -using osu.Game.Overlays; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public partial class BeatmapCardMatchmakingRandomContent : BeatmapCardMatchmakingContent - { - public override AvatarOverlay SelectionOverlay => selectionOverlay; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - private AvatarOverlay selectionOverlay = null!; - - [BackgroundDependencyLoader] - private void load() - { - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background2, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = - [ - new SpriteIcon - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(32), - Icon = FontAwesome.Solid.Random, - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Random", - } - ] - }, - selectionOverlay = new AvatarOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - } - } - } - }; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 967e2777b7..d27b0e3818 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Microsoft.Toolkit.HighPerformance; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -33,10 +34,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public event Action? ItemSelected; - private readonly Dictionary panelLookup = new Dictionary(); + private readonly Dictionary panelLookup = new Dictionary(); + private readonly Dictionary playlistItems = new Dictionary(); + private MatchmakingSelectPanelRandom randomPanel = null!; private readonly PanelGridContainer panelGridContainer; - private readonly Container rollContainer; + private readonly Container rollContainer; private readonly OsuScrollContainer scroll; private bool allowSelection = true; @@ -64,15 +67,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Spacing = new Vector2(panel_spacing) }, }, - rollContainer = new Container + rollContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true, }, }; - - // Special item denoting a random selection. - AddItem(new MultiplayerPlaylistItem { ID = -1 }); } [BackgroundDependencyLoader] @@ -86,9 +86,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); } - protected override void LoadComplete() + public void AddItems(IEnumerable items) { - base.LoadComplete(); + foreach (var item in items) + { + playlistItems[item.ID] = item; + + var panel = panelLookup[item.ID] = new MatchmakingSelectPanelBeatmap(item) + { + AllowSelection = allowSelection, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = i => ItemSelected?.Invoke(i), + }; + + panelGridContainer.Add(panel); + panelGridContainer.SetLayoutPosition(panel, (float)panel.Item.StarRating); + } + + panelLookup[-1] = randomPanel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 }) + { + AllowSelection = allowSelection, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = i => ItemSelected?.Invoke(i), + }; + panelGridContainer.Add(randomPanel); + panelGridContainer.SetLayoutPosition(randomPanel, float.MinValue); const double enter_duration = 500; @@ -104,24 +128,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect panel.FadeInAndEnterFromBelow(duration: enter_duration, delay: delay); } + + panelsLoaded.SetResult(); }); } - public void AddItem(MultiplayerPlaylistItem item) - { - var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item) - { - AllowSelection = allowSelection, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Action = i => ItemSelected?.Invoke(i), - }; - - panelGridContainer.Add(panel); - panelGridContainer.SetLayoutPosition(panel, (float)item.StarRating); - } - - public void SetUserSelection(APIUser user, long itemId, bool selected) + public void SetUserSelection(APIUser user, long itemId, bool selected) => whenPanelsLoaded(() => { if (!panelLookup.TryGetValue(itemId, out var panel)) return; @@ -130,18 +142,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect panel.AddUser(user); else panel.RemoveUser(user); - } + }); - public void RevealRandomItem(MultiplayerPlaylistItem item) + public void RevealRandomItem(MultiplayerPlaylistItem item) => whenPanelsLoaded(() => { - if (!panelLookup.TryGetValue(-1, out var panel)) - return; + playlistItems.TryGetValue(item.ID, out var playlistItem); + + Debug.Assert(playlistItem != null); - panel.DisplayItem(item); randomRevealSample?.Play(); - } + randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods); + }); - public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) + public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) => whenPanelsLoaded(() => { Debug.Assert(candidateItemIds.Length >= 1); Debug.Assert(candidateItemIds.Contains(finalItemId)); @@ -168,7 +181,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect .Delay(roll_duration + present_beatmap_delay) .Schedule(() => PresentRolledBeatmap(finalItemId)); } - } + }); internal void TransferCandidatePanelsToRollContainer(long[] candidateItemIds, double duration = hide_duration) { @@ -177,7 +190,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()) { @@ -217,7 +230,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { var panel = rollContainer.Children[i]; - var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing)); + var position = positions[i] * (MatchmakingSelectPanel.SIZE + new Vector2(panel_spacing)); panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); @@ -286,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex) numSteps++; - BeatmapSelectPanel? lastPanel = null; + MatchmakingSelectPanel? lastPanel = null; for (int i = 0; i < numSteps; i++) { @@ -347,7 +360,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect PresentRolledBeatmap(finalItem); } - private partial class PanelGridContainer : FillFlowContainer + private readonly TaskCompletionSource panelsLoaded = new TaskCompletionSource(); + + private void whenPanelsLoaded(Action action) => Task.Run(async () => + { + await panelsLoaded.Task.ConfigureAwait(false); + Schedule(action); + }); + + private partial class PanelGridContainer : FillFlowContainer { public bool LayoutDisabled; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingPlaylistItem.cs new file mode 100644 index 0000000000..6b7fb9f21e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingPlaylistItem.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public record MatchmakingPlaylistItem(MultiplayerPlaylistItem PlaylistItem, APIBeatmap Beatmap, Mod[] Mods) + { + public long ID => PlaylistItem.ID; + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs new file mode 100644 index 0000000000..48c64f2f66 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs @@ -0,0 +1,156 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanel + { + public abstract partial class CardContent : CompositeDrawable + { + public abstract AvatarOverlay SelectionOverlay { get; } + + protected CardContent() + { + RelativeSizeAxes = Axes.Both; + } + + public partial class AvatarOverlay : CompositeDrawable + { + private readonly Container avatars; + + private Sample? userAddedSample; + private double? lastSamplePlayback; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public AvatarOverlay() + { + AutoSizeAxes = Axes.Both; + + InternalChild = avatars = new Container + { + AutoSizeAxes = Axes.X, + Height = SelectionAvatar.AVATAR_SIZE, + }; + + Padding = new MarginPadding { Vertical = 5 }; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); + } + + public bool AddUser(APIUser user) + { + if (avatars.Any(a => a.User.Id == user.Id)) + return false; + + var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value)); + + avatars.Add(avatar); + + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) + { + userAddedSample?.Play(); + lastSamplePlayback = Time.Current; + } + + updateAvatarLayout(); + + avatar.FinishTransforms(); + + return true; + } + + public bool RemoveUser(int id) + { + if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) + return false; + + avatar.PopOutAndExpire(); + avatars.ChangeChildDepth(avatar, float.MaxValue); + + updateAvatarLayout(); + + return true; + } + + private void updateAvatarLayout() + { + const double stagger = 30; + const float spacing = 4; + + double delay = 0; + float x = 0; + + for (int i = avatars.Count - 1; i >= 0; i--) + { + var avatar = avatars[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 const float AVATAR_SIZE = 30; + + public APIUser User { get; } + + public bool Expired { get; private set; } + + private readonly MatchmakingAvatar avatar; + + public SelectionAvatar(APIUser user, bool isOwnUser) + { + User = user; + Size = new Vector2(AVATAR_SIZE); + + InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + avatar.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + } + + public void PopOutAndExpire() + { + avatar.ScaleTo(0, 400, Easing.OutExpo); + + this.FadeOut(100).Expire(); + Expired = true; + } + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs new file mode 100644 index 0000000000..b27ab2850b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs @@ -0,0 +1,381 @@ +// 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.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +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; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapSet; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanel + { + public partial class CardContentBeatmap : CardContent, IHasContextMenu + { + public override AvatarOverlay SelectionOverlay => selectionOverlay; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + + private readonly IBindable downloadState = new Bindable(); + private readonly IBindableNumber downloadProgress = new BindableDouble(); + private readonly Bindable favouriteState = new Bindable(); + private readonly APIBeatmapSet beatmapSet; + private readonly APIBeatmap beatmap; + private readonly Mod[] mods; + + private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + private AvatarOverlay selectionOverlay = null!; + + public CardContentBeatmap(APIBeatmap beatmap, Mod[] mods) + { + this.beatmap = beatmap; + this.mods = mods; + + beatmapSet = beatmap.BeatmapSet!; + favouriteState.Value = new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + FillFlowContainer leftIconArea; + FillFlowContainer titleBadgeArea; + GridContainer artistContainer; + + InternalChildren = new Drawable[] + { + new BeatmapDownloadTracker(beatmap.BeatmapSet!) + { + State = { BindTarget = downloadState }, + Progress = { BindTarget = downloadProgress }, + }, + thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true) + { + Name = @"Left (icon) area", + Size = new Vector2(MatchmakingSelectPanel.HEIGHT), + Padding = new MarginPadding { Right = BeatmapCard.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, keepBackgroundLoaded: true) + { + X = MatchmakingSelectPanel.HEIGHT - BeatmapCard.CORNER_RADIUS, + Width = BeatmapCard.WIDTH - MatchmakingSelectPanel.HEIGHT + BeatmapCard.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 = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)), + 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[] + { + 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 Container + { + Masking = true, + CornerRadius = BeatmapCard.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(4), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6, 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.9f), + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + } + }, + new ModFlowDisplay + { + AutoSizeAxes = Axes.Both, + Scale = new Vector2(0.5f), + Margin = new MarginPadding { Left = 5 }, + Current = { Value = mods } + }, + }, + } + }, + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 5, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = downloadState }, + Progress = { BindTarget = downloadProgress } + } + } + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + } + }; + + 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 } + }; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + downloadState.BindValueChanged(_ => updateState(), true); + + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + thumbnail.Dimmed.Value = showDetails; + + bool showProgress = downloadState.Value == DownloadState.Downloading || downloadState.Value == DownloadState.Importing; + + idleBottomContent.FadeTo(showProgress ? 0 : 1, 340, Easing.OutQuint); + downloadProgressBar.FadeTo(showProgress ? 1 : 0, 340, Easing.OutQuint); + } + + public MenuItem[] ContextMenuItems + { + get + { + List items = new List + { + new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID)) + }; + + 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/MatchmakingSelectPanel.CardContentRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs new file mode 100644 index 0000000000..24422de1b5 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanel + { + public partial class CardContentRandom : CardContent + { + public override AvatarOverlay SelectionOverlay => selectionOverlay; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private AvatarOverlay selectionOverlay = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = + [ + new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(32), + Icon = FontAwesome.Solid.Random, + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Random", + } + ] + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + } + }; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs similarity index 56% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs index cbd8480da4..ca10133a36 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -19,9 +20,12 @@ using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class BeatmapSelectPanel : Container + public abstract partial class MatchmakingSelectPanel : Container { - public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT); + public const float WIDTH = 345; + public const float HEIGHT = 80; + + public static readonly Vector2 SIZE = new Vector2(WIDTH, HEIGHT); public bool AllowSelection { get; set; } @@ -29,14 +33,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public Action? Action { private get; init; } + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + private const float border_width = 3; private Container scaleContainer = null!; private Drawable lighting = null!; private Container border = null!; - private BeatmapCardMatchmaking card = null!; - public BeatmapSelectPanel(MultiplayerPlaylistItem item) + protected MatchmakingSelectPanel(MultiplayerPlaylistItem item) { Item = item; Size = SIZE; @@ -45,88 +50,70 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - InternalChild = scaleContainer = new Container + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] + scaleContainer = new Container { - new Container + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] { - Masking = true, - CornerRadius = BeatmapCard.CORNER_RADIUS, - CornerExponent = 10, - RelativeSizeAxes = Axes.Both, - Children = new[] + new Container { - card = new BeatmapCardMatchmaking + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerExponent = 10, + RelativeSizeAxes = Axes.Both, + Children = new[] { - Action = () => + Content, + lighting = new Box { - if (AllowSelection) - Action?.Invoke(Item); + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Alpha = 0, }, - }, - lighting = new Box - { - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - } - }, - border = new Container - { - Alpha = 0, - Masking = true, - CornerRadius = BeatmapCard.CORNER_RADIUS, - CornerExponent = 10, - Blending = BlendingParameters.Additive, - 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[] + border = new Container { - new Box + Alpha = 0, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerExponent = 10, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + BorderThickness = border_width, + BorderColour = colourProvider.Light1, + EdgeEffect = new EdgeEffectParameters { - AlwaysPresent = true, - Alpha = 0, - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, + 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, + }, + } + }, + } + }, + new HoverClickSounds(), }; - - if (Item.ID == -1) - card.DisplayRandom(); - else - card.DisplayItem(Item); } - public void AddUser(APIUser user) - { - card.AddUser(user); - } + // TODO: making these abstract for now but avatar overlay should really be owned by the top level class + public abstract void AddUser(APIUser user); - public void RemoveUser(APIUser user) - { - card.RemoveUser(user); - } - - public void DisplayItem(MultiplayerPlaylistItem item) - { - card.DisplayItem(item); - } + public abstract void RemoveUser(APIUser user); protected override bool OnHover(HoverEvent e) { @@ -171,10 +158,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect lighting.FadeTo(0.5f, 50) .Then() .FadeTo(0.1f, 400); + + Action?.Invoke(Item); } - // pass through to let the beatmap card handle actual click. - return false; + return true; } public void ShowChosenBorder() diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs new file mode 100644 index 0000000000..ec00ed3847 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.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 osu.Framework.Allocation; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanelBeatmap : MatchmakingSelectPanel + { + private readonly APIBeatmap beatmap; + private readonly Mod[] mods; + + public MatchmakingSelectPanelBeatmap(MatchmakingPlaylistItem item) + : base(item.PlaylistItem) + { + beatmap = item.Beatmap; + mods = item.Mods; + } + + private CardContent content = null!; + + [BackgroundDependencyLoader] + private void load() + { + Add(content = new CardContentBeatmap(beatmap, mods)); + } + + public override void AddUser(APIUser user) + { + content.SelectionOverlay.AddUser(user); + } + + public override void RemoveUser(APIUser user) + { + content.SelectionOverlay.RemoveUser(user.Id); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs new file mode 100644 index 0000000000..0c818df06b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs @@ -0,0 +1,60 @@ +// 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.Shapes; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanelRandom : MatchmakingSelectPanel + { + public MatchmakingSelectPanelRandom(MultiplayerPlaylistItem item) + : base(item) + { + } + + private CardContent content = null!; + private readonly List users = new List(); + + [BackgroundDependencyLoader] + private void load() + { + Add(content = new CardContentRandom()); + } + + public void RevealBeatmap(APIBeatmap beatmap, Mod[] mods) + { + content.Expire(); + + var flashLayer = new Box { RelativeSizeAxes = Axes.Both }; + + AddRange(new Drawable[] + { + content = new CardContentBeatmap(beatmap, mods), + flashLayer, + }); + + foreach (var user in users) + content.SelectionOverlay.AddUser(user); + + flashLayer.FadeOutFromOne(1000, Easing.In); + } + + public override void AddUser(APIUser user) + { + users.Add(user); + content.SelectionOverlay.AddUser(user); + } + + public override void RemoveUser(APIUser user) + { + users.Remove(user); + content.SelectionOverlay.RemoveUser(user.Id); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index e0db69783c..7951fc5448 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -1,14 +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.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +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.Rulesets.Mods; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { @@ -18,10 +27,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public override Drawable PlayersDisplayArea { get; } private readonly BeatmapSelectGrid beatmapSelectGrid; + private readonly LoadingSpinner loadingSpinner; [Resolved] private MultiplayerClient client { get; set; } = null!; + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + public SubScreenBeatmapSelect() { InternalChildren = new Drawable[] @@ -30,9 +46,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 200 }, - Child = beatmapSelectGrid = new BeatmapSelectGrid + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, + beatmapSelectGrid = new BeatmapSelectGrid + { + RelativeSizeAxes = Axes.Both, + }, + loadingSpinner = new LoadingSpinner + { + Size = new Vector2(64), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { Value = Visibility.Visible } + } }, }, new Container @@ -50,25 +76,53 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { base.LoadComplete(); - client.ItemAdded += onItemAdded; - - foreach (var item in client.Room!.Playlist) - onItemAdded(item); - beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); - client.MatchmakingItemSelected += onItemSelected; client.MatchmakingItemDeselected += onItemDeselected; client.SettingsChanged += onSettingsChanged; + + Debug.Assert(client.Room != null); + + loadItems(client.Room.Playlist.ToArray()).FireAndForget(); } - private void onItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => + private async Task loadItems(MultiplayerPlaylistItem[] items) { - if (item.Expired) - return; + var beatmaps = await beatmapLookupCache.GetBeatmapsAsync(items.Select(it => it.BeatmapID).ToArray()).ConfigureAwait(false); + var matchmakingItems = new List(); - beatmapSelectGrid.AddItem(item); - }); + foreach (var entry in items.Zip(beatmaps)) + { + var (item, beatmap) = entry; + + beatmap ??= new APIBeatmap + { + BeatmapSet = new APIBeatmapSet + { + Title = "unknown beatmap", + TitleUnicode = "unknown beatmap", + Artist = "unknown artist", + ArtistUnicode = "unknown artist", + } + }; + + beatmap.StarRating = item.StarRating; + + Ruleset? ruleset = rulesetStore.GetRuleset(item.RulesetID)?.CreateInstance(); + + Debug.Assert(ruleset != null); + + Mod[] mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); + + matchmakingItems.Add(new MatchmakingPlaylistItem(item, beatmap, mods)); + } + + Scheduler.Add(() => + { + loadingSpinner.Hide(); + beatmapSelectGrid.AddItems(matchmakingItems); + }); + } private void onItemSelected(int userId, long itemId) { @@ -104,7 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect if (client.IsNotNull()) { - client.ItemAdded -= onItemAdded; client.MatchmakingItemSelected -= onItemSelected; client.MatchmakingItemDeselected -= onItemDeselected; client.SettingsChanged -= onSettingsChanged; From e349a597bae4618cc474505348d00f74c3325bd6 Mon Sep 17 00:00:00 2001 From: marvin Date: Mon, 17 Nov 2025 07:29:27 +0100 Subject: [PATCH 145/308] Use dice icon for MatchmakingSelectPanelRandom --- ...atchmakingSelectPanel.CardContentRandom.cs | 79 ++++++++++--------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs index 24422de1b5..156d213806 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs @@ -3,9 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; @@ -22,6 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private OverlayColourProvider colourProvider { get; set; } = null!; private AvatarOverlay selectionOverlay = null!; + public SpriteIcon Dice { get; private set; } = null!; [BackgroundDependencyLoader] private void load() @@ -31,50 +33,49 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background2, + Colour = colourProvider.Dark5, }, - new Container + new TrianglesV2 { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = - [ - new SpriteIcon - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(32), - Icon = FontAwesome.Solid.Random, - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Random", - } - ] - }, - selectionOverlay = new AvatarOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - } - } + Alpha = 0.1f, + }, + new OsuSpriteText + { + Y = 20, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Random" + }, + Dice = new SpriteIcon + { + Y = -10, + Size = new Vector2(28), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = randomDiceIcon(), + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, } }; + + Dice.Spin(10_000, RotationDirection.Clockwise); } + + private static IconUsage[] diceIcons => new[] + { + FontAwesome.Solid.DiceOne, + FontAwesome.Solid.DiceTwo, + FontAwesome.Solid.DiceThree, + FontAwesome.Solid.DiceFour, + FontAwesome.Solid.DiceFive, + FontAwesome.Solid.DiceSix, + }; + + private static IconUsage randomDiceIcon() => diceIcons[RNG.Next(diceIcons.Length)]; } } } From 32900f563c0b2b0ff5c8efc394e6f1c116a3a862 Mon Sep 17 00:00:00 2001 From: marvin Date: Mon, 17 Nov 2025 07:30:15 +0100 Subject: [PATCH 146/308] Roll dice on click --- .../MatchmakingSelectPanel.CardContentRandom.cs | 13 +++++++++++++ .../BeatmapSelect/MatchmakingSelectPanelRandom.cs | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs index 156d213806..ad7293b811 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs @@ -65,6 +65,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Dice.Spin(10_000, RotationDirection.Clockwise); } + public void RollDice() + { + var icon = randomDiceIcon(); + + while (icon.Equals(Dice.Icon)) + icon = randomDiceIcon(); + + Dice.ScaleTo(0.65f, 60, Easing.Out) + .Then() + .Schedule(() => Dice.Icon = icon) + .ScaleTo(1f, 400, Easing.OutElasticHalf); + } + private static IconUsage[] diceIcons => new[] { FontAwesome.Solid.DiceOne, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs index 0c818df06b..4c50f4184c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; @@ -56,5 +57,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect users.Remove(user); content.SelectionOverlay.RemoveUser(user.Id); } + + protected override bool OnClick(ClickEvent e) + { + if (AllowSelection && content is CardContentRandom randomContent) + randomContent.RollDice(); + + return base.OnClick(e); + } } } From 77963946851427d23a4c21a5d6281f5d0e6ecc33 Mon Sep 17 00:00:00 2001 From: marvin Date: Mon, 17 Nov 2025 07:31:46 +0100 Subject: [PATCH 147/308] Play roll animation when revealing random beatmap --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 10 ++-- ...atchmakingSelectPanel.CardContentRandom.cs | 3 +- .../MatchmakingSelectPanelRandom.cs | 54 +++++++++++++++---- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 0e5e5b8aae..f7efbf0648 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -228,12 +228,12 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(-1, duration: 0); - Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(-1), 500); + Scheduler.AddDelayed(() => + { + grid.PresentUnanimouslyChosenBeatmap(-1); + grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem); + }, 500); }); - - AddWaitStep("wait for animation", 5); - - AddStep("reveal beatmap", () => grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem)); } private (long[] candidateItems, long finalItem) pickRandomItems(int count) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs index ad7293b811..0fc5a9fa46 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private AvatarOverlay selectionOverlay = null!; public SpriteIcon Dice { get; private set; } = null!; + public OsuSpriteText Label { get; private set; } = null!; [BackgroundDependencyLoader] private void load() @@ -40,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect RelativeSizeAxes = Axes.Both, Alpha = 0.1f, }, - new OsuSpriteText + Label = new OsuSpriteText { Y = 20, Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs index 4c50f4184c..1c2114ad91 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Transforms; using osu.Framework.Input.Events; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; @@ -19,31 +21,50 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { } - private CardContent content = null!; + private CardContentRandom content = null!; + private Drawable diceProxy = null!; private readonly List users = new List(); [BackgroundDependencyLoader] private void load() { Add(content = new CardContentRandom()); + + AddInternal(diceProxy = content.Dice.CreateProxy()); } public void RevealBeatmap(APIBeatmap beatmap, Mod[] mods) { - content.Expire(); + const double duration = 800; - var flashLayer = new Box { RelativeSizeAxes = Axes.Both }; + content.Dice.MoveToY(-200, duration * 0.55, new PowEasingFunction(2.75, easeOut: true)) + .Then() + .Schedule(() => ChangeInternalChildDepth(diceProxy, float.MaxValue)) + .MoveToY(-DrawHeight / 2, duration * 0.45, new PowEasingFunction(2.2)) + .Then() + .FadeOut() + .Expire(); - AddRange(new Drawable[] + content.Dice.RotateTo(content.Dice.Rotation - 360 * 5, duration * 1.3f, Easing.Out); + content.Label.FadeOut(200).Expire(); + + Scheduler.AddDelayed(() => { - content = new CardContentBeatmap(beatmap, mods), - flashLayer, - }); + content.Expire(); - foreach (var user in users) - content.SelectionOverlay.AddUser(user); + var flashLayer = new Box { RelativeSizeAxes = Axes.Both }; - flashLayer.FadeOutFromOne(1000, Easing.In); + AddRange(new Drawable[] + { + new CardContentBeatmap(beatmap, mods), + flashLayer, + }); + + foreach (var user in users) + content.SelectionOverlay.AddUser(user); + + flashLayer.FadeOutFromOne(1000, Easing.In); + }, duration); } public override void AddUser(APIUser user) @@ -65,5 +86,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect return base.OnClick(e); } + + private readonly struct PowEasingFunction(double exponent, bool easeOut = false) : IEasingFunction + { + public double ApplyEasing(double time) + { + if (easeOut) + time = 1 - time; + + double value = Math.Pow(time, exponent); + + return easeOut ? 1 - value : value; + } + } } } From e541e917a471744d332548bf9484cc435490ed24 Mon Sep 17 00:00:00 2001 From: marvin Date: Mon, 17 Nov 2025 07:32:27 +0100 Subject: [PATCH 148/308] Change order of tests --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index f7efbf0648..daf9db85be 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -179,6 +179,23 @@ namespace osu.Game.Tests.Visual.Matchmaking }); } + [Test] + public void TestPresentRandomItem() + { + AddStep("present random item panel", () => + { + grid.TransferCandidatePanelsToRollContainer(pickRandomItems(4).candidateItems.Append(-1).ToArray(), duration: 0); + grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); + grid.PlayRollAnimation(-1, duration: 0); + + Scheduler.AddDelayed(() => + { + grid.PresentUnanimouslyChosenBeatmap(-1); + grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem); + }, 500); + }); + } + [TestCase(1)] [TestCase(2)] [TestCase(3)] @@ -219,23 +236,6 @@ namespace osu.Game.Tests.Visual.Matchmaking }); } - [Test] - public void TestPresentRandomItem() - { - AddStep("present random item panel", () => - { - grid.TransferCandidatePanelsToRollContainer(pickRandomItems(4).candidateItems.Append(-1).ToArray(), duration: 0); - grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); - grid.PlayRollAnimation(-1, duration: 0); - - Scheduler.AddDelayed(() => - { - grid.PresentUnanimouslyChosenBeatmap(-1); - grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem); - }, 500); - }); - } - private (long[] candidateItems, long finalItem) pickRandomItems(int count) { long[] candidateItems = items.Select(it => it.ID).ToArray(); From 424ef9237faffa7808292a5e4dbe1396af9edb31 Mon Sep 17 00:00:00 2001 From: marvin Date: Fri, 14 Nov 2025 23:11:50 +0100 Subject: [PATCH 149/308] Move result animation & sample implementation into selection panels --- .../TestSceneBeatmapSelectPanel.cs | 2 +- .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 14 ++++---------- .../BeatmapSelect/MatchmakingSelectPanel.cs | 2 ++ .../MatchmakingSelectPanelBeatmap.cs | 18 +++++++++++++++++- .../MatchmakingSelectPanelRandom.cs | 19 +++++++++++++++---- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 9ac64288ed..d01a0cf2f8 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddToggleStep("allow selection", value => panel!.AllowSelection = value); - AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), [])); + // AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), [])); } [Test] diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index d27b0e3818..11b9d1bfec 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -46,8 +46,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private readonly Sample?[] spinSamples = new Sample?[5]; private static readonly int[] spin_sample_sequence = [0, 1, 2, 3, 4, 2, 3, 4]; - private Sample? randomRevealSample; - private Sample? resultSample; private Sample? swooshSample; private double? lastSamplePlayback; @@ -81,8 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect for (int i = 0; i < spinSamples.Length; i++) spinSamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Selection/roulette-{i}"); - randomRevealSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/random-reveal"); - resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result"); swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); } @@ -150,8 +146,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Debug.Assert(playlistItem != null); - randomRevealSample?.Play(); - randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods); + // TODO: make this happen via panel.PresentAsChosenBeatmap + // randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods); }); public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) => whenPanelsLoaded(() => @@ -344,11 +340,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { rollContainer.ChangeChildDepth(panel, float.MinValue); - panel.ShowChosenBorder(); - panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) - .ScaleTo(1.5f, 1000, Easing.OutExpo); + var item = playlistItems[finalItem]; - resultSample?.Play(); + panel.PresentAsChosenBeatmap(item); }); } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs index ca10133a36..66d8b42492 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs @@ -182,6 +182,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect border.FadeOut(500, Easing.OutQuint); } + public abstract void PresentAsChosenBeatmap(MatchmakingPlaylistItem playlistItem); + public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) { scaleContainer diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs index ec00ed3847..0f70c1b2ed 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs @@ -2,8 +2,12 @@ // 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.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { @@ -20,13 +24,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } private CardContent content = null!; + private Sample? resultSample; [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { + resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result"); + Add(content = new CardContentBeatmap(beatmap, mods)); } + public override void PresentAsChosenBeatmap(MatchmakingPlaylistItem playlistItem) + { + ShowChosenBorder(); + this.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) + .ScaleTo(1.5f, 1000, Easing.OutExpo); + + resultSample?.Play(); + } + public override void AddUser(APIUser user) { content.SelectionOverlay.AddUser(user); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs index 1c2114ad91..26b0e6610f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs @@ -4,13 +4,14 @@ using System; 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.Shapes; using osu.Framework.Graphics.Transforms; using osu.Framework.Input.Events; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { @@ -25,15 +26,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private Drawable diceProxy = null!; private readonly List users = new List(); + private Sample? resultSample; + private Sample? swooshSample; + [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { + resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result"); + swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); + Add(content = new CardContentRandom()); AddInternal(diceProxy = content.Dice.CreateProxy()); } - public void RevealBeatmap(APIBeatmap beatmap, Mod[] mods) + public override void PresentAsChosenBeatmap(MatchmakingPlaylistItem playlistItem) { const double duration = 800; @@ -48,6 +55,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect content.Dice.RotateTo(content.Dice.Rotation - 360 * 5, duration * 1.3f, Easing.Out); content.Label.FadeOut(200).Expire(); + swooshSample?.Play(); + Scheduler.AddDelayed(() => { content.Expire(); @@ -56,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect AddRange(new Drawable[] { - new CardContentBeatmap(beatmap, mods), + new CardContentBeatmap(playlistItem.Beatmap, playlistItem.Mods), flashLayer, }); @@ -64,6 +73,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect content.SelectionOverlay.AddUser(user); flashLayer.FadeOutFromOne(1000, Easing.In); + + resultSample?.Play(); }, duration); } From 1e05613859562cf0211665758ef8aa4058f34c50 Mon Sep 17 00:00:00 2001 From: marvin Date: Fri, 14 Nov 2025 23:23:44 +0100 Subject: [PATCH 150/308] Combine random card reveal & panel roll animation into the same event --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 13 ++++++----- .../TestSceneBeatmapSelectPanel.cs | 2 +- .../Visual/Matchmaking/TestScenePickScreen.cs | 3 ++- .../Matchmaking/MatchmakingRoomState.cs | 9 ++++++++ .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 22 +++++++++++++------ .../MatchmakingSelectPanelRandom.cs | 4 ++++ .../BeatmapSelect/SubScreenBeatmapSelect.cs | 3 ++- .../Match/ScreenMatchmaking.ScreenStack.cs | 2 +- 8 files changed, 42 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index daf9db85be..8ed595dcb9 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; @@ -131,7 +132,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { var (candidateItems, finalItem) = pickRandomItems(5); - grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); + grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, MatchmakingRoomState.CandidateType.UserSelection); }); } @@ -160,7 +161,7 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(finalItem, duration: 0); - Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem), 500); + Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem, MatchmakingRoomState.CandidateType.UserSelection), 500); }); } @@ -175,7 +176,7 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(finalItem, duration: 0); - Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem), 500); + Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem, MatchmakingRoomState.CandidateType.UserSelection), 500); }); } @@ -184,13 +185,15 @@ namespace osu.Game.Tests.Visual.Matchmaking { AddStep("present random item panel", () => { - grid.TransferCandidatePanelsToRollContainer(pickRandomItems(4).candidateItems.Append(-1).ToArray(), duration: 0); + var (candidateItems, finalItem) = pickRandomItems(4); + + grid.TransferCandidatePanelsToRollContainer(candidateItems.Append(-1).ToArray(), duration: 0); grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(-1, duration: 0); Scheduler.AddDelayed(() => { - grid.PresentUnanimouslyChosenBeatmap(-1); + grid.PresentRolledBeatmap(finalItem, MatchmakingRoomState.CandidateType.Random); grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem); }, 500); }); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index d01a0cf2f8..6eebb27ef0 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddToggleStep("allow selection", value => panel!.AllowSelection = value); - // AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), [])); + AddStep("reveal beatmap", () => panel!.PresentAsChosenBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), []))); } [Test] diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index e894616f9e..e63370a01e 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Screens; using osu.Framework.Utils; 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.Match.BeatmapSelect; using osu.Game.Tests.Visual.Multiplayer; @@ -104,7 +105,7 @@ namespace osu.Game.Tests.Visual.Matchmaking long[] candidateItems = selectedItems.ToArray(); long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)]; - screen.RollFinalBeatmap(candidateItems, finalItem); + screen.RollFinalBeatmap(candidateItems, finalItem, MatchmakingRoomState.CandidateType.UserSelection); }); } } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs index b55fa63844..9bbc8f6f15 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs @@ -45,6 +45,9 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking [Key(4)] public MatchmakingUserList Users { get; set; } = new MatchmakingUserList(); + [Key(5)] + public CandidateType CandidateItemType { get; set; } + /// /// Advances to the next round. /// @@ -97,5 +100,11 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking foreach (var user in Users.Order(new MatchmakingUserComparer(CurrentRound))) user.Placement = i++; } + + public enum CandidateType + { + UserSelection, + Random, + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 11b9d1bfec..687e5314df 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osuTK; @@ -150,7 +151,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect // randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods); }); - public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) => whenPanelsLoaded(() => + public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId, MatchmakingRoomState.CandidateType candidateType) => whenPanelsLoaded(() => { Debug.Assert(candidateItemIds.Length >= 1); Debug.Assert(candidateItemIds.Contains(finalItemId)); @@ -166,7 +167,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect this.Delay(ARRANGE_DELAY) .Schedule(() => ArrangeItemsForRollAnimation()) .Delay(arrange_duration + present_beatmap_delay) - .Schedule(() => PresentUnanimouslyChosenBeatmap(finalItemId)); + .Schedule(() => PresentUnanimouslyChosenBeatmap(finalItemId, candidateType)); } else { @@ -175,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect .Delay(arrange_duration) .Schedule(() => PlayRollAnimation(finalItemId, roll_duration)) .Delay(roll_duration + present_beatmap_delay) - .Schedule(() => PresentRolledBeatmap(finalItemId)); + .Schedule(() => PresentRolledBeatmap(finalItemId, candidateType)); } }); @@ -322,13 +323,20 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentRolledBeatmap(long finalItem) + internal void PresentRolledBeatmap(long finalItem, MatchmakingRoomState.CandidateType candidateType) { + long itemToReveal = candidateType switch + { + MatchmakingRoomState.CandidateType.UserSelection => finalItem, + MatchmakingRoomState.CandidateType.Random => -1, + _ => throw new ArgumentOutOfRangeException(nameof(candidateType), candidateType, null) + }; + Debug.Assert(rollContainer.Children.Any(it => it.Item.ID == finalItem)); foreach (var panel in rollContainer.Children) { - if (panel.Item.ID != finalItem) + if (panel.Item.ID != itemToReveal) { panel.FadeOut(200); panel.PopOutAndExpire(easing: Easing.InQuad); @@ -347,11 +355,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentUnanimouslyChosenBeatmap(long finalItem) + internal void PresentUnanimouslyChosenBeatmap(long finalItem, MatchmakingRoomState.CandidateType candidateType) { // TODO: display special animation in this case - PresentRolledBeatmap(finalItem); + PresentRolledBeatmap(finalItem, candidateType); } private readonly TaskCompletionSource panelsLoaded = new TaskCompletionSource(); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs index 26b0e6610f..6acc1e3d8e 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Transforms; using osu.Framework.Input.Events; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { @@ -44,6 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { const double duration = 800; + this.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) + .ScaleTo(1.5f, 1000, Easing.OutExpo); + content.Dice.MoveToY(-200, duration * 0.55, new PowEasingFunction(2.75, easeOut: true)) .Then() .Schedule(() => ChangeInternalChildDepth(diceProxy, float.MaxValue)) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index 7951fc5448..358033d1c8 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -150,7 +150,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect beatmapSelectGrid.RevealRandomItem(client.Room!.CurrentPlaylistItem); } - public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); + public void RollFinalBeatmap(long[] candidateItems, long finalItem, MatchmakingRoomState.CandidateType panelType) => + beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, panelType); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs index 279dd98a5e..e144344994 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match case MatchmakingStage.ServerBeatmapFinalised: Debug.Assert(screenStack.CurrentScreen is SubScreenBeatmapSelect); - ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem); + ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem, matchmakingState.CandidateItemType); break; case MatchmakingStage.ResultsDisplaying: From 1ca4c8860baa16cfc8c5f888789331c9de81c839 Mon Sep 17 00:00:00 2001 From: marvin Date: Fri, 14 Nov 2025 23:42:45 +0100 Subject: [PATCH 151/308] Add slight wiggle when random card reveals beatmap --- .../Match/BeatmapSelect/MatchmakingSelectPanel.cs | 12 ++++++------ .../BeatmapSelect/MatchmakingSelectPanelRandom.cs | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs index 66d8b42492..23d5f8cfb0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private const float border_width = 3; - private Container scaleContainer = null!; + protected Container ScaleContainer = null!; private Drawable lighting = null!; private Container border = null!; @@ -52,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { InternalChildren = new Drawable[] { - scaleContainer = new Container + ScaleContainer = new Container { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -138,7 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect protected override bool OnMouseDown(MouseDownEvent e) { if (AllowSelection && e.Button == MouseButton.Left) - scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); + ScaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); return base.OnMouseDown(e); } @@ -148,7 +148,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect base.OnMouseUp(e); if (e.Button == MouseButton.Left) - scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); + ScaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); } protected override bool OnClick(ClickEvent e) @@ -186,7 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) { - scaleContainer + ScaleContainer .FadeOut() .MoveToY(distance) .Delay(delay) @@ -198,7 +198,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { AllowSelection = false; - scaleContainer.Delay(delay) + ScaleContainer.Delay(delay) .ScaleTo(0, duration, easing) .FadeOut(duration); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs index 6acc1e3d8e..c789c3dc74 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs @@ -78,6 +78,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect flashLayer.FadeOutFromOne(1000, Easing.In); + ScaleContainer.ScaleTo(0.92f, 120, Easing.Out) + .Then() + .ScaleTo(1f, 600, Easing.OutElasticHalf); + resultSample?.Play(); }, duration); } From fe56ba292174f3b5389c6b0fd1ee5e10c2eacf46 Mon Sep 17 00:00:00 2001 From: marvin Date: Mon, 17 Nov 2025 07:46:05 +0100 Subject: [PATCH 152/308] Turn MatchmakingCandidateType into top level declaration --- .../Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs | 8 ++++---- .../Visual/Matchmaking/TestScenePickScreen.cs | 2 +- .../Matchmaking/MatchmakingCandidateType.cs | 11 +++++++++++ .../MatchTypes/Matchmaking/MatchmakingRoomState.cs | 8 +------- .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 10 +++++----- .../Match/BeatmapSelect/SubScreenBeatmapSelect.cs | 2 +- .../Match/ScreenMatchmaking.ScreenStack.cs | 2 +- 7 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingCandidateType.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 8ed595dcb9..c2c4b6a797 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { var (candidateItems, finalItem) = pickRandomItems(5); - grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, MatchmakingRoomState.CandidateType.UserSelection); + grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, MatchmakingCandidateType.UserSelection); }); } @@ -161,7 +161,7 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(finalItem, duration: 0); - Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem, MatchmakingRoomState.CandidateType.UserSelection), 500); + Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem, MatchmakingCandidateType.UserSelection), 500); }); } @@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(finalItem, duration: 0); - Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem, MatchmakingRoomState.CandidateType.UserSelection), 500); + Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem, MatchmakingCandidateType.UserSelection), 500); }); } @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Matchmaking Scheduler.AddDelayed(() => { - grid.PresentRolledBeatmap(finalItem, MatchmakingRoomState.CandidateType.Random); + grid.PresentRolledBeatmap(finalItem, MatchmakingCandidateType.Random); grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem); }, 500); }); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index e63370a01e..1b7cc90132 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Matchmaking long[] candidateItems = selectedItems.ToArray(); long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)]; - screen.RollFinalBeatmap(candidateItems, finalItem, MatchmakingRoomState.CandidateType.UserSelection); + screen.RollFinalBeatmap(candidateItems, finalItem, MatchmakingCandidateType.UserSelection); }); } } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingCandidateType.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingCandidateType.cs new file mode 100644 index 0000000000..91b436088b --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingCandidateType.cs @@ -0,0 +1,11 @@ +// 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.Multiplayer.MatchTypes.Matchmaking +{ + public enum MatchmakingCandidateType + { + UserSelection, + Random, + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs index 9bbc8f6f15..44774c350d 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking public MatchmakingUserList Users { get; set; } = new MatchmakingUserList(); [Key(5)] - public CandidateType CandidateItemType { get; set; } + public MatchmakingCandidateType CandidateType { get; set; } /// /// Advances to the next round. @@ -100,11 +100,5 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking foreach (var user in Users.Order(new MatchmakingUserComparer(CurrentRound))) user.Placement = i++; } - - public enum CandidateType - { - UserSelection, - Random, - } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 687e5314df..5da2546745 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -151,7 +151,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect // randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods); }); - public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId, MatchmakingRoomState.CandidateType candidateType) => whenPanelsLoaded(() => + public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId, MatchmakingCandidateType candidateType) => whenPanelsLoaded(() => { Debug.Assert(candidateItemIds.Length >= 1); Debug.Assert(candidateItemIds.Contains(finalItemId)); @@ -323,12 +323,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentRolledBeatmap(long finalItem, MatchmakingRoomState.CandidateType candidateType) + internal void PresentRolledBeatmap(long finalItem, MatchmakingCandidateType candidateType) { long itemToReveal = candidateType switch { - MatchmakingRoomState.CandidateType.UserSelection => finalItem, - MatchmakingRoomState.CandidateType.Random => -1, + MatchmakingCandidateType.UserSelection => finalItem, + MatchmakingCandidateType.Random => -1, _ => throw new ArgumentOutOfRangeException(nameof(candidateType), candidateType, null) }; @@ -355,7 +355,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentUnanimouslyChosenBeatmap(long finalItem, MatchmakingRoomState.CandidateType candidateType) + internal void PresentUnanimouslyChosenBeatmap(long finalItem, MatchmakingCandidateType candidateType) { // TODO: display special animation in this case diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index 358033d1c8..c9cfa74e3e 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -150,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect beatmapSelectGrid.RevealRandomItem(client.Room!.CurrentPlaylistItem); } - public void RollFinalBeatmap(long[] candidateItems, long finalItem, MatchmakingRoomState.CandidateType panelType) => + public void RollFinalBeatmap(long[] candidateItems, long finalItem, MatchmakingCandidateType panelType) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, panelType); protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs index e144344994..ad517b0de5 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match case MatchmakingStage.ServerBeatmapFinalised: Debug.Assert(screenStack.CurrentScreen is SubScreenBeatmapSelect); - ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem, matchmakingState.CandidateItemType); + ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem, matchmakingState.CandidateType); break; case MatchmakingStage.ResultsDisplaying: From 76c0bd475051e453c9a0be8be233769a9c6258c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Nov 2025 11:55:28 +0100 Subject: [PATCH 153/308] Add pooling support to smoke segments - Closes https://github.com/ppy/osu/issues/35703 - Supersedes / closes https://github.com/ppy/osu/pull/35711 Can test using something dumb like diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 11b3b5c71d..e21d8389ef 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -540,6 +541,10 @@ protected override void ParseConfigurationStream(Stream stream) case "Menu/fountain-star": componentName = "star2"; break; + + case "cursor-smoke": + Thread.Sleep(500); + break; } Texture? texture = null; --- .../Skinning/SmokeSegment.cs | 9 ++++++-- osu.Game.Rulesets.Osu/UI/SmokeContainer.cs | 21 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs index f4fe42b8de..2962bce635 100644 --- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs +++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs @@ -77,9 +77,14 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); RelativeSizeAxes = Axes.Both; + } - LifetimeStart = smokeStartTime = Time.Current; - + public void StartDrawing(double time) + { + LifetimeStart = smokeStartTime = time; + LifetimeEnd = smokeEndTime = double.MaxValue; + SmokePoints.Clear(); + lastPosition = null; totalDistance = pointInterval; } diff --git a/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs b/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs index 389440ba2d..ff28444e82 100644 --- a/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs @@ -1,9 +1,9 @@ // 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; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -19,17 +19,24 @@ namespace osu.Game.Rulesets.Osu.UI /// public partial class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler { + private DrawablePool segmentPool = null!; private SmokeSkinnableDrawable? currentSegmentSkinnable; private Vector2 lastMousePosition; public override bool ReceivePositionalInputAt(Vector2 _) => true; + [BackgroundDependencyLoader] + private void load() + { + AddInternal(segmentPool = new DrawablePool(10)); + } + public bool OnPressed(KeyBindingPressEvent e) { if (e.Action == OsuAction.Smoke) { - AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment())); + AddInternal(currentSegmentSkinnable = segmentPool.Get(segment => segment.Segment?.StartDrawing(Time.Current))); // Add initial position immediately. addPosition(); @@ -59,17 +66,19 @@ namespace osu.Game.Rulesets.Osu.UI return base.OnMouseMove(e); } - private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current); + private void addPosition() => currentSegmentSkinnable?.Segment?.AddPosition(lastMousePosition, Time.Current); private partial class SmokeSkinnableDrawable : SkinnableDrawable { + public SmokeSegment? Segment => Drawable as SmokeSegment; + public override bool RemoveWhenNotAlive => true; public override double LifetimeStart => Drawable.LifetimeStart; public override double LifetimeEnd => Drawable.LifetimeEnd; - public SmokeSkinnableDrawable(ISkinComponentLookup lookup, Func? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling) - : base(lookup, defaultImplementation, confineMode) + public SmokeSkinnableDrawable() + : base(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment()) { } } From 214122f633f5c61260964ef076e15e25250a28bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Nov 2025 12:01:27 +0100 Subject: [PATCH 154/308] Fix bad localisation reuse in pause overlay (#35717) Closes https://github.com/ppy/osu-resources/issues/393. Matches break overlay: https://github.com/ppy/osu/blob/5dc44fbdf9dfaff573f596b0085a954c6f420e07/osu.Game/Screens/Play/Break/BreakInfo.cs#L48 --- osu.Game/Screens/Play/GameplayMenuOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 7d946dc678..d4c40c78ae 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -22,6 +22,7 @@ using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; using osu.Game.Utils; namespace osu.Game.Screens.Play @@ -236,7 +237,7 @@ namespace osu.Game.Screens.Play if (gameplayState != null) { playInfoText.NewLine(); - playInfoText.AddText(SongSelectStrings.Accuracy); + playInfoText.AddText(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy); playInfoText.AddText(": "); playInfoText.AddText(gameplayState!.ScoreProcessor.Accuracy.Value.FormatAccuracy(), cp => cp.Font = cp.Font.With(weight: FontWeight.Bold)); } From 7b952b83bf5f56f03aa0ee993c64a9bc446aae89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Nov 2025 13:55:53 +0100 Subject: [PATCH 155/308] Fix test --- osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs index d5d3cbb146..0e7d94cb9f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Input.Events; @@ -10,6 +11,7 @@ using osu.Framework.Input.States; using osu.Framework.Logging; using osu.Framework.Testing.Input; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Tests @@ -58,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests foreach (var smokeContainer in smokeContainers) { - if (smokeContainer.Children.Count != 0) + if (smokeContainer.Children.OfType().Any()) return false; } From ae5584bd88d35ff5a5e4be0708e577eadcb03838 Mon Sep 17 00:00:00 2001 From: Kawaritai <72053972+Kawaritai@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:10:18 +1100 Subject: [PATCH 156/308] Center window within usable bounds --- .../Settings/Sections/Graphics/LayoutSettings.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 99d47aab1f..36a273f412 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -255,8 +255,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics window.WindowState = Framework.Platform.WindowState.Normal; } - windowedPositionX.Value = 0.5; - windowedPositionY.Value = 0.5; + var dBounds = currentDisplay.Value.Bounds; + var dUsable = currentDisplay.Value.UsableBounds; + int w = size.NewValue.Width; + int h = size.NewValue.Height; + + float adjustedY = Math.Max( + dUsable.Y + (dUsable.Height - h) / 2f, + dUsable.Y + (host.Window?.BorderSize.Value.Top ?? 0) // titlebar adjustment + ); + windowedPositionY.Value = dBounds.Height - h != 0 ? (adjustedY - dBounds.Y) / (dBounds.Height - h) : 0; + windowedPositionX.Value = dBounds.Width - w != 0 ? (dUsable.X - dBounds.X + (dUsable.Width - w) / 2f) / (dBounds.Width - w) : 0; }); sizeWindowed.BindValueChanged(size => From 0c341c1f3e36921186b0d3fac03a20df2bf96312 Mon Sep 17 00:00:00 2001 From: Kawaritai <72053972+Kawaritai@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:38:34 +1100 Subject: [PATCH 157/308] Clamp sizing --- .../Settings/Sections/Graphics/LayoutSettings.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 36a273f412..e1b1b7ccce 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -248,8 +248,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (size.NewValue == sizeWindowed.Value || windowModeDropdown.Current.Value != WindowMode.Windowed) return; - sizeWindowed.Value = size.NewValue; - if (window?.WindowState == Framework.Platform.WindowState.Maximised) { window.WindowState = Framework.Platform.WindowState.Normal; @@ -257,12 +255,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics var dBounds = currentDisplay.Value.Bounds; var dUsable = currentDisplay.Value.UsableBounds; - int w = size.NewValue.Width; - int h = size.NewValue.Height; + float topBar = host.Window?.BorderSize.Value.Top ?? 0; + + int w = Math.Min(size.NewValue.Width, dUsable.Width); + int h = (int)Math.Min(size.NewValue.Height, dUsable.Height - topBar); + + windowedResolution.Value = new Size(w, h); + sizeWindowed.Value = windowedResolution.Value; float adjustedY = Math.Max( dUsable.Y + (dUsable.Height - h) / 2f, - dUsable.Y + (host.Window?.BorderSize.Value.Top ?? 0) // titlebar adjustment + dUsable.Y + topBar // titlebar adjustment ); windowedPositionY.Value = dBounds.Height - h != 0 ? (adjustedY - dBounds.Y) / (dBounds.Height - h) : 0; windowedPositionX.Value = dBounds.Width - w != 0 ? (dUsable.X - dBounds.X + (dUsable.Width - w) / 2f) / (dBounds.Width - w) : 0; From 19b6761697b214230a485a5c37f261903e2c7b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Nov 2025 09:39:06 +0100 Subject: [PATCH 158/308] Clarify target branch requirements in `CONTRIBUTING.md` Because it appears to be a point of confusion to new contributors (https://github.com/ppy/osu/pull/35725#issuecomment-3545734262). --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebe1e08074..1d9861baf7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,9 @@ Aside from the above, below is a brief checklist of things to watch out when you After you're done with your changes and you wish to open the PR, please observe the following recommendations: - Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary. +- Please pick the following target branch for your pull request: + - `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets, + - `master`, otherwise. - Please avoid pushing untested or incomplete code. - Please do not force-push or rebase unless we ask you to. - Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge. From fbd83cb0482b9f73c8bfe20c52cbb28b21a5d2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Nov 2025 09:50:42 +0100 Subject: [PATCH 159/308] 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 6f0543935b..2df686d354 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 adab5435ea..74dae877f1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From f0ca079fe6f0e0f2006bc44e1357475db93f28a0 Mon Sep 17 00:00:00 2001 From: Urantij Date: Tue, 18 Nov 2025 15:52:37 +0700 Subject: [PATCH 160/308] Fix cursor incorrectly flashing red after a rewind in replays with Alternate mod active (#35725) * Fix red cursor with alt mod when rewind * Change rewind detection in input blocking --- osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs index b56fdbdf74..34eb2be077 100644 --- a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs +++ b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs @@ -67,6 +67,9 @@ namespace osu.Game.Rulesets.Osu.Mods { if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) LastAcceptedAction = null; + + if (LastAcceptedAction != null && gameplayClock.IsRewinding) + LastAcceptedAction = null; } protected abstract bool CheckValidNewAction(OsuAction action); From 89f2c7160d91ed991fea7db7de6b6ca425521969 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Nov 2025 18:45:38 +0900 Subject: [PATCH 161/308] 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 6f0543935b..2df686d354 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 adab5435ea..74dae877f1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 9c2319b98992c5698c0b530b6a6612c00481f27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Nov 2025 14:40:47 +0100 Subject: [PATCH 162/308] Use existing bindables instead of refetching --- osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index a355205a85..314e1a1f4c 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -170,10 +170,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input float gameplayWidth = host.Window.ClientSize.Width; float gameplayHeight = host.Window.ClientSize.Height; - if (osuConfig.Get(OsuSetting.Scaling) == ScalingMode.Everything) + if (scalingMode.Value == ScalingMode.Everything) { - gameplayWidth *= osuConfig.Get(OsuSetting.ScalingSizeX); - gameplayHeight *= osuConfig.Get(OsuSetting.ScalingSizeY); + gameplayWidth *= scalingSizeX.Value; + gameplayHeight *= scalingSizeY.Value; } forceAspectRatio(gameplayWidth / gameplayHeight); From 80fbcd5fbdbd46c9fa26fa0b6528f7679439b58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Nov 2025 14:41:10 +0100 Subject: [PATCH 163/308] Move application of scaling to tablet output area to scaling container It's the safest place for it to be there, really. --- .../Graphics/Containers/ScalingContainer.cs | 15 ++++++++++- .../Settings/Sections/Input/TabletSettings.cs | 25 ------------------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 9d2a1c16af..22dabc55ce 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; +using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Layout; using osu.Framework.Platform; using osu.Framework.Screens; @@ -50,6 +52,8 @@ namespace osu.Game.Graphics.Containers private RectangleF? customRect; private bool customRectIsRelativePosition; + private ITabletHandler? tabletHandler; + /// /// Set a custom position and scale which overrides any user specification. /// @@ -123,7 +127,7 @@ namespace osu.Game.Graphics.Containers } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, ISafeArea safeArea) + private void load(GameHost host, OsuConfigManager config, ISafeArea safeArea) { scalingMode = config.GetBindable(OsuSetting.Scaling); scalingMode.ValueChanged += _ => Scheduler.AddOnce(updateSize); @@ -148,6 +152,8 @@ namespace osu.Game.Graphics.Containers scalingMenuBackgroundDim = config.GetBindable(OsuSetting.ScalingBackgroundDim); scalingMenuBackgroundDim.ValueChanged += _ => Scheduler.AddOnce(updateSize); + + tabletHandler = host.AvailableInputHandlers.OfType().SingleOrDefault(); } protected override void LoadComplete() @@ -222,6 +228,13 @@ namespace osu.Game.Graphics.Containers // An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything". sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, TRANSITION_DURATION, requiresMasking ? Easing.OutQuart : Easing.None) .OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); + + // when "everything" scaling mode is active, tablets are expected to constrain output area to the scaled size of the game + if (tabletHandler != null) + { + tabletHandler.OutputAreaSize.Value = scalingMode.Value == ScalingMode.Everything ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; + tabletHandler.OutputAreaOffset.Value = scalingMode.Value == ScalingMode.Everything ? new Vector2(posX.Value, posY.Value) : new Vector2(0.5f); + } } private partial class ScalingBackgroundScreen : BackgroundScreenDefault diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 314e1a1f4c..9ef2fc0933 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -52,8 +52,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input private Bindable scalingMode = null!; private Bindable scalingSizeX = null!; private Bindable scalingSizeY = null!; - private Bindable scalingPositionX = new Bindable(); - private Bindable scalingPositionY = new Bindable(); [Resolved] private GameHost host { get; set; } @@ -91,8 +89,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input scalingMode = osuConfig.GetBindable(OsuSetting.Scaling); scalingSizeX = osuConfig.GetBindable(OsuSetting.ScalingSizeX); scalingSizeY = osuConfig.GetBindable(OsuSetting.ScalingSizeY); - scalingPositionX = osuConfig.GetBindable(OsuSetting.ScalingPositionX); - scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY); Children = new Drawable[] { @@ -299,13 +295,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue)); }); - updateScaling(); - scalingMode.BindValueChanged(_ => updateScaling()); - scalingSizeX.BindValueChanged(_ => updateScaling()); - scalingSizeY.BindValueChanged(_ => updateScaling()); - scalingPositionX.BindValueChanged(_ => updateScaling()); - scalingPositionY.BindValueChanged(_ => updateScaling()); - pressureThreshold.BindTo(tabletHandler.PressureThreshold); tablet.BindTo(tabletHandler.Tablet); @@ -393,20 +382,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input aspectLock.Value = true; } - private void updateScaling() - { - if (scalingMode.Value == ScalingMode.Everything) - { - outputAreaSize.Value = new Vector2(scalingSizeX.Value, scalingSizeY.Value); - outputAreaOffset.Value = new Vector2(scalingPositionX.Value, scalingPositionY.Value); - } - else - { - outputAreaSize.Value = new Vector2(1, 1); - outputAreaOffset.Value = new Vector2(0.5f, 0.5f); - } - } - private void updateAspectRatio() => aspectRatio.Value = currentAspectRatio; private float currentAspectRatio => sizeX.Value / sizeY.Value; From 277f4268db271250a43cdb46bcdd45ae7bb98b9d Mon Sep 17 00:00:00 2001 From: marvin Date: Tue, 18 Nov 2025 19:33:59 +0100 Subject: [PATCH 164/308] Remove `BeatmapSelectGrid.RevealRandomItem` method --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 2 -- .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 10 ---------- .../BeatmapSelect/SubScreenBeatmapSelect.cs | 16 ---------------- 3 files changed, 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index c2c4b6a797..f2448a5e26 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -194,7 +193,6 @@ namespace osu.Game.Tests.Visual.Matchmaking Scheduler.AddDelayed(() => { grid.PresentRolledBeatmap(finalItem, MatchmakingCandidateType.Random); - grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem); }, 500); }); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 5da2546745..f5796fe760 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -141,16 +141,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect panel.RemoveUser(user); }); - public void RevealRandomItem(MultiplayerPlaylistItem item) => whenPanelsLoaded(() => - { - playlistItems.TryGetValue(item.ID, out var playlistItem); - - Debug.Assert(playlistItem != null); - - // TODO: make this happen via panel.PresentAsChosenBeatmap - // randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods); - }); - public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId, MatchmakingCandidateType candidateType) => whenPanelsLoaded(() => { Debug.Assert(candidateItemIds.Length >= 1); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index c9cfa74e3e..5f6886d23a 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -79,7 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); client.MatchmakingItemSelected += onItemSelected; client.MatchmakingItemDeselected += onItemDeselected; - client.SettingsChanged += onSettingsChanged; Debug.Assert(client.Room != null); @@ -136,20 +135,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect beatmapSelectGrid.SetUserSelection(user, itemId, false); } - private void onSettingsChanged(MultiplayerRoomSettings settings) - { - if (client.Room!.MatchState is not MatchmakingRoomState matchmakingState) - return; - - if (matchmakingState.Stage != MatchmakingStage.ServerBeatmapFinalised) - return; - - if (matchmakingState.CandidateItem != -1) - return; - - beatmapSelectGrid.RevealRandomItem(client.Room!.CurrentPlaylistItem); - } - public void RollFinalBeatmap(long[] candidateItems, long finalItem, MatchmakingCandidateType panelType) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, panelType); @@ -161,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { client.MatchmakingItemSelected -= onItemSelected; client.MatchmakingItemDeselected -= onItemDeselected; - client.SettingsChanged -= onSettingsChanged; } } } From 6f7f9802bd33676a8398a313bbaaa4237e02fec8 Mon Sep 17 00:00:00 2001 From: Kawaritai <72053972+Kawaritai@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:18:07 +1100 Subject: [PATCH 165/308] Change windowed resolutions filtering. Add comment about borders logic. --- .../Sections/Graphics/LayoutSettings.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index e1b1b7ccce..cdc4f328c3 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -228,15 +228,18 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics var buffer = new Bindable(windowedResolution.Value); resolutionWindowedDropdown.Current = buffer; - var newResolutions = display.NewValue.DisplayModes - .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) - .OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width)) - .Select(m => m.Size) - .Distinct() - .ToList(); + var fullscreenResolutions = display.NewValue.DisplayModes + .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) + .OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width)) + .Select(m => m.Size) + .Distinct() + .ToList(); + var windowedResolutions = fullscreenResolutions + .Where(res => res.Width <= display.NewValue.UsableBounds.Width && res.Height <= display.NewValue.UsableBounds.Height) + .ToList(); - resolutionsFullscreen.ReplaceRange(1, resolutionsFullscreen.Count - 1, newResolutions); - resolutionsWindowed.ReplaceRange(0, resolutionsWindowed.Count, newResolutions); + resolutionsFullscreen.ReplaceRange(1, resolutionsFullscreen.Count - 1, fullscreenResolutions); + resolutionsWindowed.ReplaceRange(0, resolutionsWindowed.Count, windowedResolutions); resolutionWindowedDropdown.Current = windowedResolution; @@ -253,6 +256,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics window.WindowState = Framework.Platform.WindowState.Normal; } + // Adjust only for top decorations (assuming system titlebar). + // Bottom/left/right borders are ignored as invisible padding, which don't align with the screen. var dBounds = currentDisplay.Value.Bounds; var dUsable = currentDisplay.Value.UsableBounds; float topBar = host.Window?.BorderSize.Value.Top ?? 0; From ef4408a73e5c5c5851bdc6073f4ca09a626032f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Nov 2025 08:29:55 +0100 Subject: [PATCH 166/308] Fix song select crashing when selecting random beatmap and changing star rating filter simultaneously (#35730) Closes https://github.com/ppy/osu/issues/35728. --- 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 58874e79d1..ae1c8eb878 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -1035,13 +1035,13 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomBeatmap() { - ICollection visibleBeatmaps = ExpandedGroup != null + ICollection visibleBeatmaps = ExpandedGroup != null && grouping.GroupItems.TryGetValue(ExpandedGroup, out var groupItems) // 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() + ? groupItems.Select(i => i.Model).OfType().ToArray() : GetCarouselItems()!.Select(i => i.Model).OfType().ToArray(); GroupedBeatmap beatmap; From 603c77e3e902cd15224a0f0993b963d99000aaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Nov 2025 11:23:58 +0100 Subject: [PATCH 167/308] Avoid nuking logged in user's joined channels on showing match chat in tournament client Closes https://github.com/ppy/osu/issues/35721. I worry that straight up removing the nuke and not adding any channel leave calls in exchange is going to leave tourney client users with the *inverse* problem of being joined into a gorillion channels from multiplayer matches they broadcasted, so this attempts to strike a reasonable balance. --- .../Components/TournamentMatchChatDisplay.cs | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index c04dbdcdd6..761ecd4a46 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -41,30 +41,33 @@ namespace osu.Game.Tournament.Components chatChannel.BindTo(ipc.ChatChannel); chatChannel.BindValueChanged(c => { - if (string.IsNullOrWhiteSpace(c.NewValue)) - return; - - int id = int.Parse(c.NewValue); - - if (id <= 0) return; - if (manager == null) { AddInternal(manager = new ChannelManager(api)); Channel.BindTo(manager.CurrentChannel); } - foreach (var ch in manager.JoinedChannels.ToList()) - manager.LeaveChannel(ch); - - var channel = new Channel + if (int.TryParse(c.OldValue, out int oldChannelId) && oldChannelId > 0) { - Id = id, - Type = ChannelType.Public - }; + var joinedChannel = manager.JoinedChannels.SingleOrDefault(ch => ch.Id == oldChannelId); + if (joinedChannel != null) + manager.LeaveChannel(joinedChannel); + } - manager.JoinChannel(channel); - manager.CurrentChannel.Value = channel; + if (string.IsNullOrWhiteSpace(c.NewValue)) + return; + + if (int.TryParse(c.NewValue, out int newChannelId) && newChannelId > 0) + { + var channel = new Channel + { + Id = newChannelId, + Type = ChannelType.Public + }; + + manager.JoinChannel(channel); + manager.CurrentChannel.Value = channel; + } }, true); } } From 02090bf6c43b921c33f7f4f3f6b1bfd76a4f2c68 Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 19 Nov 2025 13:15:53 +0100 Subject: [PATCH 168/308] Resolve candidateItem in RollAndDisplayFinalBeatmap instead of PresentRolledBeatmap --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 6 ++-- .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 31 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index f2448a5e26..4ca014bf2a 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(finalItem, duration: 0); - Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem, MatchmakingCandidateType.UserSelection), 500); + Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem, finalItem, MatchmakingCandidateType.UserSelection), 500); }); } @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(finalItem, duration: 0); - Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem, MatchmakingCandidateType.UserSelection), 500); + Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem, finalItem, MatchmakingCandidateType.UserSelection), 500); }); } @@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.Matchmaking Scheduler.AddDelayed(() => { - grid.PresentRolledBeatmap(finalItem, MatchmakingCandidateType.Random); + grid.PresentRolledBeatmap(-1, finalItem, MatchmakingCandidateType.Random); }, 500); }); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index f5796fe760..137f7712e0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -143,8 +143,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId, MatchmakingCandidateType candidateType) => whenPanelsLoaded(() => { + long candidateItemId = candidateType switch + { + MatchmakingCandidateType.UserSelection => finalItemId, + MatchmakingCandidateType.Random => -1, + _ => throw new ArgumentOutOfRangeException(nameof(candidateType), candidateType, null) + }; + Debug.Assert(candidateItemIds.Length >= 1); - Debug.Assert(candidateItemIds.Contains(finalItemId)); + Debug.Assert(candidateItemIds.Contains(candidateItemId)); Debug.Assert(panelLookup.ContainsKey(finalItemId)); Debug.Assert(candidateItemIds.All(id => panelLookup.ContainsKey(id))); @@ -157,7 +164,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect this.Delay(ARRANGE_DELAY) .Schedule(() => ArrangeItemsForRollAnimation()) .Delay(arrange_duration + present_beatmap_delay) - .Schedule(() => PresentUnanimouslyChosenBeatmap(finalItemId, candidateType)); + .Schedule(() => PresentUnanimouslyChosenBeatmap(candidateItemId, finalItemId, candidateType)); } else { @@ -166,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect .Delay(arrange_duration) .Schedule(() => PlayRollAnimation(finalItemId, roll_duration)) .Delay(roll_duration + present_beatmap_delay) - .Schedule(() => PresentRolledBeatmap(finalItemId, candidateType)); + .Schedule(() => PresentRolledBeatmap(candidateItemId, finalItemId, candidateType)); } }); @@ -313,20 +320,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentRolledBeatmap(long finalItem, MatchmakingCandidateType candidateType) + internal void PresentRolledBeatmap(long candidateItem, long finalItem, MatchmakingCandidateType candidateType) { - long itemToReveal = candidateType switch - { - MatchmakingCandidateType.UserSelection => finalItem, - MatchmakingCandidateType.Random => -1, - _ => throw new ArgumentOutOfRangeException(nameof(candidateType), candidateType, null) - }; - - Debug.Assert(rollContainer.Children.Any(it => it.Item.ID == finalItem)); + Debug.Assert(rollContainer.Children.Any(it => it.Item.ID == candidateItem)); + Debug.Assert(playlistItems.ContainsKey(finalItem)); foreach (var panel in rollContainer.Children) { - if (panel.Item.ID != itemToReveal) + if (panel.Item.ID != candidateItem) { panel.FadeOut(200); panel.PopOutAndExpire(easing: Easing.InQuad); @@ -345,11 +346,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentUnanimouslyChosenBeatmap(long finalItem, MatchmakingCandidateType candidateType) + internal void PresentUnanimouslyChosenBeatmap(long candidateItem, long finalItem, MatchmakingCandidateType candidateType) { // TODO: display special animation in this case - PresentRolledBeatmap(finalItem, candidateType); + PresentRolledBeatmap(candidateItem, finalItem, candidateType); } private readonly TaskCompletionSource panelsLoaded = new TaskCompletionSource(); From 4b59a4657f8a1cd5ebee6d284a9e873c4db23522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Nov 2025 13:22:07 +0100 Subject: [PATCH 169/308] Use new sliders-with-text-input in editor toolboxes Addresses https://github.com/ppy/osu/discussions/35732. And yes, I renamed "perfect curve threshold" to "bias" so that the text can fit. Sue me. --- .../Edit/FreehandSliderToolboxGroup.cs | 19 ++++----- .../Edit/OsuGridToolboxGroup.cs | 8 ++-- .../TestSceneExpandingContainer.cs | 9 ++-- .../Graphics/Containers/ExpandingContainer.cs | 40 +++++++++--------- .../UserInterface/ExpandableSlider.cs | 42 +++++-------------- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 38 +++++++++++++---- .../Edit/ComposerDistanceSnapProvider.cs | 7 ++-- 7 files changed, 81 insertions(+), 82 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs index f17118ba34..43bc4420f3 100644 --- a/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs @@ -68,15 +68,18 @@ namespace osu.Game.Rulesets.Osu.Edit { toleranceSlider = new ExpandableSlider { - Current = displayTolerance + Current = displayTolerance, + ExpandedLabelText = "Control point spacing", }, cornerThresholdSlider = new ExpandableSlider { - Current = displayCornerThreshold + Current = displayCornerThreshold, + ExpandedLabelText = "Corner bias", }, circleThresholdSlider = new ExpandableSlider { - Current = displayCircleThreshold + Current = displayCircleThreshold, + ExpandedLabelText = "Perfect curve bias" } }; } @@ -88,24 +91,18 @@ namespace osu.Game.Rulesets.Osu.Edit displayTolerance.BindValueChanged(tolerance => { toleranceSlider.ContractedLabelText = $"C. P. S.: {tolerance.NewValue:N0}"; - toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {tolerance.NewValue:N0}"; - Tolerance.Value = displayToInternalTolerance(tolerance.NewValue); }, true); displayCornerThreshold.BindValueChanged(threshold => { - cornerThresholdSlider.ContractedLabelText = $"C. T.: {threshold.NewValue:N0}"; - cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {threshold.NewValue:N0}"; - + cornerThresholdSlider.ContractedLabelText = $"C. B.: {threshold.NewValue:N0}"; CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue); }, true); displayCircleThreshold.BindValueChanged(threshold => { - circleThresholdSlider.ContractedLabelText = $"P. C. T.: {threshold.NewValue:N0}"; - circleThresholdSlider.ExpandedLabelText = $"Perfect Curve Threshold: {threshold.NewValue:N0}"; - + circleThresholdSlider.ContractedLabelText = $"P. C. B.: {threshold.NewValue:N0}"; CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue); }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 991d42c7b4..5bd5b54f39 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -127,21 +127,25 @@ namespace osu.Game.Rulesets.Osu.Edit { Current = StartPositionX, KeyboardStep = 1, + ExpandedLabelText = "X offset", }, startPositionYSlider = new ExpandableSlider { Current = StartPositionY, KeyboardStep = 1, + ExpandedLabelText = "Y offset", }, spacingSlider = new ExpandableSlider { Current = Spacing, KeyboardStep = 1, + ExpandedLabelText = "Spacing", }, gridLinesRotationSlider = new ExpandableSlider { Current = GridLinesRotation, KeyboardStep = 1, + ExpandedLabelText = "Rotation", }, new FillFlowContainer { @@ -182,14 +186,12 @@ namespace osu.Game.Rulesets.Osu.Edit StartPositionX.BindValueChanged(x => { startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}"; - startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:#,0.##}"; StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y); }, true); StartPositionY.BindValueChanged(y => { startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}"; - startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:#,0.##}"; StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue); }, true); @@ -202,7 +204,6 @@ namespace osu.Game.Rulesets.Osu.Edit Spacing.BindValueChanged(spacing => { spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}"; - spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}"; SpacingVector.Value = new Vector2(spacing.NewValue); editorBeatmap.GridSize = (int)spacing.NewValue; }, true); @@ -210,7 +211,6 @@ namespace osu.Game.Rulesets.Osu.Edit GridLinesRotation.BindValueChanged(rotation => { gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}"; - gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}"; }, true); GridType.BindValueChanged(v => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index 3f4f86e424..44998e6fa2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Overlays.Settings.Sections; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -19,7 +18,7 @@ namespace osu.Game.Tests.Visual.UserInterface private TestExpandingContainer container; private SettingsToolboxGroup toolboxGroup; - private ExpandableSlider> slider1; + private ExpandableSlider slider1; private ExpandableSlider slider2; [SetUp] @@ -36,7 +35,7 @@ namespace osu.Game.Tests.Visual.UserInterface Width = 1, Children = new Drawable[] { - slider1 = new ExpandableSlider> + slider1 = new ExpandableSlider { Current = new BindableFloat { @@ -62,13 +61,13 @@ namespace osu.Game.Tests.Visual.UserInterface slider1.Current.BindValueChanged(v => { - slider1.ExpandedLabelText = $"Slider One ({v.NewValue:0.##x})"; + slider1.ExpandedLabelText = "Slider One"; slider1.ContractedLabelText = $"S. 1. ({v.NewValue:0.##x})"; }, true); slider2.Current.BindValueChanged(v => { - slider2.ExpandedLabelText = $"Slider Two ({v.NewValue:N2})"; + slider2.ExpandedLabelText = "Slider Two"; slider2.ContractedLabelText = $"S. 2. ({v.NewValue:N2})"; }, true); }); diff --git a/osu.Game/Graphics/Containers/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs index 65a00b725c..7cce49fb81 100644 --- a/osu.Game/Graphics/Containers/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -4,7 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; +using osu.Framework.Input; using osu.Framework.Threading; namespace osu.Game.Graphics.Containers @@ -58,6 +58,8 @@ namespace osu.Game.Graphics.Containers protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + private InputManager inputManager = null!; + private bool? lastMouseInBounds; private ScheduledDelegate? hoverExpandEvent; protected override void LoadComplete() @@ -68,37 +70,35 @@ namespace osu.Game.Graphics.Containers { this.ResizeWidthTo(v.NewValue ? expandedWidth : contractedWidth, TRANSITION_DURATION, Easing.OutQuint); }, true); + + inputManager = GetContainingInputManager()!; } - protected override bool OnHover(HoverEvent e) + protected override void Update() { - updateHoverExpansion(); - return true; + base.Update(); + + bool mouseInBounds = Contains(inputManager.CurrentState.Mouse.Position); + + if (lastMouseInBounds != mouseInBounds) + updateExpansionState(mouseInBounds); + + lastMouseInBounds = mouseInBounds; } - protected override void OnHoverLost(HoverLostEvent e) - { - if (hoverExpandEvent != null) - { - hoverExpandEvent?.Cancel(); - hoverExpandEvent = null; - - Expanded.Value = false; - return; - } - - base.OnHoverLost(e); - } - - private void updateHoverExpansion() + private void updateExpansionState(bool mouseInBounds) { if (!ExpandOnHover) return; hoverExpandEvent?.Cancel(); + hoverExpandEvent = null; - if (IsHovered && !Expanded.Value) + if (mouseInBounds && !Expanded.Value) hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + + if (!mouseInBounds && Expanded.Value) + Expanded.Value = false; } } } diff --git a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs index 4cc77e218f..addf4c9110 100644 --- a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; using Vector2 = osuTK.Vector2; namespace osu.Game.Graphics.UserInterface @@ -19,49 +20,27 @@ namespace osu.Game.Graphics.UserInterface /// public partial class ExpandableSlider : CompositeDrawable, IExpandable, IHasCurrentValue where T : struct, INumber, IMinMaxValue - where TSlider : RoundedSliderBar, new() + where TSlider : FormSliderBar, new() { - private readonly OsuSpriteText label; + private readonly OsuSpriteText contractedLabel; private readonly TSlider slider; - private LocalisableString contractedLabelText; - /// /// The label text to display when this slider is in a contracted state. /// public LocalisableString ContractedLabelText { - get => contractedLabelText; - set - { - if (value == contractedLabelText) - return; - - contractedLabelText = value; - - if (!Expanded.Value) - label.Text = value; - } + get => contractedLabel.Text; + set => contractedLabel.Text = value; } - private LocalisableString expandedLabelText; - /// /// The label text to display when this slider is in an expanded state. /// public LocalisableString ExpandedLabelText { - get => expandedLabelText; - set - { - if (value == expandedLabelText) - return; - - expandedLabelText = value; - - if (Expanded.Value) - label.Text = value; - } + get => slider.Caption; + set => slider.Caption = value; } public Bindable Current @@ -95,7 +74,7 @@ namespace osu.Game.Graphics.UserInterface Spacing = new Vector2(0f, 10f), Children = new Drawable[] { - label = new OsuSpriteText(), + contractedLabel = new OsuSpriteText(), slider = new TSlider { RelativeSizeAxes = Axes.X, @@ -118,7 +97,8 @@ namespace osu.Game.Graphics.UserInterface Expanded.BindValueChanged(v => { - label.Text = v.NewValue ? expandedLabelText : contractedLabelText; + contractedLabel.FadeTo(v.NewValue ? 0 : 1); + slider.FadeTo(v.NewValue ? Current.Disabled ? 0.3f : 1f : 0f, 500, Easing.OutQuint); slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); @@ -133,7 +113,7 @@ namespace osu.Game.Graphics.UserInterface /// /// An implementation for the UI slider bar control. /// - public partial class ExpandableSlider : ExpandableSlider> + public partial class ExpandableSlider : ExpandableSlider> where T : struct, INumber, IMinMaxValue { } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 1304c298fb..59217f64ab 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -58,26 +58,49 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } + private LocalisableString caption; + /// /// Caption describing this slider bar, displayed on top of the controls. /// - public LocalisableString Caption { get; init; } + public LocalisableString Caption + { + get => caption; + set + { + caption = value; + + if (IsLoaded) + captionText.Caption = value; + } + } /// /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. /// public LocalisableString HintText { get; init; } + private float keyboardStep; + /// /// A custom step value for each key press which actuates a change on this control. /// - public float KeyboardStep { get; init; } + public float KeyboardStep + { + get => keyboardStep; + set + { + keyboardStep = value; + if (IsLoaded) + slider.KeyboardStep = value; + } + } private Box background = null!; private Box flashLayer = null!; private FormTextBox.InnerTextBox textBox = null!; private InnerSlider slider = null!; - private FormFieldCaption caption = null!; + private FormFieldCaption captionText = null!; private IFocusManager focusManager = null!; [Resolved] @@ -117,11 +140,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, Children = new Drawable[] { - caption = new FormFieldCaption + captionText = new FormFieldCaption { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Caption = Caption, TooltipText = HintText, }, textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true) @@ -145,7 +167,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.X, Width = 0.5f, - KeyboardStep = KeyboardStep, Current = currentNumberInstantaneous, OnCommit = () => current.Value = currentNumberInstantaneous.Value, } @@ -161,6 +182,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 { base.LoadComplete(); + slider.KeyboardStep = keyboardStep; + captionText.Caption = caption; + focusManager = GetContainingFocusManager()!; textBox.Focused.BindValueChanged(_ => updateState()); @@ -270,7 +294,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 textBox.Alpha = 1; background.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background4 : colourProvider.Background5; - caption.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + captionText.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; BorderThickness = childHasFocus || IsHovered || slider.IsDragging.Value ? 2 : 0; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 2d6e09b3fd..d2f402a6fa 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -20,7 +20,6 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.OSD; -using osu.Game.Overlays.Settings.Sections; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; @@ -42,7 +41,7 @@ namespace osu.Game.Rulesets.Edit Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; - private ExpandableSlider> distanceSpacingSlider = null!; + private ExpandableSlider distanceSpacingSlider = null!; private ExpandableButton currentDistanceSpacingButton = null!; [Resolved] @@ -78,11 +77,12 @@ namespace osu.Game.Rulesets.Edit Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, Children = new Drawable[] { - distanceSpacingSlider = new ExpandableSlider> + distanceSpacingSlider = new ExpandableSlider { KeyboardStep = adjust_step, // Manual binding in LoadComplete to handle one-way event flow. Current = DistanceSpacingMultiplier.GetUnboundCopy(), + ExpandedLabelText = "Distance spacing", }, currentDistanceSpacingButton = new ExpandableButton { @@ -104,7 +104,6 @@ namespace osu.Game.Rulesets.Edit DistanceSpacingMultiplier.BindValueChanged(multiplier => { distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; - distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})"; if (multiplier.NewValue != multiplier.OldValue) onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); From 397041099e27551e2f782a5fd6adf6e216b45872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Nov 2025 13:38:31 +0100 Subject: [PATCH 170/308] Adjust element spacing in editor toolboxes --- .../Edit/Blueprints/GridPlacementBlueprint.cs | 8 ++++---- .../Edit/FreehandSliderToolboxGroup.cs | 10 ++++++++++ osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 14 ++++++++------ osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 ++-- osu.Game/Overlays/SettingsToolboxGroup.cs | 6 ++++++ .../Rulesets/Edit/ComposerDistanceSnapProvider.cs | 3 +++ 6 files changed, 33 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index f54dc2c85b..07856f11e0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints { this.gridToolboxGroup = gridToolboxGroup; originalOrigin = gridToolboxGroup.StartPosition.Value; - originalSpacing = gridToolboxGroup.Spacing.Value; + originalSpacing = gridToolboxGroup.GridLineSpacing.Value; originalRotation = gridToolboxGroup.GridLinesRotation.Value; } @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints { // Reset the grid to the default values. gridToolboxGroup.StartPosition.Value = gridToolboxGroup.StartPosition.Default; - gridToolboxGroup.Spacing.Value = gridToolboxGroup.Spacing.Default; + gridToolboxGroup.GridLineSpacing.Value = gridToolboxGroup.GridLineSpacing.Default; if (!gridToolboxGroup.GridLinesRotation.Disabled) gridToolboxGroup.GridLinesRotation.Value = gridToolboxGroup.GridLinesRotation.Default; EndPlacement(true); @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints // Default to the original spacing and rotation if the distance is too small. if (Vector2.Distance(gridToolboxGroup.StartPosition.Value, pos) < 2) { - gridToolboxGroup.Spacing.Value = originalSpacing; + gridToolboxGroup.GridLineSpacing.Value = originalSpacing; if (!gridToolboxGroup.GridLinesRotation.Disabled) gridToolboxGroup.GridLinesRotation.Value = originalRotation; } @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints private void resetGridState() { gridToolboxGroup.StartPosition.Value = originalOrigin; - gridToolboxGroup.Spacing.Value = originalSpacing; + gridToolboxGroup.GridLineSpacing.Value = originalSpacing; if (!gridToolboxGroup.GridLinesRotation.Disabled) gridToolboxGroup.GridLinesRotation.Value = originalRotation; } diff --git a/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs index 43bc4420f3..7909044361 100644 --- a/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs @@ -5,8 +5,10 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit { @@ -61,6 +63,9 @@ namespace osu.Game.Rulesets.Osu.Edit private ExpandableSlider cornerThresholdSlider = null!; private ExpandableSlider circleThresholdSlider = null!; + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -116,6 +121,11 @@ namespace osu.Game.Rulesets.Osu.Edit displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue) ); + expandingContainer?.Expanded.BindValueChanged(v => + { + Spacing = v.NewValue ? new Vector2(5) : new Vector2(15); + }, true); + float displayToInternalTolerance(float v) => v / 50f; int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 5bd5b54f39..e0c486a688 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// The spacing between grid lines. /// - public BindableFloat Spacing { get; } = new BindableFloat(4f) + public BindableFloat GridLineSpacing { get; } = new BindableFloat(4f) { MinValue = 4f, MaxValue = 256f, @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit float dist = Vector2.Distance(point1, point2); while (dist >= max_automatic_spacing) dist /= 2; - Spacing.Value = dist; + GridLineSpacing.Value = dist; } [BackgroundDependencyLoader] @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Edit }, spacingSlider = new ExpandableSlider { - Current = Spacing, + Current = GridLineSpacing, KeyboardStep = 1, ExpandedLabelText = "Spacing", }, @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Edit }, }; - Spacing.Value = editorBeatmap.GridSize; + GridLineSpacing.Value = editorBeatmap.GridSize; } protected override void LoadComplete() @@ -201,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Edit StartPositionY.Value = pos.NewValue.Y; }); - Spacing.BindValueChanged(spacing => + GridLineSpacing.BindValueChanged(spacing => { spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}"; SpacingVector.Value = new Vector2(spacing.NewValue); @@ -239,6 +239,8 @@ namespace osu.Game.Rulesets.Osu.Edit { gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; + + Spacing = v.NewValue ? new Vector2(5) : new Vector2(15); }, true); } @@ -252,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Edit switch (e.Action) { case GlobalAction.EditorCycleGridSpacing: - Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2; + GridLineSpacing.Value = GridLineSpacing.Value * 2 >= max_automatic_spacing ? GridLineSpacing.Value / 8 : GridLineSpacing.Value * 2; return true; case GlobalAction.EditorCycleGridType: diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e4f8ee5b6d..0dac4cb2df 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Edit case PositionSnapGridType.Triangle: var triangularPositionSnapGrid = new TriangularPositionSnapGrid(); - triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); + triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.GridLineSpacing); triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); positionSnapGrid = triangularPositionSnapGrid; @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Edit case PositionSnapGridType.Circle: var circularPositionSnapGrid = new CircularPositionSnapGrid(); - circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); + circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.GridLineSpacing); positionSnapGrid = circularPositionSnapGrid; break; diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index d82118fa1a..f26ed962cb 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -46,6 +46,12 @@ namespace osu.Game.Overlays public BindableBool Expanded { get; } = new BindableBool(true); + public Vector2 Spacing + { + get => content.Spacing; + set => content.Spacing = value; + } + private OsuSpriteText headerText = null!; private Container headerContent = null!; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index d2f402a6fa..64f938ba6c 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.TernaryButtons; +using osuTK; namespace osu.Game.Rulesets.Edit { @@ -74,6 +75,7 @@ namespace osu.Game.Rulesets.Edit toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("snapping") { Name = "snapping", + Spacing = new Vector2(5), Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, Children = new Drawable[] { @@ -104,6 +106,7 @@ namespace osu.Game.Rulesets.Edit DistanceSpacingMultiplier.BindValueChanged(multiplier => { distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; + distanceSpacingSlider.Current.Value = multiplier.NewValue; if (multiplier.NewValue != multiplier.OldValue) onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); From be77257ddb4d819001e999be20962d99fed4ae88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Nov 2025 03:10:12 +0100 Subject: [PATCH 171/308] Do not overwrite website state of 'hide online presence' toggle (#35741) Closes https://github.com/ppy/osu/issues/35735. --- osu.Game/Online/API/LocalUserState.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/LocalUserState.cs b/osu.Game/Online/API/LocalUserState.cs index 1359d62ae7..94b298fdb4 100644 --- a/osu.Game/Online/API/LocalUserState.cs +++ b/osu.Game/Online/API/LocalUserState.cs @@ -62,6 +62,10 @@ namespace osu.Game.Online.API localUser.Value = me; configSupporter.Value = me.IsSupporter; + // `last_visit` is assumed to be `null` if and only if the web-side "hide online presence toggle" is enabled + if (me.LastVisit == null) + configStatus.Value = UserStatus.Offline; + UpdateFriends(); UpdateBlocks(); UpdateFavouriteBeatmapSets(); From 47faf774b04072f3ebd5d158005028960a3dfa1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Nov 2025 10:12:43 +0100 Subject: [PATCH 172/308] Fix tests --- .../TestSceneExpandingContainer.cs | 4 ++++ .../Graphics/Containers/ExpandingContainer.cs | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index 44998e6fa2..db949c6754 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -4,6 +4,7 @@ #nullable disable using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Containers; @@ -21,6 +22,9 @@ namespace osu.Game.Tests.Visual.UserInterface private ExpandableSlider slider1; private ExpandableSlider slider2; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game/Graphics/Containers/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs index 7cce49fb81..90322e92bc 100644 --- a/osu.Game/Graphics/Containers/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -59,7 +59,18 @@ namespace osu.Game.Graphics.Containers protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); private InputManager inputManager = null!; + + /// + /// Tracks whether the mouse was in bounds of this expanding container in the last frame. + /// private bool? lastMouseInBounds; + + /// + /// Tracks whether the last expansion of the container was caused by the mouse moving into its bounds + /// (as opposed to an external set of `Expanded`, in which case moving the mouse outside of its bounds should not contract). + /// + private bool? expandedByMouse; + private ScheduledDelegate? hoverExpandEvent; protected override void LoadComplete() @@ -95,10 +106,18 @@ namespace osu.Game.Graphics.Containers hoverExpandEvent = null; if (mouseInBounds && !Expanded.Value) + { hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + expandedByMouse = true; + } if (!mouseInBounds && Expanded.Value) - Expanded.Value = false; + { + if (expandedByMouse == true) + Expanded.Value = false; + + expandedByMouse = false; + } } } } From a8ac82aa1f3da33a9b8d9ac06617f5ecb4597ccb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Nov 2025 18:19:30 +0900 Subject: [PATCH 173/308] Fix test failure due to channel not being joined --- .../TestSceneTournamentMatchChatDisplay.cs | 25 +++++++++++++++++-- osu.Game/Properties/AssemblyInfo.cs | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index 231bd77655..4b1d56dea2 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -6,6 +6,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; @@ -61,14 +63,33 @@ namespace osu.Game.Tournament.Tests.Components Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - - chatDisplay.Channel.Value = testChannel; } protected override void LoadComplete() { base.LoadComplete(); + AddStep("set up API", () => + { + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case JoinChannelRequest joinChannelRequest: + joinChannelRequest.TriggerSuccess(); + return true; + + case LeaveChannelRequest leaveChannelRequest: + leaveChannelRequest.TriggerSuccess(); + return true; + + default: + return false; + } + }; + }); + AddStep("set channel", () => chatDisplay.Channel.Value = testChannel); + AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId()) { Sender = admin, diff --git a/osu.Game/Properties/AssemblyInfo.cs b/osu.Game/Properties/AssemblyInfo.cs index be430a0fe4..75e3ff8fd0 100644 --- a/osu.Game/Properties/AssemblyInfo.cs +++ b/osu.Game/Properties/AssemblyInfo.cs @@ -11,6 +11,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")] [assembly: InternalsVisibleTo("osu.Game.Tests.iOS")] [assembly: InternalsVisibleTo("osu.Game.Tests.Android")] +[assembly: InternalsVisibleTo("osu.Game.Tournament.Tests")] // intended for Moq usage [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] From c7e1a5770d4d95ee9c0fd0a01556e4dd3ff3beb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Nov 2025 18:22:16 +0900 Subject: [PATCH 174/308] Adjust code structure slightly to simplify logic --- .../Components/TournamentMatchChatDisplay.cs | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 761ecd4a46..02fb5a7ae0 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Components { public partial class TournamentMatchChatDisplay : StandAloneChatDisplay { - private readonly Bindable chatChannel = new Bindable(); + private readonly Bindable channelName = new Bindable(); private ChannelManager? manager; @@ -34,42 +34,33 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(MatchIPCInfo? ipc, IAPIProvider api) + private void load(MatchIPCInfo ipc, IAPIProvider api) { - if (ipc != null) + AddInternal(manager = new ChannelManager(api)); + Channel.BindTo(manager.CurrentChannel); + + channelName.BindTo(ipc.ChatChannel); + channelName.BindValueChanged(c => { - chatChannel.BindTo(ipc.ChatChannel); - chatChannel.BindValueChanged(c => + if (int.TryParse(c.OldValue, out int oldChannelId) && oldChannelId > 0) { - if (manager == null) + var joinedChannel = manager.JoinedChannels.SingleOrDefault(ch => ch.Id == oldChannelId); + if (joinedChannel != null) + manager.LeaveChannel(joinedChannel); + } + + if (int.TryParse(c.NewValue, out int newChannelId) && newChannelId > 0) + { + var channel = new Channel { - AddInternal(manager = new ChannelManager(api)); - Channel.BindTo(manager.CurrentChannel); - } + Id = newChannelId, + Type = ChannelType.Public + }; - if (int.TryParse(c.OldValue, out int oldChannelId) && oldChannelId > 0) - { - var joinedChannel = manager.JoinedChannels.SingleOrDefault(ch => ch.Id == oldChannelId); - if (joinedChannel != null) - manager.LeaveChannel(joinedChannel); - } - - if (string.IsNullOrWhiteSpace(c.NewValue)) - return; - - if (int.TryParse(c.NewValue, out int newChannelId) && newChannelId > 0) - { - var channel = new Channel - { - Id = newChannelId, - Type = ChannelType.Public - }; - - manager.JoinChannel(channel); - manager.CurrentChannel.Value = channel; - } - }, true); - } + manager.JoinChannel(channel); + manager.CurrentChannel.Value = channel; + } + }, true); } public void Expand() => this.FadeIn(300); From 094454499ca04ad4d6e1138615dc8a6773b04a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Nov 2025 11:51:28 +0100 Subject: [PATCH 175/308] Add `created` alias for `submitted` song select filter Symmetrical change to https://github.com/ppy/osu-web/pull/12561 (can probably wait until that one is reviewed to be legitimate). --- .../NonVisual/Filtering/FilterQueryParserTest.cs | 10 ++++++++++ osu.Game/Screens/Select/FilterQueryParser.cs | 1 + 2 files changed, 11 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 8bef6b04a7..87e439534b 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -738,6 +738,16 @@ namespace osu.Game.Tests.NonVisual.Filtering new object[] { "submitted=99999", false }, new object[] { "submitted>=2012-03-05-04", false }, new object[] { "submitted>=2012/03.05-04", false }, + + new object[] { "created<2012", true }, + new object[] { "created<2012.03", true }, + new object[] { "created<2012/03/05", true }, + new object[] { "created<2012-3-5", true }, + + new object[] { "created<0", false }, + new object[] { "created=99999", false }, + new object[] { "created>=2012-03-05-04", false }, + new object[] { "created>=2012/03.05-04", false }, }; [Test] diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 8cf3bda1c5..0adcf5d454 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -68,6 +68,7 @@ namespace osu.Game.Screens.Select case "ranked": return tryUpdateRankedDateRange(ref criteria.DateRanked, op, value); + case "created": case "submitted": return tryUpdateRankedDateRange(ref criteria.DateSubmitted, op, value); From aba567d258734e6ce058f2248ebcd16dfa066f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Thu, 20 Nov 2025 11:16:05 +0100 Subject: [PATCH 176/308] Add background screen --- .../Match/MatchmakingBackgroundScreen.cs | 56 +++++++++++++++++++ .../Matchmaking/Match/ScreenMatchmaking.cs | 2 + 2 files changed, 58 insertions(+) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs new file mode 100644 index 0000000000..cfb3f2e6c4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Overlays; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class MatchmakingBackgroundScreen : BackgroundScreen + { + public MatchmakingBackgroundScreen() + { + InternalChild = new Content + { + RelativeSizeAxes = Axes.Both + }; + } + + public partial class Content : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(TextureStore textures, OverlayColourProvider colourProvider) + { + AddRangeInternal(new Drawable[] + { + new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get("Backgrounds/bg1"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Colour4, + Alpha = 0.5f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.6f, + } + }); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 972f0b4adb..8a48a089b4 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -57,6 +57,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public override bool ShowFooter => true; + protected override BackgroundScreen CreateBackground() => new MatchmakingBackgroundScreen(); + [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker(); From 107c481fb96522e5d65cc41266bc13802ac2d1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Thu, 20 Nov 2025 12:03:42 +0100 Subject: [PATCH 177/308] Use new background in all matchmaking test scenes --- .../Matchmaking/MatchmakingTestScene.cs | 32 +++++++++++++++++++ .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 3 +- .../TestSceneBeatmapSelectPanel.cs | 3 +- .../TestSceneMatchmakingChatDisplay.cs | 2 +- .../Matchmaking/TestScenePanelRoomAward.cs | 3 +- .../Visual/Matchmaking/TestScenePickScreen.cs | 3 +- .../Matchmaking/TestScenePlayerPanel.cs | 3 +- .../TestScenePlayerPanelOverlay.cs | 3 +- .../Matchmaking/TestSceneResultsScreen.cs | 3 +- .../TestSceneRoundResultsScreen.cs | 3 +- .../Matchmaking/TestSceneStageDisplay.cs | 3 +- 11 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 osu.Game.Tests/Visual/Matchmaking/MatchmakingTestScene.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/MatchmakingTestScene.cs b/osu.Game.Tests/Visual/Matchmaking/MatchmakingTestScene.cs new file mode 100644 index 0000000000..8c01083b12 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/MatchmakingTestScene.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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public abstract partial class MatchmakingTestScene : MultiplayerTestScene + { + protected override Container Content { get; } + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + protected MatchmakingTestScene() + { + base.Content.AddRange(new Drawable[] + { + new MatchmakingBackgroundScreen.Content + { + RelativeSizeAxes = Axes.Both, + }, + Content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 0e5e5b8aae..cf98a785f1 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -17,12 +17,11 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; 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 TestSceneBeatmapSelectGrid : OnlinePlayTestScene + public partial class TestSceneBeatmapSelectGrid : MatchmakingTestScene { private MatchmakingPlaylistItem[] items = null!; diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 9ac64288ed..cd1d8297da 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -12,11 +12,10 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; -using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneBeatmapSelectPanel : MultiplayerTestScene + public partial class TestSceneBeatmapSelectPanel : MatchmakingTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs index d8e42cd946..b22c4dd74a 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneMatchmakingChatDisplay : ScreenTestScene + public partial class TestSceneMatchmakingChatDisplay : MatchmakingTestScene { private MatchmakingChatDisplay? chat; diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs index bdae656855..f03af8b8f5 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs @@ -3,11 +3,10 @@ using osu.Framework.Graphics; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; -using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestScenePanelRoomAward : MultiplayerTestScene + public partial class TestScenePanelRoomAward : MatchmakingTestScene { public override void SetUpSteps() { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index e894616f9e..6f281fd6b3 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -11,11 +11,10 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; -using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestScenePickScreen : MultiplayerTestScene + public partial class TestScenePickScreen : MatchmakingTestScene { private readonly IReadOnlyList users = new[] { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index 0c78038179..897d59657c 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -11,12 +11,11 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.Match; -using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestScenePlayerPanel : MultiplayerTestScene + public partial class TestScenePlayerPanel : MatchmakingTestScene { private PlayerPanel panel = null!; diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs index cdc0c93d11..f414169251 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs @@ -15,12 +15,11 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.Match; -using osu.Game.Tests.Visual.Multiplayer; using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestScenePlayerPanelOverlay : MultiplayerTestScene + public partial class TestScenePlayerPanelOverlay : MatchmakingTestScene { private PlayerPanelOverlay list = null!; diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs index 843c20b1e5..f717849a2a 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -11,12 +11,11 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; -using osu.Game.Tests.Visual.Multiplayer; using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneResultsScreen : MultiplayerTestScene + public partial class TestSceneResultsScreen : MatchmakingTestScene { public override void SetUpSteps() { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs index cbdbd33158..ff4e2c1932 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs @@ -14,12 +14,11 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults; -using osu.Game.Tests.Visual.Multiplayer; using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneRoundResultsScreen : MultiplayerTestScene + public partial class TestSceneRoundResultsScreen : MatchmakingTestScene { public override void SetUpSteps() { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs index dc4f09c555..a4aa4e2ceb 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs @@ -9,11 +9,10 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Matchmaking.Match; -using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneStageDisplay : MultiplayerTestScene + public partial class TestSceneStageDisplay : MatchmakingTestScene { [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); From 6052ed790d70a5c552e5360c68d90dd289129eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Nov 2025 12:23:31 +0100 Subject: [PATCH 178/308] Debounce continuous track seeks to at most one every 500ms See https://github.com/ppy/osu/pull/35677#issuecomment-3555903209. --- .../Graphics/UserInterface/ProgressBar.cs | 5 ++- osu.Game/Overlays/NowPlayingOverlay.cs | 20 +++++++++- .../Timelines/Summary/Parts/MarkerPart.cs | 39 ++++++++++++------- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ProgressBar.cs b/osu.Game/Graphics/UserInterface/ProgressBar.cs index dcf96f04c0..1169d4ca88 100644 --- a/osu.Game/Graphics/UserInterface/ProgressBar.cs +++ b/osu.Game/Graphics/UserInterface/ProgressBar.cs @@ -16,6 +16,7 @@ namespace osu.Game.Graphics.UserInterface public bool Seeking { get; private set; } public Action OnSeek; + public Action OnCommit; private readonly Box fill; private readonly Box background; @@ -80,12 +81,14 @@ namespace osu.Game.Graphics.UserInterface protected override void OnUserChange(double value) { Seeking = true; + OnSeek?.Invoke(value); + base.OnUserChange(value); } protected override bool Commit() { - OnSeek?.Invoke(CurrentNumber.Value); Seeking = false; + OnCommit?.Invoke(CurrentNumber.Value); return base.Commit(); } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 84c279476f..9f9f57336c 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -207,7 +207,8 @@ namespace osu.Game.Overlays Height = progress_height / 2, FillColour = colours.Yellow, BackgroundColour = colours.YellowDarker.Opacity(0.5f), - OnSeek = musicController.SeekTo + OnSeek = onSeek, + OnCommit = onCommit, } }, }, @@ -221,6 +222,23 @@ namespace osu.Game.Overlays }; } + private double? lastSeekTime; + + private void onSeek(double progress) + { + if (lastSeekTime == null || Time.Current - lastSeekTime > 500) + { + musicController.SeekTo(progress); + lastSeekTime = Time.Current; + } + } + + private void onCommit(double progress) + { + musicController.SeekTo(progress); + lastSeekTime = null; + } + private void togglePlaylist() { if (playlist == null) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index afe14de3ea..c9a44eab4a 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Framework.Threading; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; @@ -34,39 +33,53 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts }); } + private double? lastSeekTime; + protected override bool OnDragStart(DragStartEvent e) => true; protected override void OnDrag(DragEvent e) { - seekToPosition(e.ScreenSpaceMousePosition); + base.OnDrag(e); + seekToPosition(e.ScreenSpaceMousePosition, instant: false); + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + seekToPosition(e.ScreenSpaceMousePosition, instant: true); } protected override bool OnMouseDown(MouseDownEvent e) { - seekToPosition(e.ScreenSpaceMousePosition); + seekToPosition(e.ScreenSpaceMousePosition, instant: true); return true; } - private ScheduledDelegate? scheduledSeek; - /// /// Seeks the to the time closest to a position on the screen relative to the . /// /// The position in screen coordinates. - private void seekToPosition(Vector2 screenPosition) + /// Whether the seek should be instant (drag end, mouse button press) or debounced (drag in progress). + private void seekToPosition(Vector2 screenPosition, bool instant) { - scheduledSeek?.Cancel(); - scheduledSeek = Schedule(() => - { - float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); - editorClock.SeekSmoothlyTo(markerPos / DrawWidth * editorClock.TrackLength); - }); + float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); + double seekDestination = markerPos / DrawWidth * editorClock.TrackLength; + marker.X = (float)seekDestination; + + if (!instant && lastSeekTime != null && Time.Current - lastSeekTime < 500) + return; + + editorClock.SeekSmoothlyTo(seekDestination); + + lastSeekTime = instant ? null : Time.Current; } protected override void Update() { base.Update(); - marker.X = (float)editorClock.CurrentTime; + + if (!IsDragged) + marker.X = (float)editorClock.CurrentTime; } protected override void LoadBeatmap(EditorBeatmap beatmap) From f0f33b6df443ce34662f8da7c999081b939788ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Nov 2025 10:29:15 +0100 Subject: [PATCH 179/308] Adjust precisions to be less weird In a perfect world you could specify different precisions for the slider and the text box but let's start here and see if we get complaints first. --- .../Editor/TestSceneOsuEditorGrids.cs | 8 ++++---- osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs | 9 ++++++--- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 8 ++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index c6893a5bdf..b9258f0053 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -245,13 +245,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("grid spacing is distance to slider tail", () => { var composer = Editor.ChildrenOfType().Single(); - return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01) + return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1) && Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y); }); AddAssert("grid rotation points to slider tail", () => { var composer = Editor.ChildrenOfType().Single(); - return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01); + return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1); }); AddStep("start grid placement", () => InputManager.Key(Key.Number5)); @@ -280,9 +280,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("grid spacing and rotation unchanged", () => { var composer = Editor.ChildrenOfType().Single(); - return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01) + return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1) && Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y) - && Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01); + && Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1); }); } diff --git a/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs index 7909044361..6f8c58e1e4 100644 --- a/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs @@ -44,19 +44,22 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly BindableInt displayTolerance = new BindableInt(90) { MinValue = 5, - MaxValue = 100 + MaxValue = 100, + Precision = 1, }; private readonly BindableInt displayCornerThreshold = new BindableInt(40) { MinValue = 5, - MaxValue = 100 + MaxValue = 100, + Precision = 1, }; private readonly BindableInt displayCircleThreshold = new BindableInt(30) { MinValue = 0, - MaxValue = 100 + MaxValue = 100, + Precision = 1, }; private ExpandableSlider toleranceSlider = null!; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index e0c486a688..5cc25630aa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 0f, MaxValue = OsuPlayfield.BASE_SIZE.X, - Precision = 0.01f, + Precision = 0.1f, }; /// @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 0f, MaxValue = OsuPlayfield.BASE_SIZE.Y, - Precision = 0.01f, + Precision = 0.1f, }; /// @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 4f, MaxValue = 256f, - Precision = 0.01f, + Precision = 0.1f, }; /// @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = -180f, MaxValue = 180f, - Precision = 0.01f, + Precision = 0.1f, }; /// From edf7a126c8afcae3a0da9713a980a0c944a03f85 Mon Sep 17 00:00:00 2001 From: marvin Date: Fri, 21 Nov 2025 01:32:11 +0100 Subject: [PATCH 180/308] Use single drawable for background --- .../Match/MatchmakingBackgroundScreen.cs | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs index cfb3f2e6c4..89a404faaa 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs @@ -4,11 +4,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Overlays; -using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { @@ -27,29 +25,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match [BackgroundDependencyLoader] private void load(TextureStore textures, OverlayColourProvider colourProvider) { - AddRangeInternal(new Drawable[] + InternalChild = new Sprite { - new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = textures.Get("Backgrounds/bg1"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Colour4, - Alpha = 0.5f, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, - } - }); + RelativeSizeAxes = Axes.Both, + Texture = textures.Get("Backgrounds/bg1"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + Colour = colourProvider.Dark2 + }; } } } From 1dd026c0f096e7e34a10110fbdb55f3eeb46ac07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 13:55:01 +0900 Subject: [PATCH 181/308] Fix everything crashing --- .../Matchmaking/MatchmakingTestScene.cs | 10 +++--- .../Matchmaking/Intro/ScreenIntro.cs | 27 ++------------- .../Match/MatchmakingBackgroundScreen.cs | 33 ++++++++----------- .../Matchmaking/Match/ScreenMatchmaking.cs | 5 ++- 4 files changed, 26 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/MatchmakingTestScene.cs b/osu.Game.Tests/Visual/Matchmaking/MatchmakingTestScene.cs index 8c01083b12..ebfe795028 100644 --- a/osu.Game.Tests/Visual/Matchmaking/MatchmakingTestScene.cs +++ b/osu.Game.Tests/Visual/Matchmaking/MatchmakingTestScene.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; +using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; @@ -19,14 +20,15 @@ namespace osu.Game.Tests.Visual.Matchmaking protected MatchmakingTestScene() { + BackgroundScreenStack backgroundStack; + base.Content.AddRange(new Drawable[] { - new MatchmakingBackgroundScreen.Content - { - RelativeSizeAxes = Axes.Both, - }, + backgroundStack = new BackgroundScreenStack(), Content = new Container { RelativeSizeAxes = Axes.Both } }); + + backgroundStack.Push(new MatchmakingBackgroundScreen(colourProvider)); } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs index 093d9f6117..7d630ff986 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs @@ -5,7 +5,6 @@ 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; @@ -13,7 +12,7 @@ using osu.Framework.Screens; 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.Match; using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro @@ -53,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro private IDisposable? duckOperation; - protected override BackgroundScreen CreateBackground() => new MatchmakingIntroBackgroundScreen(colourProvider); + protected override BackgroundScreen CreateBackground() => new MatchmakingBackgroundScreen(colourProvider); public ScreenIntro() { @@ -240,27 +239,5 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro 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/Match/MatchmakingBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs index 89a404faaa..bc832a9346 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingBackgroundScreen.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Overlays; @@ -12,29 +11,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { public partial class MatchmakingBackgroundScreen : BackgroundScreen { - public MatchmakingBackgroundScreen() + private readonly OverlayColourProvider colourProvider; + + public MatchmakingBackgroundScreen(OverlayColourProvider colourProvider) { - InternalChild = new Content - { - RelativeSizeAxes = Axes.Both - }; + this.colourProvider = colourProvider; } - public partial class Content : CompositeDrawable + [BackgroundDependencyLoader] + private void load(TextureStore textures) { - [BackgroundDependencyLoader] - private void load(TextureStore textures, OverlayColourProvider colourProvider) + InternalChild = new Sprite { - InternalChild = new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = textures.Get("Backgrounds/bg1"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - Colour = colourProvider.Dark2 - }; - } + RelativeSizeAxes = Axes.Both, + Texture = textures.Get("Backgrounds/bg1"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + Colour = colourProvider.Dark2 + }; } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 8a48a089b4..a809270574 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -57,7 +57,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public override bool ShowFooter => true; - protected override BackgroundScreen CreateBackground() => new MatchmakingBackgroundScreen(); + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + protected override BackgroundScreen CreateBackground() => new MatchmakingBackgroundScreen(colourProvider); [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker(); From fa8d303922d3fec0ec6910cf895c8619a5490dce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 14:54:25 +0900 Subject: [PATCH 182/308] 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 2df686d354..98d4ebc316 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 74dae877f1..4a5919d16d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 8e78f4dac4fb1975eb48da6d767bbf9edfd43079 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 15:33:14 +0900 Subject: [PATCH 183/308] Adjust button colour and don't show warning --- osu.Game/Overlays/Settings/Sections/GeneralSection.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index 848fbd9c7a..b3ef1b9242 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Localisation; +using osu.Game.Online.Chat; using osu.Game.Overlays.Settings.Sections.General; namespace osu.Game.Overlays.Settings.Sections @@ -49,7 +50,8 @@ namespace osu.Game.Overlays.Settings.Sections { Text = GeneralSettingsStrings.ReportIssue, TooltipText = GeneralSettingsStrings.ReportIssueTooltip, - Action = () => game?.OpenUrlExternally(@"https://osu.ppy.sh/community/forums/topics/create?forum_id=5") + BackgroundColour = colours.DarkOrange2, + Action = () => game?.OpenUrlExternally(@"https://osu.ppy.sh/community/forums/topics/create?forum_id=5", LinkWarnMode.NeverWarn) }, new LanguageSettings(), new UpdateSettings(), From 6362cdb6755509395dcc7618a7594d6dd566f15f Mon Sep 17 00:00:00 2001 From: marvin Date: Thu, 20 Nov 2025 18:45:40 +0100 Subject: [PATCH 184/308] Replace `MatchmakingRoomState.CandidateType` with `MatchmakingRoomState.FinalItem` --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 9 ++++---- .../Matchmaking/TestSceneMatchmakingScreen.cs | 1 + .../Visual/Matchmaking/TestScenePickScreen.cs | 3 +-- .../Matchmaking/MatchmakingCandidateType.cs | 11 ---------- .../Matchmaking/MatchmakingRoomState.cs | 2 +- .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 22 ++++++------------- .../BeatmapSelect/SubScreenBeatmapSelect.cs | 5 ++--- .../Match/ScreenMatchmaking.ScreenStack.cs | 2 +- 8 files changed, 17 insertions(+), 38 deletions(-) delete mode 100644 osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingCandidateType.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 4ca014bf2a..3bbf001b64 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -13,7 +13,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; @@ -131,7 +130,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { var (candidateItems, finalItem) = pickRandomItems(5); - grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, MatchmakingCandidateType.UserSelection); + grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, finalItem); }); } @@ -160,7 +159,7 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(finalItem, duration: 0); - Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem, finalItem, MatchmakingCandidateType.UserSelection), 500); + Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem, finalItem), 500); }); } @@ -175,7 +174,7 @@ namespace osu.Game.Tests.Visual.Matchmaking grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0); grid.PlayRollAnimation(finalItem, duration: 0); - Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem, finalItem, MatchmakingCandidateType.UserSelection), 500); + Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem, finalItem), 500); }); } @@ -192,7 +191,7 @@ namespace osu.Game.Tests.Visual.Matchmaking Scheduler.AddDelayed(() => { - grid.PresentRolledBeatmap(-1, finalItem, MatchmakingCandidateType.Random); + grid.PresentRolledBeatmap(-1, finalItem); }, 500); }); } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index e88b10d30d..642e9926c3 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -110,6 +110,7 @@ namespace osu.Game.Tests.Visual.Matchmaking state.CandidateItems = beatmaps.Select(b => b.ID).ToArray(); state.CandidateItem = beatmaps[0].ID; + state.FinalItem = beatmaps[0].ID; }, waitTime: 35); changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index 1b7cc90132..5bb6ab2c44 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -9,7 +9,6 @@ using osu.Framework.Screens; using osu.Framework.Utils; 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.Match.BeatmapSelect; using osu.Game.Tests.Visual.Multiplayer; @@ -105,7 +104,7 @@ namespace osu.Game.Tests.Visual.Matchmaking long[] candidateItems = selectedItems.ToArray(); long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)]; - screen.RollFinalBeatmap(candidateItems, finalItem, MatchmakingCandidateType.UserSelection); + screen.RollFinalBeatmap(candidateItems, finalItem, finalItem); }); } } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingCandidateType.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingCandidateType.cs deleted file mode 100644 index 91b436088b..0000000000 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingCandidateType.cs +++ /dev/null @@ -1,11 +0,0 @@ -// 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.Multiplayer.MatchTypes.Matchmaking -{ - public enum MatchmakingCandidateType - { - UserSelection, - Random, - } -} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs index 44774c350d..2901074fb0 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking public MatchmakingUserList Users { get; set; } = new MatchmakingUserList(); [Key(5)] - public MatchmakingCandidateType CandidateType { get; set; } + public long FinalItem { get; set; } /// /// Advances to the next round. diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 137f7712e0..eb2bb37482 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -17,7 +17,6 @@ using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osuTK; @@ -141,18 +140,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect panel.RemoveUser(user); }); - public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId, MatchmakingCandidateType candidateType) => whenPanelsLoaded(() => + public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long candidateItemId, long finalItemId) => whenPanelsLoaded(() => { - long candidateItemId = candidateType switch - { - MatchmakingCandidateType.UserSelection => finalItemId, - MatchmakingCandidateType.Random => -1, - _ => throw new ArgumentOutOfRangeException(nameof(candidateType), candidateType, null) - }; - Debug.Assert(candidateItemIds.Length >= 1); Debug.Assert(candidateItemIds.Contains(candidateItemId)); - Debug.Assert(panelLookup.ContainsKey(finalItemId)); + Debug.Assert(panelLookup.ContainsKey(candidateItemId)); Debug.Assert(candidateItemIds.All(id => panelLookup.ContainsKey(id))); allowSelection = false; @@ -164,7 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect this.Delay(ARRANGE_DELAY) .Schedule(() => ArrangeItemsForRollAnimation()) .Delay(arrange_duration + present_beatmap_delay) - .Schedule(() => PresentUnanimouslyChosenBeatmap(candidateItemId, finalItemId, candidateType)); + .Schedule(() => PresentUnanimouslyChosenBeatmap(candidateItemId, finalItemId)); } else { @@ -173,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect .Delay(arrange_duration) .Schedule(() => PlayRollAnimation(finalItemId, roll_duration)) .Delay(roll_duration + present_beatmap_delay) - .Schedule(() => PresentRolledBeatmap(candidateItemId, finalItemId, candidateType)); + .Schedule(() => PresentRolledBeatmap(candidateItemId, finalItemId)); } }); @@ -320,7 +312,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentRolledBeatmap(long candidateItem, long finalItem, MatchmakingCandidateType candidateType) + internal void PresentRolledBeatmap(long candidateItem, long finalItem) { Debug.Assert(rollContainer.Children.Any(it => it.Item.ID == candidateItem)); Debug.Assert(playlistItems.ContainsKey(finalItem)); @@ -346,11 +338,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentUnanimouslyChosenBeatmap(long candidateItem, long finalItem, MatchmakingCandidateType candidateType) + internal void PresentUnanimouslyChosenBeatmap(long candidateItem, long finalItem) { // TODO: display special animation in this case - PresentRolledBeatmap(candidateItem, finalItem, candidateType); + PresentRolledBeatmap(candidateItem, finalItem); } private readonly TaskCompletionSource panelsLoaded = new TaskCompletionSource(); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index 5f6886d23a..607a8e47f0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -13,7 +13,6 @@ using osu.Game.Database; using osu.Game.Graphics.UserInterface; 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.Rulesets.Mods; @@ -135,8 +134,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect beatmapSelectGrid.SetUserSelection(user, itemId, false); } - public void RollFinalBeatmap(long[] candidateItems, long finalItem, MatchmakingCandidateType panelType) => - beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, panelType); + public void RollFinalBeatmap(long[] candidateItems, long candidateItem, long finalItem) => + beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, candidateItem, finalItem); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs index ad517b0de5..4aee7e8a21 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match case MatchmakingStage.ServerBeatmapFinalised: Debug.Assert(screenStack.CurrentScreen is SubScreenBeatmapSelect); - ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem, matchmakingState.CandidateType); + ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem, matchmakingState.FinalItem); break; case MatchmakingStage.ResultsDisplaying: From 871c0ebe3d7a75fa8a21c27eb3f333a37d10442a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Nov 2025 15:34:30 +0900 Subject: [PATCH 185/308] Rename to `GameplayItem` + adjust documentation --- .../Matchmaking/TestSceneMatchmakingScreen.cs | 2 +- .../Matchmaking/MatchmakingRoomState.cs | 18 +++++++++++++++--- .../Match/BeatmapSelect/BeatmapSelectGrid.cs | 18 +++++++++--------- .../BeatmapSelect/SubScreenBeatmapSelect.cs | 4 ++-- .../Match/ScreenMatchmaking.ScreenStack.cs | 2 +- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index 642e9926c3..5b60f1e7a1 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Matchmaking state.CandidateItems = beatmaps.Select(b => b.ID).ToArray(); state.CandidateItem = beatmaps[0].ID; - state.FinalItem = beatmaps[0].ID; + state.GameplayItem = beatmaps[0].ID; }, waitTime: 35); changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload); diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs index 2901074fb0..0c4106ae2b 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs @@ -28,14 +28,20 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking public int CurrentRound { get; set; } /// - /// The playlist items that were picked as gameplay candidates. + /// The playlist items that were picked as candidates by user. /// + /// + /// May contain -1 when any users picked the "random" playlist item. + /// [Key(2)] public long[] CandidateItems { get; set; } = []; /// - /// The final gameplay candidate. + /// A playlist item from that was randomly picked by the server. /// + /// + /// May be -1 to indicate the "random" playlist item was chosen. + /// [Key(3)] public long CandidateItem { get; set; } @@ -45,8 +51,14 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking [Key(4)] public MatchmakingUserList Users { get; set; } = new MatchmakingUserList(); + /// + /// A playlist item from the room's playlist that will be played in the current round. + /// + /// + /// The value of this property may not equal or exist in . + /// [Key(5)] - public long FinalItem { get; set; } + public long GameplayItem { get; set; } /// /// Advances to the next round. diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index eb2bb37482..5e8975410f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect panel.RemoveUser(user); }); - public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long candidateItemId, long finalItemId) => whenPanelsLoaded(() => + public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long candidateItemId, long gameplayItemId) => whenPanelsLoaded(() => { Debug.Assert(candidateItemIds.Length >= 1); Debug.Assert(candidateItemIds.Contains(candidateItemId)); @@ -156,16 +156,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect this.Delay(ARRANGE_DELAY) .Schedule(() => ArrangeItemsForRollAnimation()) .Delay(arrange_duration + present_beatmap_delay) - .Schedule(() => PresentUnanimouslyChosenBeatmap(candidateItemId, finalItemId)); + .Schedule(() => PresentUnanimouslyChosenBeatmap(candidateItemId, gameplayItemId)); } else { this.Delay(ARRANGE_DELAY) .Schedule(() => ArrangeItemsForRollAnimation()) .Delay(arrange_duration) - .Schedule(() => PlayRollAnimation(finalItemId, roll_duration)) + .Schedule(() => PlayRollAnimation(gameplayItemId, roll_duration)) .Delay(roll_duration + present_beatmap_delay) - .Schedule(() => PresentRolledBeatmap(candidateItemId, finalItemId)); + .Schedule(() => PresentRolledBeatmap(candidateItemId, gameplayItemId)); } }); @@ -312,10 +312,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect } } - internal void PresentRolledBeatmap(long candidateItem, long finalItem) + internal void PresentRolledBeatmap(long candidateItem, long gameplayItem) { Debug.Assert(rollContainer.Children.Any(it => it.Item.ID == candidateItem)); - Debug.Assert(playlistItems.ContainsKey(finalItem)); + Debug.Assert(playlistItems.ContainsKey(gameplayItem)); foreach (var panel in rollContainer.Children) { @@ -331,18 +331,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { rollContainer.ChangeChildDepth(panel, float.MinValue); - var item = playlistItems[finalItem]; + var item = playlistItems[gameplayItem]; panel.PresentAsChosenBeatmap(item); }); } } - internal void PresentUnanimouslyChosenBeatmap(long candidateItem, long finalItem) + internal void PresentUnanimouslyChosenBeatmap(long candidateItem, long gameplayItem) { // TODO: display special animation in this case - PresentRolledBeatmap(candidateItem, finalItem); + PresentRolledBeatmap(candidateItem, gameplayItem); } private readonly TaskCompletionSource panelsLoaded = new TaskCompletionSource(); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index 607a8e47f0..9262e10526 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -134,8 +134,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect beatmapSelectGrid.SetUserSelection(user, itemId, false); } - public void RollFinalBeatmap(long[] candidateItems, long candidateItem, long finalItem) => - beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, candidateItem, finalItem); + public void RollFinalBeatmap(long[] candidateItems, long candidateItem, long gameplayItem) => + beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, candidateItem, gameplayItem); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs index 4aee7e8a21..4d5a7099c4 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match case MatchmakingStage.ServerBeatmapFinalised: Debug.Assert(screenStack.CurrentScreen is SubScreenBeatmapSelect); - ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem, matchmakingState.FinalItem); + ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem, matchmakingState.GameplayItem); break; case MatchmakingStage.ResultsDisplaying: From 34146b8bcbaa2bf3adb77390e67c303c81e82e83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 16:23:53 +0900 Subject: [PATCH 186/308] Update rounded button to be less rounded Intended to match the rest of the UI which is less rounded these days. See inline comment for reason for not matching `FormControl` corner radius just yet. --- .../Graphics/UserInterfaceV2/RoundedButton.cs | 18 +++--------------- osu.Game/Overlays/Settings/SettingsButton.cs | 7 ++++++- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index bf92f20526..01c495ae30 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -26,18 +26,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 private Color4? triangleGradientSecondColour; - public override float Height - { - get => base.Height; - set - { - base.Height = value; - - if (IsLoaded) - updateCornerRadius(); - } - } - public override Color4 BackgroundColour { get => base.BackgroundColour; @@ -61,7 +49,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 { base.LoadComplete(); - updateCornerRadius(); + // This doesn't match the latest design spec (should be 5) but is an in-between that feels right to the eye + // until we move everything over to Form controls. + Content.CornerRadius = 10; Add(Triangles = new TrianglesV2 { @@ -98,8 +88,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 base.OnHoverLost(e); } - private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2; - public virtual IEnumerable FilterTerms => new[] { Text }; public bool MatchingFilter diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 196ddca953..0033543fdb 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -16,7 +16,12 @@ namespace osu.Game.Overlays.Settings public SettingsButton() { RelativeSizeAxes = Axes.X; - Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }; + Margin = new MarginPadding { Vertical = -5 }; + Padding = new MarginPadding + { + Left = SettingsPanel.CONTENT_MARGINS, + Right = SettingsPanel.CONTENT_MARGINS, + }; } public IEnumerable Keywords { get; set; } = Array.Empty(); From df79269e6fa409cf7340de5553fb332f3ef57f9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 16:24:10 +0900 Subject: [PATCH 187/308] Adjust tablet settings layout to feel a touch nicer --- .../Overlays/Settings/Sections/Input/TabletAreaSelection.cs | 2 +- osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs index 33f4f49173..b3fdd53466 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -137,7 +137,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input rotation.BindValueChanged(val => { usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint); - tabletContainer.RotateTo(-val.NewValue, 800, Easing.OutQuint); + tabletContainer.RotateTo(-val.NewValue, 400, Easing.OutQuint); checkBounds(); }, true); diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 6aebec88a9..86b52328b2 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Alpha = 0, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0, 8), + Spacing = new Vector2(0, SettingsSection.ITEM_SPACING), Direction = FillDirection.Vertical, Children = new Drawable[] { From 908a950cd2431ed2dbed1a763101a709859dbf4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 16:25:04 +0900 Subject: [PATCH 188/308] Move quick action settings into own subsection --- .../Localisation/GeneralSettingsStrings.cs | 5 ++ .../Sections/General/QuickActionSettings.cs | 52 +++++++++++++++++++ .../Settings/Sections/GeneralSection.cs | 31 +---------- 3 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 7e4ee94286..d1e22c197e 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -29,6 +29,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Prefer24HourTimeDisplay => new TranslatableString(getKey(@"prefer_24_hour_time_display"), @"Prefer 24-hour time display"); + /// + /// "Quick Actions" + /// + public static LocalisableString QuickActionsHeader => new TranslatableString(getKey(@"quick_actions_header"), @"Quick Actions"); + /// /// "Updates" /// diff --git a/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs b/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs new file mode 100644 index 0000000000..94f0e7a7d1 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.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.Localisation; +using osu.Game.Graphics; +using osu.Game.Localisation; +using osu.Game.Online.Chat; + +namespace osu.Game.Overlays.Settings.Sections.General +{ + public partial class QuickActionSettings : SettingsSubsection + { + [Resolved(CanBeNull = true)] + private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; } + + [Resolved(CanBeNull = true)] + private OsuGame? game { get; set; } + + protected override LocalisableString Header => GeneralSettingsStrings.QuickActionsHeader; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AddRange(new Drawable[] + { + new SettingsButton + { + Text = GeneralSettingsStrings.RunSetupWizard, + Keywords = new[] { @"first run", @"initial", @"getting started", @"import", @"tutorial", @"recommended beatmaps" }, + TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription, + Action = () => firstRunSetupOverlay?.Show(), + }, + new SettingsButton + { + Text = GeneralSettingsStrings.LearnMoreAboutLazer, + TooltipText = GeneralSettingsStrings.LearnMoreAboutLazerTooltip, + BackgroundColour = colours.YellowDark, + Action = () => game?.ShowWiki(@"Help_centre/Upgrading_to_lazer") + }, + new SettingsButton + { + Text = GeneralSettingsStrings.ReportIssue, + TooltipText = GeneralSettingsStrings.ReportIssueTooltip, + BackgroundColour = colours.YellowDarker, + Action = () => game?.OpenUrlExternally(@"https://osu.ppy.sh/community/forums/topics/create?forum_id=5", LinkWarnMode.NeverWarn) + }, + }); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index b3ef1b9242..a8acbb6bbb 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -7,19 +7,12 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Localisation; -using osu.Game.Online.Chat; using osu.Game.Overlays.Settings.Sections.General; namespace osu.Game.Overlays.Settings.Sections { public partial class GeneralSection : SettingsSection { - [Resolved(CanBeNull = true)] - private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; } - - [Resolved(CanBeNull = true)] - private OsuGame? game { get; set; } - public override LocalisableString Header => CommonStrings.General; public override Drawable CreateIcon() => new SpriteIcon @@ -28,33 +21,13 @@ namespace osu.Game.Overlays.Settings.Sections }; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Children = new Drawable[] { - new SettingsButton - { - Text = GeneralSettingsStrings.RunSetupWizard, - Keywords = new[] { @"first run", @"initial", @"getting started", @"import", @"tutorial", @"recommended beatmaps" }, - TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription, - Action = () => firstRunSetupOverlay?.Show(), - }, - new SettingsButton - { - Text = GeneralSettingsStrings.LearnMoreAboutLazer, - TooltipText = GeneralSettingsStrings.LearnMoreAboutLazerTooltip, - BackgroundColour = colours.YellowDark, - Action = () => game?.ShowWiki(@"Help_centre/Upgrading_to_lazer") - }, - new SettingsButton - { - Text = GeneralSettingsStrings.ReportIssue, - TooltipText = GeneralSettingsStrings.ReportIssueTooltip, - BackgroundColour = colours.DarkOrange2, - Action = () => game?.OpenUrlExternally(@"https://osu.ppy.sh/community/forums/topics/create?forum_id=5", LinkWarnMode.NeverWarn) - }, new LanguageSettings(), new UpdateSettings(), + new QuickActionSettings(), }; } } From 15ee49348d9506cb21ce50eefcdb45a06cc5ed2d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Nov 2025 16:24:08 +0900 Subject: [PATCH 189/308] Add failing test --- .../Visual/Matchmaking/TestScenePickScreen.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index e894616f9e..53db8efaf2 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; @@ -107,5 +108,25 @@ namespace osu.Game.Tests.Visual.Matchmaking screen.RollFinalBeatmap(candidateItems, finalItem); }); } + + [Test] + public void TestExpiredBeatmapNotShown() + { + SubScreenBeatmapSelect screen = null!; + + AddStep("add screen with expired items", () => + { + MultiplayerClient.ClientRoom!.Playlist = + [ + new MultiplayerPlaylistItem(items[0]) { Expired = true }, + new MultiplayerPlaylistItem(items[1]) + ]; + + Child = new ScreenStack(screen = new SubScreenBeatmapSelect()); + }); + + AddUntilStep("items displayed", () => screen.ChildrenOfType().Any()); + AddAssert("expired item not shown", () => screen.ChildrenOfType().Count(), () => Is.EqualTo(1)); + } } } From d3860f1630d9bb56363c5f437e57997bb8173db5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Nov 2025 14:58:22 +0900 Subject: [PATCH 190/308] Fix quick play showing expired playlist items --- .../Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index 7951fc5448..8b11e84437 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Debug.Assert(client.Room != null); - loadItems(client.Room.Playlist.ToArray()).FireAndForget(); + loadItems(client.Room.Playlist.Where(item => !item.Expired).ToArray()).FireAndForget(); } private async Task loadItems(MultiplayerPlaylistItem[] items) From a8594f1c08dae7d72c91e25c32737132744f0e20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 16:25:13 +0900 Subject: [PATCH 191/308] Move installation settings into own subsection --- .../Localisation/GeneralSettingsStrings.cs | 5 + .../Sections/General/InstallationSettings.cs | 114 ++++++++++++++++++ .../Sections/General/UpdateSettings.cs | 88 +------------- .../Settings/Sections/GeneralSection.cs | 1 + 4 files changed, 121 insertions(+), 87 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index d1e22c197e..9b6276781a 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -29,6 +29,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Prefer24HourTimeDisplay => new TranslatableString(getKey(@"prefer_24_hour_time_display"), @"Prefer 24-hour time display"); + /// + /// "Installation" + /// + public static LocalisableString InstallationHeader => new TranslatableString(getKey(@"installation_header"), @"Installation"); + /// /// "Quick Actions" /// diff --git a/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs b/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs new file mode 100644 index 0000000000..92f6c5e3b0 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs @@ -0,0 +1,114 @@ +// 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; +using osu.Framework.Allocation; +using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Framework.Statistics; +using osu.Game.Graphics; +using osu.Game.IO; +using osu.Game.Localisation; +using osu.Game.Overlays.Notifications; +using osu.Game.Overlays.Settings.Sections.Maintenance; +using osu.Game.Utils; +using SharpCompress.Archives.Zip; + +namespace osu.Game.Overlays.Settings.Sections.General +{ + public partial class InstallationSettings : SettingsSubsection + { + protected override LocalisableString Header => GeneralSettingsStrings.InstallationHeader; + + [Resolved] + private INotificationOverlay? notifications { get; set; } + + [Resolved] + private OsuGame? game { get; set; } + + private Storage exportStorage = null!; + + [BackgroundDependencyLoader] + private void load(Storage storage, OsuColour colours) + { + bool isDesktop = RuntimeInfo.IsDesktop; + bool supportsExport = RuntimeInfo.OS != RuntimeInfo.Platform.Android; + + // Loosely update-related maintenance buttons. + if (isDesktop) + { + Add(new SettingsButton + { + Text = GeneralSettingsStrings.OpenOsuFolder, + Keywords = new[] { @"logs", @"files", @"access", "directory" }, + Action = () => storage.PresentExternally(), + }); + + Add(new DangerousSettingsButton + { + Text = GeneralSettingsStrings.ChangeFolderLocation, + Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) + }); + } + + if (supportsExport) + { + Add(new SettingsButton + { + Text = GeneralSettingsStrings.ExportLogs, + BackgroundColour = colours.YellowDarker, + Keywords = new[] { @"bug", "report", "logs", "files" }, + Action = () => Task.Run(exportLogs), + }); + + exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports"); + } + } + + private void exportLogs() + { + ProgressNotification notification = new ProgressNotification + { + State = ProgressNotificationState.Active, + Text = "Exporting logs...", + }; + + notifications?.Post(notification); + + const string archive_filename = "compressed-logs.zip"; + + try + { + GlobalStatistics.OutputToLog(); + Logger.Flush(); + + var logStorage = Logger.Storage; + + using (var outStream = exportStorage.CreateFileSafely(archive_filename)) + using (var zip = ZipArchive.Create()) + { + foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) + FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip); + + zip.SaveTo(outStream); + } + } + catch + { + notification.State = ProgressNotificationState.Cancelled; + + // cleanup if export is failed or canceled. + exportStorage.Delete(archive_filename); + throw; + } + + notification.CompletionText = "Exported logs! Click to view."; + notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename); + + notification.State = ProgressNotificationState.Completed; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 596e4b2589..04dec05399 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -7,20 +7,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Framework.Screens; -using osu.Framework.Statistics; using osu.Game.Configuration; -using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online.Multiplayer; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Notifications; -using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Updater; -using osu.Game.Utils; -using SharpCompress.Archives.Zip; namespace osu.Game.Overlays.Settings.Sections.General { @@ -45,15 +37,12 @@ namespace osu.Game.Overlays.Settings.Sections.General [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - private Storage exportStorage = null!; - [BackgroundDependencyLoader] - private void load(OsuConfigManager config, Storage storage) + private void load(OsuConfigManager config) { config.BindWith(OsuSetting.ReleaseStream, configReleaseStream); bool isDesktop = RuntimeInfo.IsDesktop; - bool supportsExport = RuntimeInfo.OS != RuntimeInfo.Platform.Android; bool canCheckUpdates = updateManager?.CanCheckForUpdate == true; if (canCheckUpdates) @@ -86,38 +75,6 @@ namespace osu.Game.Overlays.Settings.Sections.General Action = () => checkForUpdates().FireAndForget() }); } - - // Loosely update-related maintenance buttons. - if (isDesktop) - { - Add(new SettingsButton - { - Text = GeneralSettingsStrings.OpenOsuFolder, - Keywords = new[] { @"logs", @"files", @"access", "directory" }, - Action = () => storage.PresentExternally(), - }); - } - - if (supportsExport) - { - Add(new SettingsButton - { - Text = GeneralSettingsStrings.ExportLogs, - Keywords = new[] { @"bug", "report", "logs", "files" }, - Action = () => Task.Run(exportLogs), - }); - } - - if (isDesktop) - { - Add(new SettingsButton - { - Text = GeneralSettingsStrings.ChangeFolderLocation, - Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) - }); - } - - exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports"); } private void releaseStreamChanged(ValueChangedEvent stream) @@ -176,48 +133,5 @@ namespace osu.Game.Overlays.Settings.Sections.General checkForUpdatesButton.Enabled.Value = true; } } - - private void exportLogs() - { - ProgressNotification notification = new ProgressNotification - { - State = ProgressNotificationState.Active, - Text = "Exporting logs...", - }; - - notifications?.Post(notification); - - const string archive_filename = "compressed-logs.zip"; - - try - { - GlobalStatistics.OutputToLog(); - Logger.Flush(); - - var logStorage = Logger.Storage; - - using (var outStream = exportStorage.CreateFileSafely(archive_filename)) - using (var zip = ZipArchive.Create()) - { - foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) - FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip); - - zip.SaveTo(outStream); - } - } - catch - { - notification.State = ProgressNotificationState.Cancelled; - - // cleanup if export is failed or canceled. - exportStorage.Delete(archive_filename); - throw; - } - - notification.CompletionText = "Exported logs! Click to view."; - notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename); - - notification.State = ProgressNotificationState.Completed; - } } } diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index a8acbb6bbb..7124d9a37d 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -27,6 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections { new LanguageSettings(), new UpdateSettings(), + new InstallationSettings(), new QuickActionSettings(), }; } From a6a98fc078049e42680eb2d463c6a3a6f8d46d4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 16:30:24 +0900 Subject: [PATCH 192/308] Only show update settings if the game can be updated --- .../Sections/General/UpdateSettings.cs | 44 +++++++++---------- .../Settings/Sections/GeneralSection.cs | 15 +++---- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 04dec05399..87b1acc23a 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -43,38 +43,34 @@ namespace osu.Game.Overlays.Settings.Sections.General config.BindWith(OsuSetting.ReleaseStream, configReleaseStream); bool isDesktop = RuntimeInfo.IsDesktop; - bool canCheckUpdates = updateManager?.CanCheckForUpdate == true; - if (canCheckUpdates) + // For simplicity, hide the concept of release streams from mobile users. + if (isDesktop) { - // For simplicity, hide the concept of release streams from mobile users. - if (isDesktop) + Add(releaseStreamDropdown = new SettingsEnumDropdown { - Add(releaseStreamDropdown = new SettingsEnumDropdown - { - LabelText = GeneralSettingsStrings.ReleaseStream, - Current = { Value = configReleaseStream.Value }, - Keywords = new[] { @"version" }, - }); + LabelText = GeneralSettingsStrings.ReleaseStream, + Current = { Value = configReleaseStream.Value }, + Keywords = new[] { @"version" }, + }); - if (updateManager!.FixedReleaseStream != null) - { - configReleaseStream.Value = updateManager.FixedReleaseStream.Value; + if (updateManager!.FixedReleaseStream != null) + { + configReleaseStream.Value = updateManager.FixedReleaseStream.Value; - releaseStreamDropdown.ShowsDefaultIndicator = false; - releaseStreamDropdown.Items = [updateManager.FixedReleaseStream.Value]; - releaseStreamDropdown.SetNoticeText(GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning); - } - - releaseStreamDropdown.Current.BindValueChanged(releaseStreamChanged); + releaseStreamDropdown.ShowsDefaultIndicator = false; + releaseStreamDropdown.Items = [updateManager.FixedReleaseStream.Value]; + releaseStreamDropdown.SetNoticeText(GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning); } - Add(checkForUpdatesButton = new SettingsButton - { - Text = GeneralSettingsStrings.CheckUpdate, - Action = () => checkForUpdates().FireAndForget() - }); + releaseStreamDropdown.Current.BindValueChanged(releaseStreamChanged); } + + Add(checkForUpdatesButton = new SettingsButton + { + Text = GeneralSettingsStrings.CheckUpdate, + Action = () => checkForUpdates().FireAndForget() + }); } private void releaseStreamChanged(ValueChangedEvent stream) diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index 7124d9a37d..1243887386 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.General; +using osu.Game.Updater; namespace osu.Game.Overlays.Settings.Sections { @@ -21,15 +22,13 @@ namespace osu.Game.Overlays.Settings.Sections }; [BackgroundDependencyLoader] - private void load() + private void load(UpdateManager? updateManager) { - Children = new Drawable[] - { - new LanguageSettings(), - new UpdateSettings(), - new InstallationSettings(), - new QuickActionSettings(), - }; + Add(new LanguageSettings()); + if (updateManager?.CanCheckForUpdate == true) + Add(new UpdateSettings()); + Add(new InstallationSettings()); + Add(new QuickActionSettings()); } } } From 73349ab1825e449a8833f46ad19b3abf9dd5294c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 16:34:53 +0900 Subject: [PATCH 193/308] Move quick actions to top --- osu.Game/Overlays/Settings/Sections/GeneralSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index 1243887386..18e650d70d 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -24,11 +24,11 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load(UpdateManager? updateManager) { + Add(new QuickActionSettings()); Add(new LanguageSettings()); if (updateManager?.CanCheckForUpdate == true) Add(new UpdateSettings()); Add(new InstallationSettings()); - Add(new QuickActionSettings()); } } } From 56ce955e0c3d16c26fabc4cc75e371b801dd98f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 16:40:38 +0900 Subject: [PATCH 194/308] Move export logs to quick actions (to sit with report issue button) --- .../Sections/General/InstallationSettings.cs | 72 +----------------- .../Sections/General/QuickActionSettings.cs | 75 ++++++++++++++++++- 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs b/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs index 92f6c5e3b0..3aaeadd158 100644 --- a/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs @@ -1,21 +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.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Localisation; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; -using osu.Framework.Statistics; -using osu.Game.Graphics; -using osu.Game.IO; using osu.Game.Localisation; -using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Settings.Sections.Maintenance; -using osu.Game.Utils; -using SharpCompress.Archives.Zip; namespace osu.Game.Overlays.Settings.Sections.General { @@ -23,19 +15,13 @@ namespace osu.Game.Overlays.Settings.Sections.General { protected override LocalisableString Header => GeneralSettingsStrings.InstallationHeader; - [Resolved] - private INotificationOverlay? notifications { get; set; } - [Resolved] private OsuGame? game { get; set; } - private Storage exportStorage = null!; - [BackgroundDependencyLoader] - private void load(Storage storage, OsuColour colours) + private void load(Storage storage) { bool isDesktop = RuntimeInfo.IsDesktop; - bool supportsExport = RuntimeInfo.OS != RuntimeInfo.Platform.Android; // Loosely update-related maintenance buttons. if (isDesktop) @@ -53,62 +39,6 @@ namespace osu.Game.Overlays.Settings.Sections.General Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) }); } - - if (supportsExport) - { - Add(new SettingsButton - { - Text = GeneralSettingsStrings.ExportLogs, - BackgroundColour = colours.YellowDarker, - Keywords = new[] { @"bug", "report", "logs", "files" }, - Action = () => Task.Run(exportLogs), - }); - - exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports"); - } - } - - private void exportLogs() - { - ProgressNotification notification = new ProgressNotification - { - State = ProgressNotificationState.Active, - Text = "Exporting logs...", - }; - - notifications?.Post(notification); - - const string archive_filename = "compressed-logs.zip"; - - try - { - GlobalStatistics.OutputToLog(); - Logger.Flush(); - - var logStorage = Logger.Storage; - - using (var outStream = exportStorage.CreateFileSafely(archive_filename)) - using (var zip = ZipArchive.Create()) - { - foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) - FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip); - - zip.SaveTo(outStream); - } - } - catch - { - notification.State = ProgressNotificationState.Cancelled; - - // cleanup if export is failed or canceled. - exportStorage.Delete(archive_filename); - throw; - } - - notification.CompletionText = "Exported logs! Click to view."; - notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename); - - notification.State = ProgressNotificationState.Completed; } } } diff --git a/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs b/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs index 94f0e7a7d1..b6b78a6d00 100644 --- a/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs @@ -1,12 +1,22 @@ // 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; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Statistics; using osu.Game.Graphics; +using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online.Chat; +using osu.Game.Overlays.Notifications; +using osu.Game.Utils; +using SharpCompress.Archives.Zip; namespace osu.Game.Overlays.Settings.Sections.General { @@ -21,7 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.General protected override LocalisableString Header => GeneralSettingsStrings.QuickActionsHeader; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, Storage storage) { AddRange(new Drawable[] { @@ -47,6 +57,69 @@ namespace osu.Game.Overlays.Settings.Sections.General Action = () => game?.OpenUrlExternally(@"https://osu.ppy.sh/community/forums/topics/create?forum_id=5", LinkWarnMode.NeverWarn) }, }); + + bool supportsExport = RuntimeInfo.OS != RuntimeInfo.Platform.Android; + + if (supportsExport) + { + Add(new SettingsButton + { + Text = GeneralSettingsStrings.ExportLogs, + BackgroundColour = colours.YellowDarker.Darken(0.5f), + Keywords = new[] { @"bug", "report", "logs", "files" }, + Action = () => Task.Run(exportLogs), + }); + + exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports"); + } + } + + [Resolved] + private INotificationOverlay? notifications { get; set; } + + private Storage exportStorage = null!; + + private void exportLogs() + { + ProgressNotification notification = new ProgressNotification + { + State = ProgressNotificationState.Active, + Text = "Exporting logs...", + }; + + notifications?.Post(notification); + + const string archive_filename = "compressed-logs.zip"; + + try + { + GlobalStatistics.OutputToLog(); + Logger.Flush(); + + var logStorage = Logger.Storage; + + using (var outStream = exportStorage.CreateFileSafely(archive_filename)) + using (var zip = ZipArchive.Create()) + { + foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) + FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip); + + zip.SaveTo(outStream); + } + } + catch + { + notification.State = ProgressNotificationState.Cancelled; + + // cleanup if export is failed or canceled. + exportStorage.Delete(archive_filename); + throw; + } + + notification.CompletionText = "Exported logs! Click to view."; + notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename); + + notification.State = ProgressNotificationState.Completed; } } } From 13dab24d418eebad5c4884ccc4e61837b8729c38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 16:56:58 +0900 Subject: [PATCH 195/308] Adjust to 200 ms debounce This [matches stable](https://github.com/peppy/osu-stable-reference/blob/52f3f75ed7efd7b9eb56e1e45c95bb91504337be/osu!/Audio/AudioEngine.cs#L1295) and feels somewhat better. --- osu.Game/Overlays/NowPlayingOverlay.cs | 4 +++- .../Edit/Components/Timelines/Summary/Parts/MarkerPart.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 9f9f57336c..28119615f3 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -29,6 +29,8 @@ namespace osu.Game.Overlays { public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { + public const double TRACK_DRAG_SEEK_DEBOUNCE = 200; + public IconUsage Icon => OsuIcon.Music; public LocalisableString Title => NowPlayingStrings.HeaderTitle; public LocalisableString Description => NowPlayingStrings.HeaderDescription; @@ -226,7 +228,7 @@ namespace osu.Game.Overlays private void onSeek(double progress) { - if (lastSeekTime == null || Time.Current - lastSeekTime > 500) + if (lastSeekTime == null || Time.Current - lastSeekTime > TRACK_DRAG_SEEK_DEBOUNCE) { musicController.SeekTo(progress); lastSeekTime = Time.Current; diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index c9a44eab4a..b7453bf7f6 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Game.Overlays; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; @@ -66,7 +67,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts double seekDestination = markerPos / DrawWidth * editorClock.TrackLength; marker.X = (float)seekDestination; - if (!instant && lastSeekTime != null && Time.Current - lastSeekTime < 500) + if (!instant && lastSeekTime != null && Time.Current - lastSeekTime < NowPlayingOverlay.TRACK_DRAG_SEEK_DEBOUNCE) return; editorClock.SeekSmoothlyTo(seekDestination); From 98e7a10e1e7b2514a64c7a423c44c96fd60d3320 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 17:13:44 +0900 Subject: [PATCH 196/308] Rename localised string --- osu.Game/Localisation/CommonStrings.cs | 2 +- osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 22fc2bb242..d72257f438 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -202,7 +202,7 @@ namespace osu.Game.Localisation /// /// "Delete..." /// - public static LocalisableString DeleteEllipsis => new TranslatableString(getKey(@"delete_ellipsis"), @"Delete..."); + public static LocalisableString DeleteWithConfirmation => new TranslatableString(getKey(@"delete_with_confrmation"), @"Delete..."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs index afbe2450d6..2f0f36c99c 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(beatmap.BeatmapSet != null); addHeader(SongSelectStrings.ForAllDifficulties, beatmap.BeatmapSet.ToString()); - addButton(CommonStrings.DeleteEllipsis, FontAwesome.Solid.Trash, () => SongSelect?.Delete(beatmap.BeatmapSet), colours.Red1); + addButton(CommonStrings.DeleteWithConfirmation, FontAwesome.Solid.Trash, () => SongSelect?.Delete(beatmap.BeatmapSet), colours.Red1); addHeader(SongSelectStrings.ForSelectedDifficulty, beatmap.DifficultyName); diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index f8459bfedb..befdba1b2b 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -273,7 +273,7 @@ namespace osu.Game.Screens.SelectV2 if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem(SongSelectStrings.RestoreAllHidden, MenuItemType.Standard, () => songSelect?.RestoreAllHidden(beatmapSet))); - items.Add(new OsuMenuItem(CommonStrings.DeleteEllipsis, MenuItemType.Destructive, () => songSelect?.Delete(beatmapSet))); + items.Add(new OsuMenuItem(CommonStrings.DeleteWithConfirmation, MenuItemType.Destructive, () => songSelect?.Delete(beatmapSet))); return items.ToArray(); } } From fc74726d114f97fd8f885d2ddb42c4bf1bb7b91e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 17:25:40 +0900 Subject: [PATCH 197/308] Ensure the skip overlay shows when someone votes to skip --- .../Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 35e85c3273..79fd0eb8d1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -95,6 +95,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserVotedToSkipIntro(int userId) => Schedule(() => { + FadingContent.TriggerShow(); + updateText(); countText.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); From 90e7faf271806bb465d9ac1f6190443b5b8716ff Mon Sep 17 00:00:00 2001 From: marvin Date: Fri, 21 Nov 2025 09:57:14 +0100 Subject: [PATCH 198/308] Replace PowEasingFunction with CubicBezieEasingFunction --- .../MatchmakingSelectPanelRandom.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs index c789c3dc74..6389766b46 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.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.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -48,10 +47,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect this.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) .ScaleTo(1.5f, 1000, Easing.OutExpo); - content.Dice.MoveToY(-200, duration * 0.55, new PowEasingFunction(2.75, easeOut: true)) + content.Dice.MoveToY(-200, duration * 0.55, new CubicBezierEasingFunction(0.33, 1, 0.8, 1)) .Then() .Schedule(() => ChangeInternalChildDepth(diceProxy, float.MaxValue)) - .MoveToY(-DrawHeight / 2, duration * 0.45, new PowEasingFunction(2.2)) + .MoveToY(-DrawHeight / 2, duration * 0.45, new CubicBezierEasingFunction(0.2, 0, 0.55, 0)) .Then() .FadeOut() .Expire(); @@ -105,18 +104,5 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect return base.OnClick(e); } - - private readonly struct PowEasingFunction(double exponent, bool easeOut = false) : IEasingFunction - { - public double ApplyEasing(double time) - { - if (easeOut) - time = 1 - time; - - double value = Math.Pow(time, exponent); - - return easeOut ? 1 - value : value; - } - } } } From d8b71423b0a1bb7d546a9c1a2fa4ccc5b120db85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Nov 2025 19:33:38 +0900 Subject: [PATCH 199/308] 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 98d4ebc316..20c0f7cec5 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 4a5919d16d..c2796cf000 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From a2bfb409d2c8edad8f3935a917faaf4f2c590b31 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 22 Nov 2025 03:16:36 +0500 Subject: [PATCH 200/308] Use actual mod-adjusted map difficulty settings in the `SongBar` --- .../Components/TestSceneSongBar.cs | 1 + osu.Game.Tournament/Components/SongBar.cs | 44 ++++++++----------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index 95d6b6d107..285937ef03 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -61,6 +61,7 @@ namespace osu.Game.Tournament.Tests.Components AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock); AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime); + AddStep("set mods to HDHRDT", () => songBar.Mods = LegacyMods.Hidden | LegacyMods.HardRock | LegacyMods.DoubleTime); AddStep("unset mods", () => songBar.Mods = LegacyMods.None); AddToggleStep("toggle expanded", expanded => songBar.Expanded = expanded); diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index cff86cf0a1..11cb04e540 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -14,6 +15,7 @@ using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Models; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Utils; using osuTK; @@ -123,27 +125,19 @@ namespace osu.Game.Tournament.Components }, }; - double bpm = beatmap.BPM; - double length = beatmap.Length; - string hardRockExtra = ""; + var rulesetInstance = ruleset.Value.CreateInstance(); + + var convertedMods = rulesetInstance.ConvertFromLegacyMods(mods).ToList(); + var adjustedDifficulty = rulesetInstance.GetAdjustedDisplayDifficulty(beatmap, convertedMods); + + double rate = ModUtils.CalculateRateWithMods(convertedMods); + double bpm = FormatUtils.RoundBPM(beatmap.BPM, rate); + double length = beatmap.Length / rate; + string srExtra = ""; - float ar = beatmap.Difficulty.ApproachRate; - - if ((mods & LegacyMods.HardRock) > 0) + if (convertedMods.Any(x => x is ModHardRock) || convertedMods.Any(x => x is ModDoubleTime)) { - hardRockExtra = "*"; - srExtra = "*"; - } - - if ((mods & LegacyMods.DoubleTime) > 0) - { - // temporary local calculation (taken from OsuDifficultyCalculator) - double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(ar, 1800, 1200, 450) / 1.5; - ar = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5); - - bpm *= 1.5f; - length /= 1.5f; srExtra = "*"; } @@ -154,9 +148,9 @@ namespace osu.Game.Tournament.Components default: stats = new (string heading, string content)[] { - ("CS", $"{beatmap.Difficulty.CircleSize:0.#}{hardRockExtra}"), - ("AR", $"{ar:0.#}{hardRockExtra}"), - ("OD", $"{beatmap.Difficulty.OverallDifficulty:0.#}{hardRockExtra}"), + ("CS", $"{adjustedDifficulty.CircleSize:0.#}"), + ("AR", $"{adjustedDifficulty.ApproachRate:0.#}"), + ("OD", $"{adjustedDifficulty.OverallDifficulty:0.#}"), }; break; @@ -164,16 +158,16 @@ namespace osu.Game.Tournament.Components case 3: stats = new (string heading, string content)[] { - ("OD", $"{beatmap.Difficulty.OverallDifficulty:0.#}{hardRockExtra}"), - ("HP", $"{beatmap.Difficulty.DrainRate:0.#}{hardRockExtra}") + ("OD", $"{adjustedDifficulty.OverallDifficulty:0.#}"), + ("HP", $"{adjustedDifficulty.DrainRate:0.#}") }; break; case 2: stats = new (string heading, string content)[] { - ("CS", $"{beatmap.Difficulty.CircleSize:0.#}{hardRockExtra}"), - ("AR", $"{ar:0.#}"), + ("CS", $"{adjustedDifficulty.CircleSize:0.#}"), + ("AR", $"{adjustedDifficulty.ApproachRate:0.#}"), }; break; } From fd652982ceded9fca5bf57e7ee35c3d0358d05c0 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 22 Nov 2025 03:29:39 +0500 Subject: [PATCH 201/308] Add ruleset tests --- .../Components/TestSceneSongBar.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index 285937ef03..28ced3e0ad 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -5,6 +5,10 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -52,6 +56,7 @@ namespace osu.Game.Tournament.Tests.Components beatmap.ApproachRate = 6.8f; beatmap.OverallDifficulty = 5.5f; beatmap.StarRating = 4.56f; + beatmap.DrainRate = 1.23f; beatmap.Length = 123456; beatmap.BPM = 133; beatmap.OnlineID = ladderBeatmap.OnlineID; @@ -62,11 +67,17 @@ namespace osu.Game.Tournament.Tests.Components AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock); AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime); AddStep("set mods to HDHRDT", () => songBar.Mods = LegacyMods.Hidden | LegacyMods.HardRock | LegacyMods.DoubleTime); + AddStep("unset mods", () => songBar.Mods = LegacyMods.None); AddToggleStep("toggle expanded", expanded => songBar.Expanded = expanded); AddStep("set null beatmap", () => songBar.Beatmap = null); + + AddStep("set ruleset to osu", () => Ruleset.Value = new OsuRuleset().RulesetInfo); + AddStep("set ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddStep("set ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); + AddStep("set ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); } } } From 8900c79758f576bd441d3f246bb6fe4f3046cf4f Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 22 Nov 2025 03:35:10 +0500 Subject: [PATCH 202/308] Set `TournamentBeatmap`'s `IBeatmapInfo.Ruleset` to a dummy ruleset. This is being queried by the https://github.com/ppy/osu/blob/master/osu.Game.Rulesets.Mania/ManiaRuleset.cs#L442 but since we don't actually draw column count anywhere nor are we supposed to be running converts in tournaments it should be safe to populate it with nothing. --- osu.Game.Tournament/Models/TournamentBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Models/TournamentBeatmap.cs b/osu.Game.Tournament/Models/TournamentBeatmap.cs index a7ba5b7db1..79dbb680d7 100644 --- a/osu.Game.Tournament/Models/TournamentBeatmap.cs +++ b/osu.Game.Tournament/Models/TournamentBeatmap.cs @@ -83,7 +83,7 @@ namespace osu.Game.Tournament.Models string IBeatmapInfo.MD5Hash => throw new NotImplementedException(); - IRulesetInfo IBeatmapInfo.Ruleset => throw new NotImplementedException(); + IRulesetInfo IBeatmapInfo.Ruleset => new RulesetInfo(); DateTimeOffset IBeatmapSetOnlineInfo.Submitted => throw new NotImplementedException(); From d0e09e5b5c31c432f5fa80980fd77ce7e4fc74dc Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 22 Nov 2025 17:03:04 -0800 Subject: [PATCH 203/308] Fix one remaining case of "copy link" not using existing localisation --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index fe03fca4b8..84b420d791 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -25,6 +25,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; @@ -430,7 +431,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { items.AddRange([ new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(url)), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyToClipboard(url)) + new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard(url)) ]); } From b6ccc8cae42d51cd424c5fe0c61dd9e95a051ca5 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 22 Nov 2025 17:06:04 -0800 Subject: [PATCH 204/308] Replace local osd and clipboard method with existing game method --- osu.Game/Overlays/Comments/DrawableComment.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 805d997998..33f09b7622 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -20,13 +20,11 @@ using System.Collections.Specialized; using System.Diagnostics; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Comments.Buttons; using osu.Game.Overlays.Dialog; -using osu.Game.Overlays.OSD; using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Comments @@ -83,10 +81,7 @@ namespace osu.Game.Overlays.Comments private IAPIProvider api { get; set; } = null!; [Resolved] - private Clipboard clipboard { get; set; } = null!; - - [Resolved] - private OnScreenDisplay? onScreenDisplay { get; set; } + private OsuGame? game { get; set; } public DrawableComment(Comment comment, IReadOnlyList meta) { @@ -329,7 +324,7 @@ namespace osu.Game.Overlays.Comments if (WasDeleted) makeDeleted(); - actionsContainer.AddLink(CommonStrings.ButtonsPermalink, copyUrl); + actionsContainer.AddLink(CommonStrings.ButtonsPermalink, () => game?.CopyToClipboard($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}")); actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10)); actionsContainer.AddLink(CommonStrings.ButtonsReply.ToLower(), toggleReply); actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10)); @@ -417,12 +412,6 @@ namespace osu.Game.Overlays.Comments api.Queue(request); } - private void copyUrl() - { - clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}"); - onScreenDisplay?.Display(new CopiedToClipboardToast()); - } - private void toggleReply() { if (replyEditorContainer.Count == 0) From 49eb013967e0e7f5802742e504f8f3047aa89b83 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 22 Nov 2025 17:06:34 -0800 Subject: [PATCH 205/308] Fix some copy link actions/buttons not showing copied toast --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 4 ++-- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 5 ++--- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 7 +++---- osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs | 5 ++--- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 258cca2ad5..53faafcf36 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -24,7 +24,7 @@ namespace osu.Game.Online.Chat private GameHost host { get; set; } = null!; [Resolved] - private Clipboard clipboard { get; set; } = null!; + private OsuGame? game { get; set; } [Resolved] private IDialogOverlay? dialogOverlay { get; set; } @@ -88,7 +88,7 @@ namespace osu.Game.Online.Chat } if (dialogOverlay != null && shouldWarn) - dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url))); + dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => game?.CopyToClipboard(url))); else host.OpenUrlExternally(url); } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index bc617cae80..e5aac279fb 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -17,7 +17,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Framework.Platform; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -76,7 +75,7 @@ namespace osu.Game.Online.Leaderboards private SongSelect songSelect { get; set; } [Resolved(canBeNull: true)] - private Clipboard clipboard { get; set; } + private OsuGame game { get; set; } [Resolved] private IAPIProvider api { get; set; } @@ -459,7 +458,7 @@ namespace osu.Game.Online.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = copyableMods)); if (Score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); if (Score.Files.Count > 0) { diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 195625dcde..7f6ddcd54c 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; @@ -22,8 +21,8 @@ namespace osu.Game.Screens.Edit.Compose { public partial class ComposeScreen : EditorScreenWithTimeline, IGameplaySettings { - [Resolved] - private Clipboard hostClipboard { get; set; } = null!; + [Resolved(canBeNull: true)] + private OsuGame game { get; set; } [Resolved] private EditorClock clock { get; set; } @@ -138,7 +137,7 @@ namespace osu.Game.Screens.Edit.Compose // regardless of whether anything was even selected at all. // UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory. // note that this means that `getTimestamp()` must handle no-selection case, too. - hostClipboard.SetText(getTimestamp()); + game?.CopyToClipboard(getTimestamp()); if (CanCopy.Value) clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 5013150f05..16c9ed64f6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Graphics; @@ -77,7 +76,7 @@ namespace osu.Game.Screens.SelectV2 private OsuConfigManager config { get; set; } = null!; [Resolved] - private Clipboard? clipboard { get; set; } + private OsuGame? game { get; set; } [Resolved] private IAPIProvider api { get; set; } = null!; @@ -625,7 +624,7 @@ namespace osu.Game.Screens.SelectV2 items.Add(new OsuMenuItem(SongSelectStrings.UseTheseMods, MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); if (Score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); if (Score.Files.Count <= 0) return items.ToArray(); From 9d88c761d3414d3e4353bd0a66f2faac2630c83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Nov 2025 10:32:42 +0100 Subject: [PATCH 206/308] Add double-click-nub-to-reset function to form slider bars See https://github.com/ppy/osu/pull/35742#issuecomment-3561517030. --- .../UserInterface/TestSceneFormSliderBar.cs | 50 ++++++++++++++++++- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 39 ++++++++++++--- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs index 97835a993d..d25ef3a889 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs @@ -1,20 +1,24 @@ // 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.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneFormSliderBar : OsuTestScene + public partial class TestSceneFormSliderBar : OsuManualInputManagerTestScene { [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -59,5 +63,49 @@ namespace osu.Game.Tests.Visual.UserInterface slider.TransferValueOnCommit = b; }); } + + [Test] + public void TestNubDoubleClickRevertToDefault() + { + FormSliderBar slider = null!; + + AddStep("create content", () => + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + slider = new FormSliderBar + { + Caption = "Slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + Default = 5f, + } + }, + } + }; + }); + AddStep("set slider to 1", () => slider.Current.Value = 1); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is default", () => slider.Current.IsDefault); + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 59217f64ab..8ebaf48ed6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -324,8 +324,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 private Box leftBox = null!; private Box rightBox = null!; - private Circle nub = null!; - private const float nub_width = 10; + private InnerSliderNub nub = null!; + public const float NUB_WIDTH = 10; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -335,7 +335,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Height = 40; RelativeSizeAxes = Axes.X; - RangePadding = nub_width / 2; + RangePadding = NUB_WIDTH / 2; Children = new Drawable[] { @@ -364,12 +364,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = RangePadding, }, - Child = nub = new Circle + Child = nub = new InnerSliderNub { - Width = nub_width, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - Origin = Anchor.TopCentre, + ResetToDefault = () => + { + if (!Current.Disabled) + Current.SetDefault(); + } } }, new HoverClickSounds() @@ -452,5 +453,27 @@ namespace osu.Game.Graphics.UserInterfaceV2 return result; } } + + private partial class InnerSliderNub : Circle + { + public Action? ResetToDefault { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Width = InnerSlider.NUB_WIDTH; + RelativeSizeAxes = Axes.Y; + RelativePositionAxes = Axes.X; + Origin = Anchor.TopCentre; + } + + protected override bool OnClick(ClickEvent e) => true; // must be handled for double click handler to ever fire + + protected override bool OnDoubleClick(DoubleClickEvent e) + { + ResetToDefault?.Invoke(); + return true; + } + } } } From 83ce56b7183916a1f4f7c5ad3a96a51f3b5d8c40 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 24 Nov 2025 15:11:47 +0500 Subject: [PATCH 207/308] Use `APIRuleset` instead of a blank `RulesetInfo` --- osu.Game.Tournament/Models/TournamentBeatmap.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Models/TournamentBeatmap.cs b/osu.Game.Tournament/Models/TournamentBeatmap.cs index 79dbb680d7..83c42b793d 100644 --- a/osu.Game.Tournament/Models/TournamentBeatmap.cs +++ b/osu.Game.Tournament/Models/TournamentBeatmap.cs @@ -6,6 +6,7 @@ using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; +using static osu.Game.Online.API.Requests.Responses.APIBeatmap; namespace osu.Game.Tournament.Models { @@ -31,6 +32,8 @@ namespace osu.Game.Tournament.Models public BeatmapSetOnlineCovers Covers { get; set; } + public APIRuleset Ruleset { get; set; } = new APIRuleset(); + public TournamentBeatmap() { } @@ -47,6 +50,7 @@ namespace osu.Game.Tournament.Models Covers = beatmap.BeatmapSet?.Covers ?? new BeatmapSetOnlineCovers(); EndTimeObjectCount = beatmap.EndTimeObjectCount; TotalObjectCount = beatmap.TotalObjectCount; + Ruleset = (APIRuleset)beatmap.Ruleset; } public bool Equals(IBeatmapInfo? other) => other is TournamentBeatmap b && this.MatchesOnlineID(b); @@ -83,7 +87,7 @@ namespace osu.Game.Tournament.Models string IBeatmapInfo.MD5Hash => throw new NotImplementedException(); - IRulesetInfo IBeatmapInfo.Ruleset => new RulesetInfo(); + IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; DateTimeOffset IBeatmapSetOnlineInfo.Submitted => throw new NotImplementedException(); From 33c8c4d639f1213bdbc35fca6a54d28a1f6546d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Nov 2025 11:07:29 +0100 Subject: [PATCH 208/308] Add failing test --- .../Chat/TestSceneChannelManager.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index 5c7f0b0a2f..768137b0cf 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -220,6 +220,31 @@ namespace osu.Game.Tests.Chat AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty); } + [Test] + public void TestPrivateChannelsPurgedOnUserChange() + { + var pmChannel = createChannel(1002, ChannelType.PM); + AddStep("join a few private channels", () => + { + channelManager.JoinChannel(createChannel(1001, ChannelType.PM)); + channelManager.JoinChannel(createChannel(1003, ChannelType.Team)); + channelManager.JoinChannel(pmChannel); + }); + AddStep("close a PM channel", () => channelManager.LeaveChannel(pmChannel)); + + AddStep("switch user", () => + { + ((DummyAPIAccess.DummyLocalUserState)API.LocalUserState).User.Value = new APIUser + { + Id = 9009, + Username = "someone_else" + }; + }); + + AddAssert("not joined to private channels of previous user", + () => !channelManager.JoinedChannels.Select(ch => ch.Id).Any(id => id >= 1001 && id <= 1003)); + } + private void handlePostMessageRequest(PostMessageRequest request) { var message = new Message(++currentMessageId) @@ -250,7 +275,7 @@ namespace osu.Game.Tests.Chat } } - private Channel createChannel(int id, ChannelType type) => new Channel(new APIUser()) + private Channel createChannel(int id, ChannelType type) => new Channel(new APIUser { Id = id }) { Id = id, Name = $"Channel {id}", From ec890cd459ce8f5d48470b37e67cbda562e4787e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Nov 2025 11:33:43 +0100 Subject: [PATCH 209/308] Clear chat state when local user changes Closes https://github.com/ppy/osu/issues/35081. --- osu.Game/Online/Chat/ChannelManager.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index eb5d6d1b9c..aec7928ba8 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -70,6 +70,7 @@ namespace osu.Game.Online.Chat [Resolved] private UserLookupCache users { get; set; } + private readonly IBindable localUser = new Bindable(); private readonly IBindable apiState = new Bindable(); private readonly IBindableList localUserBlocks = new BindableList(); private ScheduledDelegate scheduledAck; @@ -95,6 +96,9 @@ namespace osu.Game.Online.Chat chatClient.PresenceReceived += () => Schedule(initializeChannels); chatClient.RequestPresence(); + localUser.BindTo(api.LocalUser); + localUser.BindValueChanged(userChanged); + apiState.BindTo(api.State); apiState.BindValueChanged(_ => SendAck(), true); @@ -102,6 +106,22 @@ namespace osu.Game.Online.Chat localUserBlocks.BindCollectionChanged((_, args) => Schedule(() => onBlocksChanged(args))); } + private void userChanged(ValueChangedEvent userChange) + { + if (userChange.OldValue?.Equals(userChange.NewValue) == true) + return; + + CurrentChannel.Value = null; + + foreach (var joinedChannel in joinedChannels) + joinedChannel.Joined.Value = false; + + joinedChannels.Clear(); + // additionally clear the history of last joined channels so that the new user can't reopen the old user's channels + // (would likely fail web-side on perms anyway, but why even get that far) + closedChannels.Clear(); + } + /// /// Opens a channel or switches to the channel if already opened. /// From e4975e8d3b33e00dc10196b0ef709381984855f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Nov 2025 12:00:33 +0100 Subject: [PATCH 210/308] Remove unnecessary cast --- osu.Game.Tournament/Models/TournamentBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Models/TournamentBeatmap.cs b/osu.Game.Tournament/Models/TournamentBeatmap.cs index 83c42b793d..72669c0ca7 100644 --- a/osu.Game.Tournament/Models/TournamentBeatmap.cs +++ b/osu.Game.Tournament/Models/TournamentBeatmap.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Models public BeatmapSetOnlineCovers Covers { get; set; } - public APIRuleset Ruleset { get; set; } = new APIRuleset(); + public IRulesetInfo Ruleset { get; set; } = new APIRuleset(); public TournamentBeatmap() { @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Models Covers = beatmap.BeatmapSet?.Covers ?? new BeatmapSetOnlineCovers(); EndTimeObjectCount = beatmap.EndTimeObjectCount; TotalObjectCount = beatmap.TotalObjectCount; - Ruleset = (APIRuleset)beatmap.Ruleset; + Ruleset = beatmap.Ruleset; } public bool Equals(IBeatmapInfo? other) => other is TournamentBeatmap b && this.MatchesOnlineID(b); From 8fb402665e6ff35d706181b866e9060f876c5a8b Mon Sep 17 00:00:00 2001 From: Arpa Date: Mon, 24 Nov 2025 13:08:50 +0200 Subject: [PATCH 211/308] Merge pull request #35698 from ArpaDeveloper/master Fix editor test play autoplay / quick play toggles being usable while pause or resume overlays were showing --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index eedde8b7a4..2c9b97114d 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Screens; @@ -180,10 +181,16 @@ namespace osu.Game.Screens.Edit.GameplayTest switch (e.Action) { case GlobalAction.EditorTestPlayToggleAutoplay: + if (PauseOverlay?.State.Value == Visibility.Visible || DrawableRuleset.ResumeOverlay?.State.Value == Visibility.Visible) + return true; + toggleAutoplay(); return true; case GlobalAction.EditorTestPlayToggleQuickPause: + if (PauseOverlay?.State.Value == Visibility.Visible || DrawableRuleset.ResumeOverlay?.State.Value == Visibility.Visible) + return true; + toggleQuickPause(); return true; From 96de47ac4f8c8e4410cc9f9a58a0f78e8b77ecd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Nov 2025 12:46:30 +0100 Subject: [PATCH 212/308] Fix hover fighting when a `SettingsToolboxGroup`'s child handles hover Addresses https://github.com/ppy/osu/discussions/35772. --- osu.Game/Overlays/SettingsToolboxGroup.cs | 34 ++++++++++------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index f26ed962cb..9090a294b5 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Graphics; @@ -64,6 +63,9 @@ namespace osu.Game.Overlays private Drawable? draggedChild; + private bool? lastMouseInBounds; + private bool mouseInBounds => Contains(inputManager.CurrentState.Mouse.Position); + /// /// Create a new instance. /// @@ -143,20 +145,6 @@ namespace osu.Game.Overlays this.Delay(600).Schedule(updateFadeState); } - protected override bool OnHover(HoverEvent e) - { - updateFadeState(); - updateExpandedState(true); - return false; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateFadeState(); - updateExpandedState(true); - base.OnHoverLost(e); - } - protected override void Update() { base.Update(); @@ -166,10 +154,16 @@ namespace osu.Game.Overlays headerText.Alpha = (float)Interpolation.DampContinuously(headerText.Alpha, headerText.DrawWidth < DrawWidth ? 1 : 0, 40, Time.Elapsed); // Dragged child finished its drag operation. - if (draggedChild != null && inputManager.DraggedDrawable != draggedChild) - { + bool childDragFinished = draggedChild != null && inputManager.DraggedDrawable != draggedChild; + + if (childDragFinished) draggedChild = null; + + if (childDragFinished || lastMouseInBounds != mouseInBounds) + { updateExpandedState(true); + updateFadeState(); + lastMouseInBounds = mouseInBounds; } } @@ -185,7 +179,7 @@ namespace osu.Game.Overlays // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - if (Expanded.Value || IsHovered || draggedChild != null) + if (Expanded.Value || mouseInBounds || draggedChild != null) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; @@ -204,8 +198,8 @@ namespace osu.Game.Overlays { const float fade_duration = 500; - background.FadeTo(IsHovered ? 1 : 0.1f, fade_duration, Easing.OutQuint); - expandButton.FadeTo(IsHovered ? 1 : 0, fade_duration, Easing.OutQuint); + background.FadeTo(mouseInBounds ? 1 : 0.1f, fade_duration, Easing.OutQuint); + expandButton.FadeTo(mouseInBounds ? 1 : 0, fade_duration, Easing.OutQuint); } } } From 9c981a52f865b611530db7318256c696b836ead0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Nov 2025 12:52:57 +0100 Subject: [PATCH 213/308] Fix test failures This is dodgy as hell but `ShortName` is completely derived from `OnlineID` anyway so there should be no valid reason to ever attempt to serialise it anyway. --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 20494a1cbf..cbd8833fe8 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -146,6 +146,7 @@ namespace osu.Game.Online.API.Requests.Responses public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})"; + [JsonIgnore] public string ShortName { get From 855d5dba3cfee04d9e506ca51359a1925e30f066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Nov 2025 13:20:39 +0100 Subject: [PATCH 214/308] Bypass 300ms debounce when requesting local leaderboards in song select RFC. Would probably close https://github.com/ppy/osu/issues/35773. --- osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 8aa3a0516f..6e0ffafa63 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -237,17 +237,19 @@ namespace osu.Game.Screens.SelectV2 SetState(LeaderboardState.Retrieving); + var fetchScope = Scope.Value; + refetchOperation?.Cancel(); refetchOperation = Scheduler.AddDelayed(() => { var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - var fetchSorting = Scope.Value == BeatmapLeaderboardScope.Local ? Sorting.Value : LeaderboardSortMode.Score; + var fetchSorting = fetchScope == BeatmapLeaderboardScope.Local ? Sorting.Value : LeaderboardSortMode.Score; // For now, we forcefully refresh to keep things simple. // In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios // (like returning from gameplay after setting a new score, returning to song select after main menu). - leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null, fetchSorting), + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, fetchScope, FilterBySelectedMods.Value ? mods.Value.ToArray() : null, fetchSorting), forceRefresh: true); if (!initialFetchComplete) @@ -257,7 +259,7 @@ namespace osu.Game.Screens.SelectV2 fetchedScores.BindValueChanged(_ => updateScores(), true); initialFetchComplete = true; } - }, initialFetchComplete ? 300 : 0); + }, initialFetchComplete && fetchScope != BeatmapLeaderboardScope.Local ? 300 : 0); } private void updateScores() From a69b2cd80395cfee1dc99c6d06a7c5388e4780bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Nov 2025 13:38:53 +0100 Subject: [PATCH 215/308] Revert "Expand group that current selection resides in when moving mouse to left side of song select" Reverts https://github.com/ppy/osu/pull/35184 as per https://github.com/ppy/osu/discussions/35683#discussioncomment-15034835. --- .../SongSelectV2/TestSceneSongSelect.cs | 36 ------------------- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 12 ------- osu.Game/Screens/SelectV2/SongSelect.cs | 6 +--- 3 files changed, 1 insertion(+), 53 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index e4f05b2e49..a480e51adf 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -22,7 +22,6 @@ using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; @@ -144,41 +143,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 void onScreenPushed(IScreen lastScreen, IScreen newScreen) => screensPushed.Add(lastScreen); } - [TestCase(true)] - [TestCase(false)] - public void TestHoveringLeftSideReexpandsGroupSelectionIsIn(bool mouseOverPanel) - { - ImportBeatmapForRuleset(0); - - LoadSongSelect(); - SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); - - AddStep("move mouse to carousel", () => InputManager.MoveMouseTo(Carousel)); - - AddUntilStep("expanded group is below 1 star", - () => (Carousel.ChildrenOfType().SingleOrDefault(p => p.Expanded.Value)?.Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars, - () => Is.EqualTo(0)); - - AddStep("select next group", () => - { - InputManager.PressKey(Key.ShiftLeft); - InputManager.Key(Key.Right); - InputManager.ReleaseKey(Key.ShiftLeft); - }); - AddUntilStep("expanded group is 3 star", - () => (Carousel.ChildrenOfType().SingleOrDefault(p => p.Expanded.Value)?.Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars, - () => Is.EqualTo(3)); - - if (mouseOverPanel) - AddStep("move mouse over left panel", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); - else - AddStep("move mouse to left side container", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); - - AddUntilStep("expanded group is below 1 star", - () => (Carousel.ChildrenOfType().Single(p => p.Expanded.Value).Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars, - () => Is.EqualTo(0)); - } - #region Hotkeys [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index ae1c8eb878..aacebe4e88 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -693,18 +693,6 @@ namespace osu.Game.Screens.SelectV2 } } - public void ExpandGroupForCurrentSelection() - { - if (CurrentGroupedBeatmap?.Group == null) - return; - - if (CheckModelEquality(ExpandedGroup, CurrentGroupedBeatmap.Group)) - return; - - if (grouping.ItemMap.TryGetValue(CurrentGroupedBeatmap.Group, out var groupItem)) - Activate(groupItem.item); - } - protected override double? GetScrollTarget() { double? target = base.GetScrollTarget(); diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index e8843876d3..1b66bd5600 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -214,11 +214,7 @@ namespace osu.Game.Screens.SelectV2 // Pad enough to only reset scroll when well into the left wedge areas. Padding = new MarginPadding { Right = 40 }, RelativeSizeAxes = Axes.Both, - Child = new Select.SongSelect.LeftSideInteractionContainer(() => - { - carousel.ExpandGroupForCurrentSelection(); - carousel.ScrollToSelection(); - }) + Child = new Select.SongSelect.LeftSideInteractionContainer(() => carousel.ScrollToSelection()) { RelativeSizeAxes = Axes.Both, }, From d59e9572d2f075c5c4e063ff0dc7aa5bd04d4130 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 00:55:22 +0900 Subject: [PATCH 216/308] Add missing padding around countdown settings button --- .../Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index a91b844900..f73983217f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -50,11 +50,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match ColumnDimensions = new[] { new Dimension(), + new Dimension(GridSizeMode.Absolute, 5), new Dimension(GridSizeMode.AutoSize) }, Content = new[] { - new Drawable[] + new Drawable?[] { readyButton = new MultiplayerReadyButton { @@ -62,6 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Size = Vector2.One, Action = onReadyButtonClick, }, + null, countdownButton = new MultiplayerCountdownButton { RelativeSizeAxes = Axes.Y, From b0762fc8ec2f3bf4c3fd983d7964aecb34b48e9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 00:55:42 +0900 Subject: [PATCH 217/308] Reduce abstractions of rounded button --- .../UserInterface/DangerousRoundedButton.cs | 2 +- .../Settings/Sections/Input/KeyBindingRow.cs | 32 +++++++------------ osu.Game/Rulesets/Edit/ExpandableButton.cs | 2 +- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs index 39ef7924b9..cb9250c15c 100644 --- a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs +++ b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs @@ -6,7 +6,7 @@ using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Graphics.UserInterface { - public partial class DangerousRoundedButton : RoundedButton + public sealed partial class DangerousRoundedButton : RoundedButton { [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 083c678176..c9ef6ef891 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -191,8 +191,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input Spacing = new Vector2(5), Children = new Drawable[] { - new CancelButton { Action = () => finalise(false) }, - new ClearButton { Action = clear }, + new RoundedButton + { + Text = CommonStrings.ButtonsCancel, + Size = new Vector2(80, 20), + Action = () => finalise(false) + }, + new DangerousRoundedButton + { + Text = CommonStrings.ButtonsClear, + Size = new Vector2(80, 20), + Action = clear + }, }, }, new HoverClickSounds() @@ -538,23 +548,5 @@ namespace osu.Game.Overlays.Settings.Sections.Input { isDefault.Value = KeyBindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); } - - private partial class CancelButton : RoundedButton - { - public CancelButton() - { - Text = CommonStrings.ButtonsCancel; - Size = new Vector2(80, 20); - } - } - - public partial class ClearButton : DangerousRoundedButton - { - public ClearButton() - { - Text = CommonStrings.ButtonsClear; - Size = new Vector2(80, 20); - } - } } } diff --git a/osu.Game/Rulesets/Edit/ExpandableButton.cs b/osu.Game/Rulesets/Edit/ExpandableButton.cs index 9139802d68..d1f855a8ad 100644 --- a/osu.Game/Rulesets/Edit/ExpandableButton.cs +++ b/osu.Game/Rulesets/Edit/ExpandableButton.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Rulesets.Edit { - public partial class ExpandableButton : RoundedButton, IExpandable + public sealed partial class ExpandableButton : RoundedButton, IExpandable { private float actualHeight; From 64668eafb96c08dcfad1f02e50053b688613eb6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 00:55:53 +0900 Subject: [PATCH 218/308] Adjust some more visual metrics to feel better --- osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs | 3 ++- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 3 ++- osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs | 3 ++- osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index 48d225de41..67a4cf6890 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -56,7 +56,8 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.Centre, Anchor = Anchor.Centre, RelativeSizeAxes = Axes.Both, - CornerRadius = 5, + CornerRadius = 10, + CornerExponent = 2.5f, Masking = true, EdgeEffect = new EdgeEffectParameters { diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index e0179f8bc4..ae5501a3dd 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -357,7 +357,8 @@ namespace osu.Game.Graphics.UserInterface Icon = FontAwesome.Solid.ChevronDown, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Size = new Vector2(16), + Size = new Vector2(10), + Margin = new MarginPadding { Right = 2 }, }, } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index a0348fa27a..90e12b128c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -178,7 +178,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 Size = new Vector2(70); Masking = true; - CornerRadius = 35; + CornerRadius = 10; + CornerExponent = 2.5f; Action = this.ShowPopover; Children = new Drawable[] diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 01c495ae30..faabb80299 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -52,6 +52,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 // This doesn't match the latest design spec (should be 5) but is an in-between that feels right to the eye // until we move everything over to Form controls. Content.CornerRadius = 10; + Content.CornerExponent = 2.5f; Add(Triangles = new TrianglesV2 { From 52af905237562a3ea9d88017cba0a90d3289d46a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 01:06:29 +0900 Subject: [PATCH 219/308] Hide full installation section on non-desktop platforms --- .../Sections/General/InstallationSettings.cs | 27 +++++++------------ .../Settings/Sections/GeneralSection.cs | 4 ++- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs b/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs index 3aaeadd158..68f3ba9b17 100644 --- a/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/InstallationSettings.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; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Framework.Platform; @@ -21,24 +20,18 @@ namespace osu.Game.Overlays.Settings.Sections.General [BackgroundDependencyLoader] private void load(Storage storage) { - bool isDesktop = RuntimeInfo.IsDesktop; - - // Loosely update-related maintenance buttons. - if (isDesktop) + Add(new SettingsButton { - Add(new SettingsButton - { - Text = GeneralSettingsStrings.OpenOsuFolder, - Keywords = new[] { @"logs", @"files", @"access", "directory" }, - Action = () => storage.PresentExternally(), - }); + Text = GeneralSettingsStrings.OpenOsuFolder, + Keywords = new[] { @"logs", @"files", @"access", "directory" }, + Action = () => storage.PresentExternally(), + }); - Add(new DangerousSettingsButton - { - Text = GeneralSettingsStrings.ChangeFolderLocation, - Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) - }); - } + Add(new DangerousSettingsButton + { + Text = GeneralSettingsStrings.ChangeFolderLocation, + Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) + }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index 18e650d70d..7136de1327 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.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 osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -28,7 +29,8 @@ namespace osu.Game.Overlays.Settings.Sections Add(new LanguageSettings()); if (updateManager?.CanCheckForUpdate == true) Add(new UpdateSettings()); - Add(new InstallationSettings()); + if (RuntimeInfo.IsDesktop) + Add(new InstallationSettings()); } } } From 1d353ef63777f2e3983691d88db11a26cc7a0a90 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 24 Nov 2025 11:06:01 -0800 Subject: [PATCH 220/308] Revert showing toast on editor timestamp clipboard --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 7f6ddcd54c..195625dcde 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; @@ -21,8 +22,8 @@ namespace osu.Game.Screens.Edit.Compose { public partial class ComposeScreen : EditorScreenWithTimeline, IGameplaySettings { - [Resolved(canBeNull: true)] - private OsuGame game { get; set; } + [Resolved] + private Clipboard hostClipboard { get; set; } = null!; [Resolved] private EditorClock clock { get; set; } @@ -137,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose // regardless of whether anything was even selected at all. // UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory. // note that this means that `getTimestamp()` must handle no-selection case, too. - game?.CopyToClipboard(getTimestamp()); + hostClipboard.SetText(getTimestamp()); if (CanCopy.Value) clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); From f0f4e7c7a549288fa56ec57e690aa27b631c7b8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 14:50:25 +0900 Subject: [PATCH 221/308] 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 da412f2709..aa66667887 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 45567f19b72cc12a0461f6c379730be4fc44d60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Nov 2025 07:46:29 +0100 Subject: [PATCH 222/308] Fix test not compiling --- .../Visual/Settings/TestSceneKeyBindingPanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 4cad283833..8e671f331d 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("schedule button clicks", () => { - var clearButton = firstRow.ChildrenOfType().Single(); + var clearButton = firstRow.ChildrenOfType().Single(); InputManager.MoveMouseTo(clearButton); @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Settings { AddStep("click clear button", () => { - var clearButton = multiBindingRow.ChildrenOfType().Single(); + var clearButton = multiBindingRow.ChildrenOfType().Single(); InputManager.MoveMouseTo(clearButton); InputManager.Click(MouseButton.Left); @@ -386,7 +386,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("clear binding", () => { var row = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == "Left (centre)")); - row.ChildrenOfType().Single().TriggerClick(); + row.ChildrenOfType().Single().TriggerClick(); }); scrollToAndStartBinding("Left (rim)"); AddStep("bind M1", () => InputManager.Click(MouseButton.Left)); @@ -462,7 +462,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("clear binding", () => { var row = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == "Left (centre)")); - row.ChildrenOfType().Single().TriggerClick(); + row.ChildrenOfType().Single().TriggerClick(); }); } From c968981697e74826682ac868e737daf4f8e9a476 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 15:51:47 +0900 Subject: [PATCH 223/308] Fix quick retry/exit overlay volume dimming potentially sticking at results Closes #35737. --- osu.Game/Screens/Play/Player.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 6158118c78..38e3fcd38d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -320,7 +320,7 @@ namespace osu.Game.Screens.Play OnRetry = Configuration.AllowUserInteraction ? () => Restart() : null, OnQuit = () => PerformExitWithConfirmation(), }, - new HotkeyExitOverlay + exitOverlay = new HotkeyExitOverlay { Action = () => { @@ -338,7 +338,7 @@ namespace osu.Game.Screens.Play { rulesetSkinProvider.AddRange(new Drawable[] { - new HotkeyRetryOverlay + retryOverlay = new HotkeyRetryOverlay { Action = () => { @@ -1033,6 +1033,9 @@ namespace osu.Game.Screens.Play private double? lastPauseActionTime; + private HotkeyRetryOverlay retryOverlay; + private HotkeyExitOverlay exitOverlay; + protected bool PauseCooldownActive => PlayingState.Value == LocalUserPlayingState.Playing && lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + PauseCooldownDuration; @@ -1171,6 +1174,10 @@ namespace osu.Game.Screens.Play { screenSuspension?.RemoveAndDisposeImmediately(); + // If these are not disposed, audio volume dimming can get stuck. + retryOverlay?.RemoveAndDisposeImmediately(); + exitOverlay?.RemoveAndDisposeImmediately(); + fadeOut(); base.OnSuspending(e); } From 0786e619f1fccebbf99204ac8d94afb99361bbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Nov 2025 07:51:43 +0100 Subject: [PATCH 224/308] Leave note about lack of toast for posterity --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 195625dcde..00690c617e 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -138,6 +138,8 @@ namespace osu.Game.Screens.Edit.Compose // regardless of whether anything was even selected at all. // UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory. // note that this means that `getTimestamp()` must handle no-selection case, too. + // additionally, note we're intentionally not using `OsuGame.CopyToClipboard()` + // because we do not want toasts to pop up on every Ctrl-C press - it'd be disruptive to mappers. hostClipboard.SetText(getTimestamp()); if (CanCopy.Value) From 79bfe7880af8243b4d5c7c83d81236c872a17314 Mon Sep 17 00:00:00 2001 From: Chirag Mahesh Date: Tue, 25 Nov 2025 06:56:18 +0000 Subject: [PATCH 225/308] Move LocalSpacePosition calculation until the time of render Would address #35734 --- .../UI/Cursor/CursorTrail.cs | 34 ++++---- .../TestSceneGameplayCursorSizeChange.cs | 85 +++++++++++++++++++ 2 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneGameplayCursorSizeChange.cs diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 1c2d69fa00..8a45475f0f 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -164,20 +164,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor return base.OnMouseMove(e); } - protected void AddTrail(Vector2 position) + protected void AddTrail(Vector2 screenSpacePosition) { - position = ToLocalSpace(position); - if (InterpolateMovements) { if (!lastPosition.HasValue) { - lastPosition = position; + lastPosition = screenSpacePosition; resampler.AddPosition(lastPosition.Value); return; } - foreach (Vector2 pos2 in resampler.AddPosition(position)) + foreach (Vector2 pos2 in resampler.AddPosition(screenSpacePosition)) { Trace.Assert(lastPosition.HasValue); @@ -198,14 +196,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } else { - lastPosition = position; + lastPosition = screenSpacePosition; addPart(lastPosition.Value); } } - private void addPart(Vector2 localSpacePosition) + private void addPart(Vector2 screenSpacePosition) { - parts[currentIndex].Position = localSpacePosition; + parts[currentIndex].ScreenSpacePosition = screenSpacePosition; parts[currentIndex].Time = time + 1; parts[currentIndex].Scale = NewPartScale; ++parts[currentIndex].InvalidationID; @@ -217,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private struct TrailPart { - public Vector2 Position; + public Vector2 ScreenSpacePosition; public float Time; public Vector2 Scale; public long InvalidationID; @@ -304,11 +302,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (time - part.Time >= 1) continue; + Vector2 localSpacePosition = Vector2Extensions.Transform(part.ScreenSpacePosition, DrawInfo.MatrixInverse); + vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), - part.Position, sin, cos), + new Vector2(localSpacePosition.X - texture.DisplayWidth * originPosition.X * part.Scale.X, localSpacePosition.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + localSpacePosition, sin, cos), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -318,8 +318,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, - part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), + new Vector2(localSpacePosition.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, + localSpacePosition.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), localSpacePosition, sin, cos), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -329,8 +329,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), - part.Position, sin, cos), + new Vector2(localSpacePosition.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, localSpacePosition.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + localSpacePosition, sin, cos), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -340,8 +340,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), - part.Position, sin, cos), + new Vector2(localSpacePosition.X - texture.DisplayWidth * originPosition.X * part.Scale.X, localSpacePosition.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + localSpacePosition, sin, cos), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayCursorSizeChange.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayCursorSizeChange.cs new file mode 100644 index 0000000000..22b3a8e378 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayCursorSizeChange.cs @@ -0,0 +1,85 @@ +// 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.Screens; +using osu.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Skinning; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneGameplayCursorSizeChange : OsuPlayerTestScene + { + [Resolved] + private SkinManager? skins { get; set; } + + protected new PausePlayer Player => (PausePlayer)base.Player; + + [BackgroundDependencyLoader] + private void load() + { + if (skins != null) skins.CurrentSkinInfo.Value = skins.DefaultClassicSkin.SkinInfo; + } + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("resume player", () => Player.GameplayClockContainer.Start()); + AddUntilStep("clock running", () => Player.GameplayClockContainer.IsRunning); + } + + [Test] + public void TestChangeCursorSize() + { + AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); + AddStep("move cursor to top left", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft)); + AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); + AddStep("move cursor to top right", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopRight)); + AddStep("press escape", () => InputManager.Key(Key.Escape)); + + for (float cursorSize = 0.4f; cursorSize <= 1.6f + 0.001f; cursorSize += 0.4f) + { + AddWaitStep("wait 2 seconds", 2); + float newCursorSize = cursorSize; + AddStep($"gameplay cursor size: {newCursorSize:F1}", () => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, newCursorSize)); + } + } + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PausePlayer(); + + protected partial class PausePlayer : TestPlayer + { + public double LastPauseTime { get; private set; } + public double LastResumeTime { get; private set; } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + GameplayClockContainer.Stop(); + } + + private bool? isRunning; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (GameplayClockContainer.IsRunning != isRunning) + { + isRunning = GameplayClockContainer.IsRunning; + + if (isRunning.Value) + LastResumeTime = GameplayClockContainer.CurrentTime; + else + LastPauseTime = GameplayClockContainer.CurrentTime; + } + } + } + } +} From 2c9fc32756e543113f72042e6eb9ec454c43541c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 16:39:39 +0900 Subject: [PATCH 226/308] Assert that player suspension is final --- osu.Game/Screens/Play/Player.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 38e3fcd38d..9988bbcd93 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1172,6 +1172,8 @@ namespace osu.Game.Screens.Play public override void OnSuspending(ScreenTransitionEvent e) { + Debug.Assert(!ValidForResume); + screenSuspension?.RemoveAndDisposeImmediately(); // If these are not disposed, audio volume dimming can get stuck. From 545b13c3fb72a9e4f10bbcee9f3fbda7bf58e6d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 16:45:52 +0900 Subject: [PATCH 227/308] Show self in online users I don't see a reason to hide self. I kinda expect to be able to see that I'm online. --- osu.Game/Online/Metadata/OnlineMetadataClient.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 75b0187388..052b742beb 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -235,15 +235,13 @@ namespace osu.Game.Online.Metadata { if (userId == api.LocalUser.Value.OnlineID) localUserPresence = presence.Value; - else - userPresences[userId] = presence.Value; + userPresences[userId] = presence.Value; } else { if (userId == api.LocalUser.Value.OnlineID) localUserPresence = default; - else - userPresences.Remove(userId); + userPresences.Remove(userId); } }); From 26c50b874cb782090eba5c197a03a4004f9cdcf1 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Tue, 25 Nov 2025 09:31:41 +0100 Subject: [PATCH 228/308] update osu!taiko drain thresholds see change in https://github.com/ppy/osu-wiki/pull/13958/commits/25169ccbe6f5b399a8a659623d622b1332d043db --- .../Edit/Checks/CheckTaikoLowestDiffDrainTime.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs index 8ef911c18e..30717d7ee9 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoLowestDiffDrainTime.cs @@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Checks protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds() { // See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21taiko#general - yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Muzukashii"); - yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Oni"); - yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Inner Oni"); + yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Muzukashii"); + yield return (DifficultyRating.Insane, new TimeSpan(0, 3, 15).TotalMilliseconds, "Oni"); + yield return (DifficultyRating.Expert, new TimeSpan(0, 4, 0).TotalMilliseconds, "Inner Oni"); } } } From f6a6c9f8859205b1caa19fc4fc1f39f42cceb9e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 18:48:03 +0900 Subject: [PATCH 229/308] Fix failing test --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 3021589cdb..1365d95a55 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -266,7 +266,11 @@ namespace osu.Game.Tests.Visual.Background FadeAccessibleResults results = null; - AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(TestResources.CreateTestScoreInfo()))); + AddStep("Transition to Results", () => + { + player.ValidForResume = false; + player.Push(results = new FadeAccessibleResults(TestResources.CreateTestScoreInfo())); + }); AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); From 0d9a50e839ce8e8adab969112cca162a4e72bcaf Mon Sep 17 00:00:00 2001 From: maarvin Date: Wed, 26 Nov 2025 14:11:40 +0100 Subject: [PATCH 230/308] Quickplay: Update top level layout to match designs (#35791) * Adjust top level matchmaking screen layout * Adjust colours in StageDisplay * Fix flipped animation in StageDisplay * Adjust colours in CurrentRoundDisplay * Fade out stage segments as they approach the left screen border * Remove redundant `OfType()` call * Soften banner shadow Co-authored-by: marvin --------- Co-authored-by: Dan Balasescu --- .../Match/ScreenMatchmaking.ScreenStack.cs | 7 +- .../Matchmaking/Match/ScreenMatchmaking.cs | 26 ++---- .../Match/StageDisplay.StageSegment.cs | 9 +- .../Matchmaking/Match/StageDisplay.cs | 89 ++++++++++++------- 4 files changed, 64 insertions(+), 67 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs index 4d5a7099c4..55bbcf7ce5 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -36,10 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(6) - { - Bottom = StageDisplay.HEIGHT + 6, - }, + Padding = new MarginPadding { Top = StageDisplay.HEIGHT, Bottom = 6 }, Children = new Drawable[] { screenStack = new Framework.Screens.ScreenStack(), @@ -51,8 +48,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match }, new StageDisplay { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X } }; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index a809270574..e3319d4c94 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -13,7 +13,6 @@ 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.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; @@ -57,8 +56,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public override bool ShowFooter => true; - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); protected override BackgroundScreen CreateBackground() => new MatchmakingBackgroundScreen(colourProvider); @@ -124,7 +123,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); @@ -143,8 +142,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Top = row_padding, + Horizontal = HORIZONTAL_OVERFLOW_PADDING, }, RowDimensions = new[] { @@ -155,21 +153,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Content = new Drawable[]?[] { [ - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }, - new ScreenStack(), - } - } + new ScreenStack(), ], null, [ diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs index 7e3b7d4468..50806e6b27 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs @@ -7,7 +7,6 @@ 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; @@ -92,11 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match new Box { RelativeSizeAxes = Axes.Both, - Colour = - ColourInfo.GradientVertical( - colourProvider.Dark2, - colourProvider.Dark1 - ), + Colour = colourProvider.Dark3, }, progressBar = new Box { @@ -104,7 +99,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match EdgeSmoothness = new Vector2(1), RelativeSizeAxes = Axes.Both, Width = 0, - Colour = colourProvider.Dark3, + Colour = colourProvider.Colour3, }, new OsuSpriteText { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs index b45e8054a0..53cdc6d85e 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs @@ -9,7 +9,9 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -17,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { @@ -31,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private const int round_count = 5; private OsuScrollContainer scroll = null!; - private FillFlowContainer flow = null!; + private FillFlowContainer flow = null!; private CurrentRoundDisplay roundDisplay = null!; @@ -46,10 +49,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { InternalChildren = new Drawable[] { - new Box + new BufferedContainer(cachedFrameBuffer: true) { - Colour = colourProvider.Dark6, RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.White, Color4.Transparent), + Alpha = 0.8f, + Child = new Box + { + Colour = ColourInfo.GradientHorizontal(colourProvider.Dark6, colourProvider.Dark6.Opacity(0.5f)), + RelativeSizeAxes = Axes.Both, + } }, new Container { @@ -63,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match ClampExtension = 0, RelativeSizeAxes = Axes.X, Height = HEIGHT, - Child = flow = new FillFlowContainer + Child = flow = new FillFlowContainer { Padding = new MarginPadding { Horizontal = 2000 }, AutoSizeAxes = Axes.Both, @@ -84,15 +93,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match 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, @@ -119,7 +119,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match protected override void Update() { base.Update(); - var bubble = flow.OfType().FirstOrDefault(b => b.Active); + var bubble = flow.FirstOrDefault(b => b.Active); if (bubble != null) { @@ -128,6 +128,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + foreach (var segment in flow) + { + if (segment.Active) + return; + + float offset = segment.ToSpaceOfOtherDrawable(Vector2.Zero, this).X; + + segment.Alpha = float.Clamp(offset / 300, 0.1f, 0.5f); + } + } + private partial class StageScrollContainer : OsuScrollContainer { public override bool HandlePositionalInput => false; @@ -158,47 +173,55 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { Size = new Vector2(76); + progress = new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.Colour2, + InnerRadius = 0.1f, + RelativeSizeAxes = Axes.Both, + RoundedCaps = true, + }; InternalChildren = new Drawable[] { new Circle { - Colour = ColourInfo.GradientVertical( - colours.Dark2, - colours.Dark4 - ), + Colour = colours.Dark4, RelativeSizeAxes = Axes.Both, }, - progress = new CircularProgress + (progress = new CircularProgress { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Colour = ColourInfo.GradientVertical( - colours.Light1, - colours.Dark2 - ), + Colour = colours.Colour2, InnerRadius = 0.1f, - RelativeSizeAxes = Axes.Both, - }, + Size = Size, + }).WithEffect(new GlowEffect + { + Colour = colours.Colour2, + BlurSigma = new Vector2(10), + Strength = 2f, + Placement = EffectPlacement.Behind, + PadExtent = true, + }), innerCircle = new Circle { Alpha = 0.2f, Blending = BlendingParameters.Additive, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Colour = ColourInfo.GradientVertical( - colours.Dark1, - colours.Dark2 - ), + Colour = colours.Dark1, Scale = new Vector2(0.9f), RelativeSizeAxes = Axes.Both, }, new OsuSpriteText { - Y = 10, + Y = 13, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.Style.Caption2, Text = "Round", + Colour = colours.Content2 }, text = new OsuSpriteText { @@ -245,10 +268,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match round = value.Value; this.ScaleTo(6, 1000, Easing.OutPow10) - .MoveToY(-300, 1000, Easing.OutPow10) + .MoveToY(300, 1000, Easing.OutPow10) .Then() - .MoveToY(0, 500, Easing.InQuart) - .ScaleTo(1, 500, Easing.InQuart); + .MoveToY(0, 500, new CubicBezierEasingFunction(0.8, 0, 0.6, 1)) + .ScaleTo(1, 500, new CubicBezierEasingFunction(0.8, 0, 0.6, 1)); swishChannel = swishSample?.GetChannel(); From 75df8e363903a2886acddfc6743cac00c9145771 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Nov 2025 15:00:14 +0900 Subject: [PATCH 231/308] Add failing tests --- .../Visual/Online/TestSceneDrawableChannel.cs | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index 6a077708e3..7c6cbb66cb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -146,5 +146,164 @@ namespace osu.Game.Tests.Visual.Online checkCount++; }, 10); } + + [Test] + public void TestAlternatingBackgroundDoesNotChangeAtMaxHistory() + { + AddStep("fill up the channel", () => + { + for (int i = 0; i < Channel.MAX_HISTORY; i++) + { + channel.AddNewMessages(new Message + { + ChannelId = channel.Id, + Content = $"Message {i}", + Timestamp = DateTimeOffset.Now, + Sender = new APIUser + { + Id = 3, + Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N") + } + }); + } + }); + + AddUntilStep($"{Channel.MAX_HISTORY} messages present", () => drawableChannel.ChildrenOfType().Count(), () => Is.EqualTo(Channel.MAX_HISTORY)); + + ChatLine? lastLine = null; + bool lastLineAlternatingBackground = false; + + AddStep("grab last line", () => + { + lastLine = drawableChannel.ChildrenOfType().Last(); + lastLineAlternatingBackground = lastLine.AlternatingBackground; + }); + + AddStep("add another message", () => channel.AddNewMessages(new Message + { + ChannelId = channel.Id, + Content = "One final message", + Timestamp = DateTimeOffset.Now, + Sender = new APIUser + { + Id = 3, + Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N") + } + })); + + AddAssert("second-last message has same background", () => lastLine!.AlternatingBackground, () => Is.EqualTo(lastLineAlternatingBackground)); + } + + [Test] + public void TestAlternatingBackgroundUpdatedOnRemoval() + { + AddStep("add 3 messages", () => + { + for (int i = 0; i < 3; i++) + { + channel.AddNewMessages(new Message + { + ChannelId = channel.Id, + Content = $"Message {i}", + Timestamp = DateTimeOffset.Now, + Sender = new APIUser + { + Id = i, + Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N") + } + }); + } + }); + + AddUntilStep("3 messages present", () => drawableChannel.ChildrenOfType().Count(), () => Is.EqualTo(3)); + assertAlternatingBackground(0, false); + assertAlternatingBackground(1, true); + assertAlternatingBackground(2, false); + + AddStep("remove middle message", () => channel.RemoveMessagesFromUser(1)); + AddUntilStep("2 messages present", () => drawableChannel.ChildrenOfType().Count(), () => Is.EqualTo(2)); + assertAlternatingBackground(0, true); + assertAlternatingBackground(1, false); + + void assertAlternatingBackground(int lineIndex, bool shouldBeAlternating) + => AddAssert($"line {lineIndex} {(shouldBeAlternating ? "has" : "does not have")} alternating background", + () => drawableChannel.ChildrenOfType().ElementAt(lineIndex).AlternatingBackground, + () => Is.EqualTo(shouldBeAlternating)); + } + + [Test] + public void TestTimestampsUpdateOnRemoval() + { + AddStep("add 3 messages", () => + { + channel.AddNewMessages(new Message + { + ChannelId = channel.Id, + Content = "Message 0", + Timestamp = new DateTimeOffset(2022, 11, 21, 20, 0, 0, TimeSpan.Zero), + Sender = new APIUser + { + Id = 0, + Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N") + } + }, + new Message + { + ChannelId = channel.Id, + Content = "Message 1", + Timestamp = new DateTimeOffset(2022, 11, 21, 20, 0, 0, TimeSpan.Zero).AddSeconds(1), + Sender = new APIUser + { + Id = 1, + Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N") + } + }, + new Message + { + ChannelId = channel.Id, + Content = "Message 2", + Timestamp = new DateTimeOffset(2022, 11, 21, 20, 0, 0, TimeSpan.Zero).AddMinutes(1), + Sender = new APIUser + { + Id = 2, + Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N") + } + }, + new Message + { + ChannelId = channel.Id, + Content = "Message 3", + Timestamp = new DateTimeOffset(2022, 11, 21, 20, 0, 0, TimeSpan.Zero).AddMinutes(1).AddSeconds(1), + Sender = new APIUser + { + Id = 3, + Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N") + } + } + ); + }); + + AddUntilStep("4 messages present", () => drawableChannel.ChildrenOfType().Count(), () => Is.EqualTo(4)); + assertTimestamp(0, true); + assertTimestamp(1, false); + assertTimestamp(2, true); + assertTimestamp(3, false); + + AddStep("remove message 0", () => channel.RemoveMessagesFromUser(0)); + AddUntilStep("3 messages present", () => drawableChannel.ChildrenOfType().Count(), () => Is.EqualTo(3)); + assertTimestamp(0, true); + assertTimestamp(1, true); + assertTimestamp(2, false); + + AddStep("remove message 2", () => channel.RemoveMessagesFromUser(2)); + AddUntilStep("2 messages present", () => drawableChannel.ChildrenOfType().Count(), () => Is.EqualTo(2)); + assertTimestamp(0, true); + assertTimestamp(1, true); + + void assertTimestamp(int lineIndex, bool shouldHaveTimestamp) + => AddAssert($"line {lineIndex} {(shouldHaveTimestamp ? "has" : "does not have")} timestamp", + () => drawableChannel.ChildrenOfType().ElementAt(lineIndex).RequiresTimestamp, + () => Is.EqualTo(shouldHaveTimestamp)); + } } } From ded8aaecfdd82430781d3f9f6094df9e636710c4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Nov 2025 15:13:25 +0900 Subject: [PATCH 232/308] Fix chat lines flipping colours at maximum history --- osu.Game/Overlays/Chat/DrawableChannel.cs | 72 ++++++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index ad327f4b28..92b5b4b082 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -85,16 +85,13 @@ namespace osu.Game.Overlays.Chat long? lastMinutes = null; - for (int i = 0; i < ChatLineFlow.Count; i++) + foreach (var line in chatLines) { - if (ChatLineFlow[i] is ChatLine chatline) - { - long minutes = chatline.Message.Timestamp.ToUnixTimeSeconds() / 60; + long minutes = line.Message.Timestamp.ToUnixTimeSeconds() / 60; - chatline.AlternatingBackground = i % 2 == 0; - chatline.RequiresTimestamp = minutes != lastMinutes; - lastMinutes = minutes; - } + line.RequiresTimestamp = minutes != lastMinutes; + + lastMinutes = minutes; } } @@ -145,19 +142,28 @@ namespace osu.Game.Overlays.Chat // Add up to last Channel.MAX_HISTORY messages var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); - Message lastMessage = chatLines.LastOrDefault()?.Message; + ChatLine lastLine = chatLines.LastOrDefault(); + Message lastMessage = lastLine?.Message; foreach (var message in displayMessages) { addDaySeparatorIfRequired(lastMessage, message); - var chatLine = CreateChatLine(message); + ChatLine line = CreateChatLine(message); - if (chatLine != null) - { - ChatLineFlow.Add(chatLine); - lastMessage = message; - } + if (line == null) + continue; + + long minutes = line.Message.Timestamp.ToUnixTimeSeconds() / 60; + long? lastMinutes = lastLine?.Message.Timestamp.ToUnixTimeSeconds() / 60; + + line.AlternatingBackground = lastLine?.AlternatingBackground == false; + line.RequiresTimestamp = minutes != lastMinutes; + + ChatLineFlow.Add(line); + + lastMessage = message; + lastLine = line; } var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray(); @@ -232,7 +238,41 @@ namespace osu.Game.Overlays.Chat private void messageRemoved(Message removed) => Schedule(() => { - chatLines.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); + const double fade_time = 600; + + ChatLine removedLine = chatLines.FirstOrDefault(c => c.Message == removed); + + if (removedLine == null) + return; + + removedLine.FadeColour(Color4.Red, 400).FadeOut(fade_time).Expire(); + + // Resolve new colours and timestamps resulting from the removal. + this.Delay(fade_time).Schedule(() => + { + ChatLine lastLine = null; + + // Preserve the colours of most-recent messages while updating the ones upwards in the list. + foreach (var line in chatLines.Reverse().Except([removedLine])) + { + if (lastLine != null) + line.AlternatingBackground = !lastLine.AlternatingBackground; + + lastLine = line; + } + + lastLine = null; + + // Timestamps may migrate to more recent messages. + foreach (var line in chatLines.Except([removedLine])) + { + long minutes = line.Message.Timestamp.ToUnixTimeSeconds() / 60; + long? lastMinutes = lastLine?.Message.Timestamp.ToUnixTimeSeconds() / 60; + line.RequiresTimestamp = minutes != lastMinutes; + + lastLine = line; + } + }); }); private IEnumerable chatLines => ChatLineFlow.Children.OfType(); From 1e43509e4a3594f24e441efa8a04971de062f091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Nov 2025 09:37:40 +0100 Subject: [PATCH 233/308] Fix formatting --- osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index 7c6cbb66cb..b475071f6e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -236,7 +236,8 @@ namespace osu.Game.Tests.Visual.Online { AddStep("add 3 messages", () => { - channel.AddNewMessages(new Message + channel.AddNewMessages( + new Message { ChannelId = channel.Id, Content = "Message 0", From db50019f3118ba034da1ed6d2163cf64c6fa3680 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Nov 2025 18:25:32 +0900 Subject: [PATCH 234/308] Display quick play pool name as sub-heading --- .../TestSceneMatchmakingPoolSelector.cs | 10 ++--- .../Matchmaking/Queue/PoolSelector.cs | 39 +++++++++++++------ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs index c05614e9a4..bd3b75f1b8 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs @@ -22,11 +22,11 @@ namespace osu.Game.Tests.Visual.Matchmaking { Value = [ - new MatchmakingPool { Id = 0, RulesetId = 0, Name = "osu!" }, - new MatchmakingPool { Id = 1, RulesetId = 1, Name = "osu!taiko" }, - new MatchmakingPool { Id = 2, RulesetId = 2, Name = "osu!catch" }, - new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4, Name = "osu!mania (4k)" }, - new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7, Name = "osu!mania (7k)" }, + new MatchmakingPool { Id = 0, RulesetId = 0, Name = "Free-for-all" }, + new MatchmakingPool { Id = 1, RulesetId = 1, Name = "1v1" }, + new MatchmakingPool { Id = 2, RulesetId = 2, Name = "1v1" }, + new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4, Name = "1v1" }, + new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7, Name = "1v1" }, ] } }); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs index 1e6dd0f231..a89700ffe3 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs @@ -92,7 +92,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue private partial class SelectorButton : OsuAnimatedButton { - public static readonly Vector2 SIZE = new Vector2(84, 64); + public static readonly Vector2 SIZE = new Vector2(84, 78); public bool IsSelected => SelectedPool.Value?.Equals(pool) == true; @@ -106,8 +106,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue private Box flashLayer = null!; - private OsuSpriteText text = null!; - public SelectorButton(MatchmakingPool pool) : base(HoverSampleSet.ButtonSidebar) { @@ -123,6 +121,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue Content.CornerRadius = 16; Content.CornerExponent = 10; + Ruleset? rulesetInstance = rulesetStore.GetRuleset(pool.RulesetId)?.CreateInstance(); + + string rulesetName = rulesetInstance?.Description ?? string.Empty; + if (pool.Variant != 0) + rulesetName += $" {pool.Variant}K"; + Children = new Drawable[] { new Box @@ -156,13 +160,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue iconSprite = createIcon(), } }, - text = new OsuSpriteText + new FillFlowContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = OsuFont.Style.Caption2, - Text = pool.Name, - }, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + Text = rulesetName, + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Style.Caption2, + Text = pool.Name + } + } + } } }, }; @@ -198,14 +217,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { this.ScaleTo(1.2f, 200, Easing.OutQuint); iconSprite.FadeColour(Color4.Gold, 100, Easing.OutQuint); - text.Font = text.Font.With(weight: FontWeight.Bold); flashLayer.FadeTo(0.1f, 200, Easing.OutQuint); } else { this.ScaleTo(1f, 200, Easing.OutQuint); iconSprite.FadeColour(OsuColour.Gray(0.5f), 100); - text.Font = text.Font.With(weight: FontWeight.Regular); flashLayer.FadeOut(200, Easing.OutQuint); } } From 5a865476cec807fc61afca1b242abb88d6bdb0eb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Nov 2025 18:42:45 +0900 Subject: [PATCH 235/308] Remove now-unnecessary timestamp updates Since #35820, this is now handled when messages are added and removed. --- osu.Game/Overlays/Chat/DrawableChannel.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 92b5b4b082..05bafae6a1 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -79,22 +79,6 @@ namespace osu.Game.Overlays.Chat highlightedMessage.BindValueChanged(_ => processMessageHighlighting(), true); } - protected override void Update() - { - base.Update(); - - long? lastMinutes = null; - - foreach (var line in chatLines) - { - long minutes = line.Message.Timestamp.ToUnixTimeSeconds() / 60; - - line.RequiresTimestamp = minutes != lastMinutes; - - lastMinutes = minutes; - } - } - /// /// Processes any pending message in . /// From 6244617e5eb8d2feb5a6781793647ba8de48bbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Nov 2025 12:58:47 +0100 Subject: [PATCH 236/308] Attempt to prevent main menu osu! logo being triggered by media keys (#35825) Maybe addresses https://github.com/ppy/osu/discussions/35813. I can't reproduce on macOS, may be a $USER_OS idiosyncrasy. --- osu.Game/Screens/Menu/ButtonSystem.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index a73fafcffd..da78ea3371 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -286,6 +286,9 @@ namespace osu.Game.Screens.Menu if (e.Key >= Key.F1 && e.Key <= Key.F35) return false; + if (e.Key >= Key.Mute && e.Key <= Key.TrackNext) + return false; + switch (e.Key) { case Key.Escape: From 037743e0028d15dfd403c2026c5390a41744ca93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Nov 2025 13:10:46 +0100 Subject: [PATCH 237/308] Add context menu shortcut to watch local replays from song select (#35823) Addresses https://github.com/ppy/osu/discussions/35811 I guess. Will only work for local leaderboards for now but maybe good enough for what is essentially a 5 minute job? Can be made to work with online leaderboards too I guess if need be. --- osu.Game/Localisation/SongSelectStrings.cs | 5 +++++ osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index c20715fb4c..f84683ac63 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -109,6 +109,11 @@ namespace osu.Game.Localisation /// public static LocalisableString UseTheseMods => new TranslatableString(getKey(@"use_these_mods"), @"Use these mods"); + /// + /// "Watch replay" + /// + public static LocalisableString WatchReplay => new TranslatableString(getKey(@"watch_replay"), @"Watch replay"); + /// /// "For all difficulties" /// diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 16c9ed64f6..079f4192e0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -628,6 +628,8 @@ namespace osu.Game.Screens.SelectV2 if (Score.Files.Count <= 0) return items.ToArray(); + items.Add(new OsuMenuItemSpacer()); + items.Add(new OsuMenuItem(SongSelectStrings.WatchReplay, MenuItemType.Standard, () => game?.PresentScore(Score, ScorePresentType.Gameplay))); items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(Score))); items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); From 6bb25b2abe09cadf61ae2c25e496a23b2827776e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Nov 2025 13:22:06 +0100 Subject: [PATCH 238/308] Fix gameplay leaderboard tracked player not using team colour (#35826) * Demonstrate colour problem in test * Fix gameplay leaderboard tracked player not using team colour Closes https://github.com/ppy/osu/issues/35806. --- ...stSceneDrawableGameplayLeaderboardScore.cs | 71 +++++++++++++++++++ .../HUD/DrawableGameplayLeaderboardScore.cs | 10 +-- 2 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneDrawableGameplayLeaderboardScore.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableGameplayLeaderboardScore.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableGameplayLeaderboardScore.cs new file mode 100644 index 0000000000..1363ef12a9 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableGameplayLeaderboardScore.cs @@ -0,0 +1,71 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneDrawableGameplayLeaderboardScore : OsuTestScene + { + private readonly APIUser user = new APIUser { Username = "user" }; + private readonly BindableLong totalScore = new BindableLong(); + private readonly Bindable position = new Bindable(); + private readonly BindableBool quit = new BindableBool(); + private readonly BindableBool expanded = new BindableBool(); + + public TestSceneDrawableGameplayLeaderboardScore() + { + AddSliderStep("total score", 0, 1_000_000, 500_000, s => totalScore.Value = s); + AddSliderStep("position", 1, 100, 5, s => position.Value = s); + AddToggleStep("toggle quit", q => quit.Value = q); + AddToggleStep("toggle expanded", e => expanded.Value = e); + } + + private static readonly OsuColour osu_colour = new OsuColour(); + + private static readonly object?[][] leaderboard_variants = + { + new object?[] { false, null }, + new object?[] { true, null }, + new object?[] { false, osu_colour.TeamColourRed }, + new object?[] { true, osu_colour.TeamColourRed }, + new object?[] { false, osu_colour.TeamColourBlue }, + new object?[] { true, osu_colour.TeamColourBlue }, + }; + + [TestCaseSource(nameof(leaderboard_variants))] + public void TestVariants(bool tracked, Color4? teamColour) + { + AddStep("show", () => + { + GameplayLeaderboardScore score = new GameplayLeaderboardScore(user, tracked, totalScore) + { + Position = { BindTarget = position }, + HasQuit = { BindTarget = quit }, + TeamColour = teamColour, + }; + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 250, + Child = new DrawableGameplayLeaderboardScore(score) + { + Expanded = { BindTarget = expanded }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + }); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index 339488e5d0..df81afed5b 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -357,7 +357,7 @@ namespace osu.Game.Screens.Play.HUD else if (Tracked) { widthExtension = true; - setPanelColourAsTracked(); + setTrackedPanelColour(BackgroundColour); } else if (isFriend) { @@ -380,11 +380,11 @@ namespace osu.Game.Screens.Play.HUD scorePanel.BorderColour = ColourInfo.GradientVertical(baseColour.Opacity(0.2f), baseColour); } - private void setPanelColourAsTracked() + private void setTrackedPanelColour(Color4? backgroundColour) { - leftLayerGradient.Colour = ColourInfo.GradientVertical(colours.Blue2.Opacity(0.3f), colours.Blue2); - rightLayerGradient.Colour = ColourInfo.GradientVertical(colours.Blue4.Opacity(0.25f), colours.Blue3.Opacity(0.6f)); - scorePanel.BorderColour = ColourInfo.GradientVertical(colours.Blue1.Opacity(0.2f), colours.Blue1); + leftLayerGradient.Colour = ColourInfo.GradientVertical((backgroundColour ?? colours.Blue2).Opacity(0.3f), backgroundColour ?? colours.Blue2); + rightLayerGradient.Colour = ColourInfo.GradientVertical((backgroundColour ?? colours.Blue4).Opacity(0.25f), (backgroundColour ?? colours.Blue3).Opacity(0.6f)); + scorePanel.BorderColour = ColourInfo.GradientVertical((backgroundColour ?? colours.Blue1).Opacity(0.2f), backgroundColour ?? colours.Blue1); } protected override void Update() From 9e2ea63e7092e8563ea8d7e0542d0535cbf7a2e9 Mon Sep 17 00:00:00 2001 From: Chirag Mahesh Date: Thu, 27 Nov 2025 14:52:57 +0000 Subject: [PATCH 239/308] Revert Changes to Trail Position Calculation - Revert changes to CursorTrail.cs made during 79bfe7880af8243b4d5c7c83d81236c872a17314 --- .../UI/Cursor/CursorTrail.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 8a45475f0f..1c2d69fa00 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -164,18 +164,20 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor return base.OnMouseMove(e); } - protected void AddTrail(Vector2 screenSpacePosition) + protected void AddTrail(Vector2 position) { + position = ToLocalSpace(position); + if (InterpolateMovements) { if (!lastPosition.HasValue) { - lastPosition = screenSpacePosition; + lastPosition = position; resampler.AddPosition(lastPosition.Value); return; } - foreach (Vector2 pos2 in resampler.AddPosition(screenSpacePosition)) + foreach (Vector2 pos2 in resampler.AddPosition(position)) { Trace.Assert(lastPosition.HasValue); @@ -196,14 +198,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } else { - lastPosition = screenSpacePosition; + lastPosition = position; addPart(lastPosition.Value); } } - private void addPart(Vector2 screenSpacePosition) + private void addPart(Vector2 localSpacePosition) { - parts[currentIndex].ScreenSpacePosition = screenSpacePosition; + parts[currentIndex].Position = localSpacePosition; parts[currentIndex].Time = time + 1; parts[currentIndex].Scale = NewPartScale; ++parts[currentIndex].InvalidationID; @@ -215,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private struct TrailPart { - public Vector2 ScreenSpacePosition; + public Vector2 Position; public float Time; public Vector2 Scale; public long InvalidationID; @@ -302,13 +304,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (time - part.Time >= 1) continue; - Vector2 localSpacePosition = Vector2Extensions.Transform(part.ScreenSpacePosition, DrawInfo.MatrixInverse); - vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(localSpacePosition.X - texture.DisplayWidth * originPosition.X * part.Scale.X, localSpacePosition.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), - localSpacePosition, sin, cos), + new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -318,8 +318,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(localSpacePosition.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, - localSpacePosition.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), localSpacePosition, sin, cos), + new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, + part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -329,8 +329,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(localSpacePosition.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, localSpacePosition.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), - localSpacePosition, sin, cos), + new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -340,8 +340,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(localSpacePosition.X - texture.DisplayWidth * originPosition.X * part.Scale.X, localSpacePosition.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), - localSpacePosition, sin, cos), + new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, From ae33690632821391593315d52a868d571920d335 Mon Sep 17 00:00:00 2001 From: Chirag Mahesh Date: Thu, 27 Nov 2025 14:59:08 +0000 Subject: [PATCH 240/308] Implement CursorTrail Scaling - Add CursorScale property to CursorTrail and adjust for scaling --- .../UI/Cursor/CursorTrail.cs | 32 ++++++++++++++++--- .../UI/Cursor/OsuCursorContainer.cs | 2 +- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 1c2d69fa00..fc5ad2d955 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -49,6 +49,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// protected bool AllowPartRotation { get; set; } + private Vector2 cursorScale; + + public Vector2 CursorScale + { + get => cursorScale; + set + { + cursorScale = value; + Invalidate(Invalidation.DrawNode); + } + } + /// /// The trail part texture origin. /// @@ -233,6 +245,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private float time; private float fadeExponent; private float angle; + private Vector2 cursorScale; private readonly TrailPart[] parts = new TrailPart[max_sprites]; private Vector2 originPosition; @@ -253,6 +266,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor time = Source.time; fadeExponent = Source.FadeExponent; angle = Source.AllowPartRotation ? float.DegreesToRadians(Source.PartRotation) : 0; + cursorScale = Source.cursorScale; originPosition = Vector2.Zero; @@ -307,7 +321,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + new Vector2( + part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X * cursorScale.X, + part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y * cursorScale.Y), part.Position, sin, cos), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), @@ -318,8 +334,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, - part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), + new Vector2( + part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X * cursorScale.X, + part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y * cursorScale.Y), + part.Position, sin, cos), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -329,7 +347,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + new Vector2( + part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X * cursorScale.X, + part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y * cursorScale.Y), part.Position, sin, cos), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), @@ -340,7 +360,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { Position = rotateAround( - new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + new Vector2( + part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X * cursorScale.X, + part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y * cursorScale.Y), part.Position, sin, cos), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 974d99d7c8..bf0ed528fb 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor var newScale = new Vector2(e.NewValue); rippleVisualiser.CursorScale = newScale; - cursorTrail.Scale = newScale; + if (cursorTrail.Drawable is CursorTrail trail) trail.CursorScale = newScale; }, true); } From d8d7c808328f31a306dc0136c3e148a9c2c6e078 Mon Sep 17 00:00:00 2001 From: Chirag Mahesh Date: Thu, 27 Nov 2025 16:08:11 +0000 Subject: [PATCH 241/308] Persist cursorScale on skin refresh --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index bf0ed528fb..0044cf26dd 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly SkinnableDrawable cursorTrail; + private Vector2 cursorScale; + private readonly CursorRippleVisualiser rippleVisualiser; public OsuCursorContainer() @@ -61,10 +63,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor ActiveCursor.CursorScale.BindValueChanged(e => { - var newScale = new Vector2(e.NewValue); + cursorScale = new Vector2(e.NewValue); - rippleVisualiser.CursorScale = newScale; - if (cursorTrail.Drawable is CursorTrail trail) trail.CursorScale = newScale; + rippleVisualiser.CursorScale = cursorScale; + if (cursorTrail.Drawable is CursorTrail trail) trail.CursorScale = cursorScale; }, true); } @@ -86,6 +88,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { trail.NewPartScale = ActiveCursor.CurrentExpandedScale; trail.PartRotation = ActiveCursor.CurrentRotation; + trail.CursorScale = cursorScale; } } From 78c6973298f038cf1609f9e48ee90ff648d5b931 Mon Sep 17 00:00:00 2001 From: Chirag Mahesh Date: Thu, 27 Nov 2025 16:38:19 +0000 Subject: [PATCH 242/308] move cursorScale persistance into OsuCursor and use skinnableCursorScale --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 2 ++ osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index e84fb9e2d6..37ceb296b8 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One; + public Vector2 CurrentCursorScale => skinnableCursor.Scale; + /// /// The current rotation of the cursor. /// diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 0044cf26dd..df72f8be97 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -31,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly SkinnableDrawable cursorTrail; - private Vector2 cursorScale; - private readonly CursorRippleVisualiser rippleVisualiser; public OsuCursorContainer() @@ -63,10 +61,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor ActiveCursor.CursorScale.BindValueChanged(e => { - cursorScale = new Vector2(e.NewValue); + var newScale = new Vector2(e.NewValue); - rippleVisualiser.CursorScale = cursorScale; - if (cursorTrail.Drawable is CursorTrail trail) trail.CursorScale = cursorScale; + rippleVisualiser.CursorScale = newScale; + if (cursorTrail.Drawable is CursorTrail trail) trail.CursorScale = newScale; }, true); } @@ -88,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { trail.NewPartScale = ActiveCursor.CurrentExpandedScale; trail.PartRotation = ActiveCursor.CurrentRotation; - trail.CursorScale = cursorScale; + trail.CursorScale = ActiveCursor.CurrentCursorScale; } } From a8f058141b4e4bdf69c0d49c0c0b4f35e6d9971a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Nov 2025 00:21:13 +0100 Subject: [PATCH 243/308] Fix several issues with editor timestamps for objects with fractional start times in osu!mania (#35829) * Fix mania editor timestamp generation being culture-dependent Mostly closes https://github.com/ppy/osu/issues/35809. * Add failing test for notes with fractions * Round note time when copying out timestamp & apply half-millisecond tolerance when parsing Closes the rest of https://github.com/ppy/osu/issues/35809. One issue here was that while the timestamp generation would allow fractional object timestamps to be output, the parsing (via `selection_regex`) would *reject* fractional timestamps, therefore making lazer incompatible even with itself. The other is that rounding is probably fine to do anyway for interoperability with stable. I'd hope nobody actually *needs* sub-millisecond precision but I'm ready to be proven wrong by some aspire jokester. * Specify invariant culture when writing out combo indices to editor timestamp in other rulesets Pretty sure this is just a much-of-muchness because it's integers but might as well if I'm spending time here already. --- .../Edit/CatchHitObjectComposer.cs | 4 ++- .../TestSceneOpenEditorTimestampInMania.cs | 27 ++++++++++--------- .../Edit/ManiaHitObjectComposer.cs | 9 ++++--- .../Edit/OsuHitObjectComposer.cs | 4 ++- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 370eb37d16..be9685ce9a 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using osu.Framework.Allocation; @@ -224,7 +225,8 @@ namespace osu.Game.Rulesets.Catch.Edit #region Clipboard handling public override string ConvertSelectionToString() - => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime) + .Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture))); // 1,2,3,4 ... private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs index 05c881d284..ad41ad9be4 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs @@ -18,15 +18,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public void TestNormalSelection() { addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)"); - AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)> - { (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) } - )); + AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, [(5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1)])); addReset(); addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)"); - AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)> - { (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) } - )); + AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, [(42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1)])); addReset(); AddStep("add notes to row", () => @@ -41,15 +37,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor EditorBeatmap.AddRange(new[] { second, third, forth }); }); addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)"); - AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)> - { (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) } - )); + AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, [(11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3)])); addReset(); addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)"); - AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)> - { (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) } - )); + AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, [(96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1)])); + } + + [Test] + public void TestRoundingToNearestMillisecondApplied() + { + AddStep("resnap note to have fractional coordinates", + () => EditorBeatmap.HitObjects.OfType().Single(ho => ho.StartTime == 85_373 && ho.Column == 1).StartTime = 85_373.125); + addStepClickLink("01:25:373 (85373|1)"); + AddAssert("selected note", () => checkSnapAndSelectColumn(85_373.125, [(85_373.125, 1)])); } [Test] @@ -75,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor private void addReset() => addStepClickLink("00:00:000", "reset", false); - private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null) + private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(double, int)>? columnPairs = null) { bool checkColumns = columnPairs != null ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index bc20456722..7da501063d 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using osu.Framework.Allocation; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -54,7 +56,8 @@ namespace osu.Game.Rulesets.Mania.Edit }; public override string ConvertSelectionToString() - => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); + => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime) + .Select(h => FormattableString.Invariant($"{Math.Round(h.StartTime)}|{h.Column}"))); // 123|0,456|1,789|2 ... private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled); @@ -73,10 +76,10 @@ namespace osu.Game.Rulesets.Mania.Edit if (split.Length != 2) continue; - if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column)) + if (!int.TryParse(split[0], out int time) || !int.TryParse(split[1], out int column)) continue; - ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column); + ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => Precision.AlmostEquals(h.StartTime, time, 0.5) && h.Column == column); if (current == null) continue; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0dac4cb2df..6ff762b82f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using JetBrains.Annotations; @@ -171,7 +172,8 @@ namespace osu.Game.Rulesets.Osu.Edit => new OsuBlueprintContainer(this); public override string ConvertSelectionToString() - => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime) + .Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture))); // 1,2,3,4 ... private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled); From 8f927ea7b5eeb70686e14c5332205c81d050a92b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Nov 2025 00:25:47 +0100 Subject: [PATCH 244/308] Fix `Beatmap.GetMostCommonBeatLength()` potentially returning a beat length smaller or larger than the actual limits (#35827) Closes https://github.com/ppy/osu/issues/35807. The reason this closes the aforementioned issue is as follows: Taking https://osu.ppy.sh/beatmapsets/1236180#osu/4650477 as the example, we have: ``` minBeatLength = 342.857142857143 maxBeatLength = 419.58041958042003 mostCommonBeatLength = 342.85700000000003 ``` Note that `mostCommonBeatLength < minBeatLength` here. Taking the inverse of that to compute BPM, we get ``` minBpm = 174.99999999999991 maxBpm = 142.99999999999986 mostCommonBpm = 175.00007291669704 ``` which without DT present doesn't do anything bad, but when DT is engaged (and thus BPM is multiplied by 1.5), midpoint rounding causes the min BPM to become 262, and the most common BPM to become 263. --- osu.Game/Beatmaps/Beatmap.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 155ded5747..c728f24368 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -105,6 +105,7 @@ namespace osu.Game.Beatmaps return (beatLength: t.BeatLength, duration: nextTime - currentTime); }) // Aggregate durations into a set of (beatLength, duration) tuples for each beat length + // Rounding is applied here (to 1e-3 milliseconds) to neutralise potential effects of floating point inaccuracies .GroupBy(t => Math.Round(t.beatLength * 1000) / 1000) .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) // Get the most common one, or 0 as a suitable default (see handling below) @@ -113,7 +114,12 @@ namespace osu.Game.Beatmaps if (mostCommon.beatLength == 0) return TimingControlPoint.DEFAULT_BEAT_LENGTH; - return mostCommon.beatLength; + // Because of the rounding applied to the beat length above, it is possible for the "most common" beat length as determined by the linq query above + // to actually be less or more than the raw range of unrounded beat lengths present in the map + // To ensure this does not become a problem anywhere else further, clamp the result to the known raw range + double minBeatLength = ControlPointInfo.TimingPoints.Min(t => t.BeatLength); + double maxBeatLength = ControlPointInfo.TimingPoints.Max(t => t.BeatLength); + return Math.Clamp(mostCommon.beatLength, minBeatLength, maxBeatLength); } public double AudioLeadIn { get; set; } From c6eba26a67732fdeb579eacb4d083cd7d4958a90 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 28 Nov 2025 01:40:07 +0100 Subject: [PATCH 245/308] trim timestamp when pasting into `TimeInfoContainer` --- osu.Game/Screens/Edit/Components/TimeInfoContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index d17f9011f4..7de94cd22e 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -151,7 +151,7 @@ namespace osu.Game.Screens.Edit.Components }); }; - inputTextBox.Current.BindValueChanged(val => editor?.HandleTimestamp(val.NewValue)); + inputTextBox.Current.BindValueChanged(val => editor?.HandleTimestamp(val.NewValue.Trim())); inputTextBox.OnCommit += (_, __) => { From 92e9a367448c5740ed4fcb69602898ebcea129a5 Mon Sep 17 00:00:00 2001 From: Vanni <103026931+imvanni@users.noreply.github.com> Date: Fri, 28 Nov 2025 03:25:43 +0100 Subject: [PATCH 246/308] Force exit to menu on quick play disonnection (#35793) --- .../Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index e3319d4c94..753b8a90aa 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -192,6 +192,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (this.IsCurrentScreen() && client.Room == null) { Logger.Log($"{this} exiting due to loss of room or connection"); + exitConfirmed = true; this.Exit(); } } From 0b4f96efc82994773f19383aee5f0758b2e85835 Mon Sep 17 00:00:00 2001 From: SollyBunny Date: Sat, 29 Nov 2025 03:21:19 +0000 Subject: [PATCH 247/308] Make tracked leaderboard score yellow again --- .../Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index df81afed5b..ef27d027c2 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -357,7 +357,7 @@ namespace osu.Game.Screens.Play.HUD else if (Tracked) { widthExtension = true; - setTrackedPanelColour(BackgroundColour); + setPanelColour(BackgroundColour ?? colours.Orange2); } else if (isFriend) { @@ -380,13 +380,6 @@ namespace osu.Game.Screens.Play.HUD scorePanel.BorderColour = ColourInfo.GradientVertical(baseColour.Opacity(0.2f), baseColour); } - private void setTrackedPanelColour(Color4? backgroundColour) - { - leftLayerGradient.Colour = ColourInfo.GradientVertical((backgroundColour ?? colours.Blue2).Opacity(0.3f), backgroundColour ?? colours.Blue2); - rightLayerGradient.Colour = ColourInfo.GradientVertical((backgroundColour ?? colours.Blue4).Opacity(0.25f), (backgroundColour ?? colours.Blue3).Opacity(0.6f)); - scorePanel.BorderColour = ColourInfo.GradientVertical((backgroundColour ?? colours.Blue1).Opacity(0.2f), backgroundColour ?? colours.Blue1); - } - protected override void Update() { base.Update(); From 82f4406c7972e7de01b06ea89a91034da02838ef Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 30 Nov 2025 05:14:02 -0500 Subject: [PATCH 248/308] Allow resizing osu! on iPadOS --- osu.iOS/Info.plist | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 120e8caecc..e002949177 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -25,8 +25,6 @@ armv7 - UIRequiresFullScreen - UIStatusBarHidden UIApplicationSupportsIndirectInputEvents From fe5cbc493237fb7152d15b599024ed02f435a066 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Dec 2025 14:20:04 +0900 Subject: [PATCH 249/308] Add remaining two locus winners as bundled beatmaps --- osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 96838bb1ba..b61b2358b0 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -192,6 +192,8 @@ namespace osu.Game.Beatmaps.Drawables "2412260 Koto Spirit - Locus of Hexagram.osz", "2412232 Will Stetson - Of Our Time.osz", "2412292 ArXe - Locus Amoenus (feat. Megurine Luka).osz", + "2412328 Akiri - Vespera Stella.osz", + "2412331 takehirotei - Haiboku no Altra Vita.osz", }; private static readonly string[] bundled_osu = From ca8247c667c12364bd73066d942fca79f01d06d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Dec 2025 10:43:18 +0100 Subject: [PATCH 250/308] Fix welcome intro skin not being looked up from user skin for supporters Closes https://github.com/ppy/osu/issues/35833. --- osu.Game/Skinning/LegacySkin.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 11b3b5c71d..6ff569869a 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -540,6 +540,10 @@ namespace osu.Game.Skinning case "Menu/fountain-star": componentName = "star2"; break; + + case @"Intro/Welcome/welcome_text": + componentName = @"welcome_text"; + break; } Texture? texture = null; From 043a1c27935510eab8e8c9e7d5950e7a0e01f13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Dec 2025 11:17:44 +0100 Subject: [PATCH 251/308] Disable quick retry binding in solo spectator (#35873) Closes https://github.com/ppy/osu/issues/35870? For some definition of "closes", I guess? Why would you ever do this, unless on purpose just to break stuff? Don't answer that. A side effect of setting this flag is that the hold-to-exit menu button that's there on devices that support touch will slightly change behaviour to the behaviour multiplayer play has: https://github.com/ppy/osu/blob/e3ea38a366601df01f045f4f111765c34d041145/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs#L67 https://github.com/ppy/osu/blob/8d9245c1d4f72db4307da7492d65bd09fc48bc02/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs#L79-L82 but upon thinking about it for three minutes I decided I don't care and it's probably fine because all of this was already racking up to fifteen minutes that I shouldn't have had to spend on any of this. Notably this shouldn't affect the actual spectated user retrying, because all of that is handled elsewhere via https://github.com/ppy/osu/blob/2f90bb4d6793475835d1d51bef92b2c40f69112c/osu.Game/Screens/Spectate/SpectatorScreen.cs#L138-L154 --- osu.Game/Screens/Play/SoloSpectatorPlayer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs index 16b1ff7ccc..d635fa9fe9 100644 --- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs @@ -21,7 +21,12 @@ namespace osu.Game.Screens.Play protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo); public SoloSpectatorPlayer(Score score) - : base(score, new PlayerConfiguration { AllowUserInteraction = false, ShowLeaderboard = true }) + : base(score, new PlayerConfiguration + { + AllowUserInteraction = false, + ShowLeaderboard = true, + AllowRestart = false + }) { this.score = score; } From 0b3ec3f1e1982a77355c69298e5a292c9f24ece2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Dec 2025 11:23:32 +0100 Subject: [PATCH 252/308] Fix changing beatmap during hold-to-reveal-background delay turning off blur (#35867) Closes https://github.com/ppy/osu/issues/35864. --- 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 1b66bd5600..ab3375e23e 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -811,7 +811,8 @@ namespace osu.Game.Screens.SelectV2 // Probably needs more thought because this needs to be in every `ApplyToBackground` currently to restore sane defaults. backgroundModeBeatmap.FadeColour(Color4.White, 250); - backgroundModeBeatmap.BlurAmount.Value = revealingBackground == null && configBackgroundBlur.Value ? 20 : 0f; + bool backgroundRevealActive = revealingBackground?.State == ScheduledDelegate.RunState.Running || revealingBackground?.State == ScheduledDelegate.RunState.Complete; + backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value && !backgroundRevealActive ? 20 : 0f; }); #endregion From 3e4c038a376bc8998c17baed66fd5ad6cc8029a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Dec 2025 11:58:27 +0100 Subject: [PATCH 253/308] Do not distinguish between left/right modifiers when assigning new key combinations Addresses https://github.com/ppy/osu/discussions/35851. And no I'm not making it "you have to press both modifiers for it to become any of the two" because that's ultra weird. --- .../Sections/Input/KeyBindingRow.KeyButton.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs index adf05a71b9..2eb900207a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs @@ -5,6 +5,7 @@ using System; using System.Linq; 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; @@ -139,9 +140,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// /// A generated from the full input state. /// The key which triggered this update, and should be used as the binding. - public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) => - // TODO: Distinct() can be removed after https://github.com/ppy/osu-framework/pull/6130 is merged. - UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey).Distinct().ToArray())); + public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) + { + var combination = fullState.Keys.Where(KeyCombination.IsModifierKey) + .Append(triggerKey) + .Select(k => k.GetVirtualKey() ?? k) + .ToArray(); + UpdateKeyCombination(new KeyCombination(combination)); + } public void UpdateKeyCombination(KeyCombination newCombination) { From 12170df80add87e8f292545f67566f8d25465efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Dec 2025 14:15:52 +0100 Subject: [PATCH 254/308] Disallow placing hit objects before first timing point Because they can break stable. See https://github.com/ppy/osu/issues/31591#issuecomment-3575270120 for detailed rationale. --- .../Edit/Blueprints/BananaShowerPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/JuiceStreamPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/HoldNotePlacementBlueprint.cs | 2 +- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs | 2 ++ 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 971c98cafd..bb71da12ce 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private double placementStartTime; private double placementEndTime; - protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0); + protected override bool IsValidForPlacement => base.IsValidForPlacement && Precision.DefinitelyBigger(HitObject.Duration, 0); public BananaShowerPlacementBlueprint() { diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 292175353a..650bdf7994 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private InputManager inputManager = null!; - protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0); + protected override bool IsValidForPlacement => base.IsValidForPlacement && Precision.DefinitelyBigger(HitObject.Duration, 0); public JuiceStreamPlacementBlueprint() { diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 094c59da46..85496b42b4 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IScrollingInfo scrollingInfo { get; set; } = null!; - protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0); + protected override bool IsValidForPlacement => base.IsValidForPlacement && Precision.DefinitelyBigger(HitObject.Duration, 0); public HoldNotePlacementBlueprint() : base(new HoldNote()) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index d934eb5a9e..ed0df96a27 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; - protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement; + protected override bool IsValidForPlacement => base.IsValidForPlacement && HitObject.Path.HasValidLengthForPlacement; public SliderPlacementBlueprint() : base(new Slider()) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index 3d5c95e1e8..291dd3bdf1 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints [Resolved] private TaikoHitObjectComposer? composer { get; set; } - protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); + protected override bool IsValidForPlacement => base.IsValidForPlacement && Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 6720540ec2..7e9ece5591 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Edit private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault(); + protected override bool IsValidForPlacement => HitObject.StartTime >= beatmap.ControlPointInfo.TimingPoints[0].Time; + [Resolved] private IPlacementHandler placementHandler { get; set; } = null!; From c6cc92315c82454dc3b3ae70c28f1783a7ef359f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Dec 2025 14:30:18 +0100 Subject: [PATCH 255/308] Add basic colour indication as to when placements are valid Unsure about this one, but I find the preceding commit to be very lacking in explaining to the user why the editor don't work. Shining some things red may help aid understanding. --- .../Edit/Blueprints/BananaShowerPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/JuiceStreamPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/HoldNotePlacementBlueprint.cs | 2 +- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs | 8 ++++++++ 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index bb71da12ce..bd5886cb82 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private double placementStartTime; private double placementEndTime; - protected override bool IsValidForPlacement => base.IsValidForPlacement && Precision.DefinitelyBigger(HitObject.Duration, 0); + protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0)); public BananaShowerPlacementBlueprint() { diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 650bdf7994..cce3b93d90 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private InputManager inputManager = null!; - protected override bool IsValidForPlacement => base.IsValidForPlacement && Precision.DefinitelyBigger(HitObject.Duration, 0); + protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0)); public JuiceStreamPlacementBlueprint() { diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 85496b42b4..2674ab4d75 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IScrollingInfo scrollingInfo { get; set; } = null!; - protected override bool IsValidForPlacement => base.IsValidForPlacement && Precision.DefinitelyBigger(HitObject.Duration, 0); + protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0)); public HoldNotePlacementBlueprint() : base(new HoldNote()) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index ed0df96a27..b5b4c8c87d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; - protected override bool IsValidForPlacement => base.IsValidForPlacement && HitObject.Path.HasValidLengthForPlacement; + protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || HitObject.Path.HasValidLengthForPlacement); public SliderPlacementBlueprint() : base(new Slider()) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index 291dd3bdf1..6073dc7912 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints [Resolved] private TaikoHitObjectComposer? composer { get; set; } - protected override bool IsValidForPlacement => base.IsValidForPlacement && Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); + protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(spanPlacementObject.Duration, 0)); public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 7e9ece5591..ea5f69be7b 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -89,6 +90,13 @@ namespace osu.Game.Rulesets.Edit placementHandler.HidePlacement(); } + protected override void Update() + { + base.Update(); + + Colour = IsValidForPlacement ? Colour4.White : Colour4.Red; + } + /// /// Updates the time and position of this . /// From 1c33291b3f041fabc5faf342bcf2b57e7bee042c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 4 Dec 2025 13:38:28 -0500 Subject: [PATCH 256/308] Adjust skip button colour and add triangles --- osu.Game/Screens/Play/SkipOverlay.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 700ea2e532..1d7961093a 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.Play Height = 5, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Colour = colours.Yellow, + Colour = colours.Orange3, RelativeSizeAxes = Axes.X } } @@ -328,8 +328,8 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(OsuColour colours, AudioManager audio) { - colourNormal = colours.Yellow; - colourHover = colours.YellowDark; + colourNormal = colours.Orange3; + colourHover = colours.Orange4; sampleConfirm = audio.Samples.Get(@"UI/submit-select"); @@ -356,6 +356,11 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Colour = colourNormal, }, + new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(colourNormal.Lighten(0.2f), colourNormal) + }, flow = new FillFlowContainer { Anchor = Anchor.TopCentre, From 99da986e0265379c6ba050c0da9f9b74bd8a62c5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 4 Dec 2025 13:00:47 -0500 Subject: [PATCH 257/308] Implement redesigned multiplayer vote-to-skip button --- .../Multiplayer/MultiplayerSkipOverlay.cs | 297 ++++++++++++++---- osu.Game/Screens/Play/SkipOverlay.cs | 31 +- 2 files changed, 255 insertions(+), 73 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 79fd0eb8d1..5c2cc25157 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -4,15 +4,24 @@ 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.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.Input.Events; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osuTK; using osuTK.Graphics; @@ -23,92 +32,49 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } = null!; - private Drawable votedIcon = null!; - private OsuSpriteText countText = null!; + [Resolved] + private OsuColour colours { get; set; } = null!; + + private Button skipButton = null!; public MultiplayerSkipOverlay(double startTime) : base(startTime) { } - [BackgroundDependencyLoader] - private void load() + protected override OsuClickableContainer CreateButton() => skipButton = new Button { - FadingContent.AddRange( - [ - votedIcon = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(50, 0), - Size = new Vector2(20), - Alpha = 0, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Green - }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.5f), - Icon = FontAwesome.Solid.Check - } - } - }, - countText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - Position = new Vector2(0.75f, 0), - Font = OsuFont.Default.With(size: 36, weight: FontWeight.Bold) - } - ]); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; protected override void LoadComplete() { base.LoadComplete(); + skipButton.Enabled.BindValueChanged(e => + { + RemainingTimeBox.Colour = e.NewValue ? colours.Orange3 : Button.COLOUR_GRAY; + }, true); + client.UserLeft += onUserLeft; client.UserStateChanged += onUserStateChanged; client.UserVotedToSkipIntro += onUserVotedToSkipIntro; - updateText(); + updateCount(); } - private void onUserLeft(MultiplayerRoomUser user) - { - Schedule(updateText); - } + private void onUserLeft(MultiplayerRoomUser user) => Schedule(updateCount); - private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) - { - Schedule(updateText); - } + private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) => Schedule(updateCount); private void onUserVotedToSkipIntro(int userId) => Schedule(() => { FadingContent.TriggerShow(); - - updateText(); - - countText.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); - - if (userId == client.LocalUser?.UserID) - { - votedIcon.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); - votedIcon.FadeInFromZero(100); - } + updateCount(); }); - private void updateText() + private void updateCount() { if (client.Room == null || client.Room.Settings.AutoSkip) return; @@ -117,7 +83,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkipIntro); int countRequired = countTotal / 2 + 1; - countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; + skipButton.SkippedCount.Value = Math.Min(countRequired, countSkipped); + skipButton.RequiredCount.Value = countRequired; } protected override void Dispose(bool isDisposing) @@ -131,5 +98,211 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.UserVotedToSkipIntro -= onUserVotedToSkipIntro; } } + + public partial class Button : OsuClickableContainer + { + private const float chevron_y = 0.4f; + private const float secondary_y = 0.7f; + + public static readonly Color4 COLOUR_GRAY = OsuColour.Gray(0.4f); + + private Box background = null!; + private Box box = null!; + private TrianglesV2 triangles = null!; + private OsuSpriteText countText = null!; + private OsuSpriteText skipText = null!; + private AspectContainer aspect = null!; + + private FillFlowContainer chevrons = null!; + + private Sample sampleConfirm = null!; + + public readonly BindableInt SkippedCount = new BindableInt(); + public readonly BindableInt RequiredCount = new BindableInt(); + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public Button() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleConfirm = audio.Samples.Get(@"UI/submit-select"); + + Children = new Drawable[] + { + background = new Box + { + Alpha = 0.2f, + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + aspect = new AspectContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 0.6f, + Masking = true, + CornerRadius = 15, + Children = new Drawable[] + { + box = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + }, + countText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + RelativePositionAxes = Axes.Y, + Y = 0.35f, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 24), + Origin = Anchor.Centre, + }, + chevrons = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + RelativePositionAxes = Axes.Y, + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Children = new[] + { + new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight }, + new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight }, + new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight }, + } + }, + skipText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + RelativePositionAxes = Axes.Y, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), + Origin = Anchor.Centre, + Text = @"SKIP", + Y = secondary_y, + }, + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SkippedCount.BindValueChanged(_ => updateCount()); + RequiredCount.BindValueChanged(_ => updateCount(), true); + Enabled.BindValueChanged(_ => updateColours(), true); + + FinishTransforms(true); + } + + private void updateChevronsSpacing() + { + if (SkippedCount.Value > 0 && RequiredCount.Value > 1) + chevrons.TransformSpacingTo(new Vector2(-5f), 500, Easing.OutQuint); + else + chevrons.TransformSpacingTo(IsHovered ? new Vector2(5f) : new Vector2(0f), 500, Easing.OutQuint); + } + + private void updateCount() + { + if (SkippedCount.Value > 0 && RequiredCount.Value > 1) + { + countText.FadeIn(300, Easing.OutQuint); + countText.Text = $"{SkippedCount.Value} / {RequiredCount.Value}"; + + chevrons.ScaleTo(0.5f, 300, Easing.OutQuint) + .MoveTo(new Vector2(-11, secondary_y), 300, Easing.OutQuint); + + skipText.MoveToX(11f, 300, Easing.OutQuint); + } + else + { + countText.FadeOut(300, Easing.OutQuint); + + chevrons.ScaleTo(1f, 300, Easing.OutQuint) + .MoveTo(new Vector2(0, chevron_y), 300, Easing.OutQuint); + + skipText.MoveToX(0f, 300, Easing.OutQuint); + } + + updateChevronsSpacing(); + updateColours(); + } + + private void updateColours() + { + if (!Enabled.Value) + { + box.FadeColour(COLOUR_GRAY, 500, Easing.OutQuint); + triangles.FadeColour(ColourInfo.GradientVertical(COLOUR_GRAY.Lighten(0.2f), COLOUR_GRAY), 500, Easing.OutQuint); + } + else + { + box.FadeColour(IsHovered ? colours.Orange4 : colours.Orange3, 500, Easing.OutQuint); + triangles.FadeColour(ColourInfo.GradientVertical(colours.Orange3.Lighten(0.2f), colours.Orange3), 500, Easing.OutQuint); + } + } + + protected override bool OnHover(HoverEvent e) + { + if (Enabled.Value) + { + updateChevronsSpacing(); + updateColours(); + background.FadeTo(0.4f, 500, Easing.OutQuint); + } + + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateChevronsSpacing(); + updateColours(); + background.FadeTo(0.2f, 500, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (Enabled.Value) + aspect.ScaleTo(0.75f, 2000, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (Enabled.Value) + aspect.ScaleTo(1, 1000, Easing.OutElastic); + base.OnMouseUp(e); + } + + protected override bool OnClick(ClickEvent e) + { + if (!Enabled.Value) + return false; + + sampleConfirm.Play(); + + box.FlashColour(Color4.White, 500, Easing.OutQuint); + aspect.ScaleTo(1.2f, 2000, Easing.OutQuint); + + base.OnClick(e); + + Enabled.Value = false; + return true; + } + } } } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 1d7961093a..305117283d 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -10,7 +10,9 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; @@ -19,6 +21,7 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; @@ -41,9 +44,10 @@ namespace osu.Game.Screens.Play protected FadeContainer FadingContent { get; private set; } - private Button button; + private OsuClickableContainer button; + private ButtonContainer buttonContainer; - private Circle remainingTimeBox; + protected Circle RemainingTimeBox; private double displayTime; private bool isClickable; @@ -83,12 +87,8 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - button = new Button - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - remainingTimeBox = new Circle + button = CreateButton(), + RemainingTimeBox = new Circle { Height = 5, Anchor = Anchor.BottomCentre, @@ -101,6 +101,12 @@ namespace osu.Game.Screens.Play }; } + protected virtual OsuClickableContainer CreateButton() => new Button + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + private const double fade_time = 300; private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME; @@ -174,10 +180,13 @@ namespace osu.Game.Screens.Play double progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); - remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); + RemainingTimeBox.Width = (float)Interpolation.Lerp(RemainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); isClickable = progress > 0; - button.Enabled.Value = isClickable; + + if (!isClickable) + button.Enabled.Value = false; + buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden; } @@ -220,7 +229,7 @@ namespace osu.Game.Screens.Play float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime); float newWidth = 1 - Math.Clamp(progress, 0, 1); - remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint); + RemainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint); } public partial class FadeContainer : Container, IStateful From fef8117b5cd5c497d49cc93e99417f6abade30e5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 4 Dec 2025 13:00:55 -0500 Subject: [PATCH 258/308] Add test coverage for players leaving during intro --- .../TestSceneMultiplayerSkipOverlay.cs | 78 +++++++++++++++++-- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs index 059af2484d..c7ce67d168 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs @@ -1,9 +1,11 @@ // 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.Extensions; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu; @@ -33,6 +35,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Children = new Drawable[] { new MultiplayerSkipOverlay(120000) + { + RequestSkip = () => MultiplayerClient.VoteToSkipIntro().WaitSafely(), + } }, }; @@ -47,26 +52,83 @@ namespace osu.Game.Tests.Visual.Multiplayer { for (int i = 0; i < 4; i++) { - int i2 = i; + int userId = i; - AddStep($"join user {i2}", () => + AddStep($"join user {userId}", () => { MultiplayerClient.AddUser(new APIUser { - Id = i2, - Username = $"User {i2}" + Id = userId, + Username = $"User {userId}" }); - MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing); + MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing); }); } - AddStep("local user votes", () => MultiplayerClient.VoteToSkipIntro().WaitSafely()); + AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely()); + AddStep("local user votes", () => this.ChildrenOfType().Single().TriggerClick()); + AddStep("user 1 votes", () => MultiplayerClient.UserVoteToSkipIntro(1).WaitSafely()); + } + [Test] + public void TestLeavingBeforeLocalVote() + { for (int i = 0; i < 4; i++) { - int i2 = i; - AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkipIntro(i2).WaitSafely()); + int userId = i; + + AddStep($"join user {userId}", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = userId, + Username = $"User {userId}" + }); + + MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing); + }); + } + + AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely()); + AddStep("user 1 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); + AddStep("user 2 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 2 })); + AddStep("user 3 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 3 })); + AddStep("user 0 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 0 })); + } + + [Test] + public void TestLeavingAfterLocalVote() + { + for (int i = 0; i < 4; i++) + { + int userId = i; + + AddStep($"join user {userId}", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = userId, + Username = $"User {userId}" + }); + + MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing); + }); + } + + AddStep("local user votes", () => this.ChildrenOfType().Single().TriggerClick()); + AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely()); + AddStep("user 1 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); + AddStep("user 2 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 2 })); + AddStep("user 3 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 3 })); + AddStep("user 0 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 0 })); + } + + public partial class TestMultiplayerSkipOverlay : MultiplayerSkipOverlay + { + public TestMultiplayerSkipOverlay() + : base(120000) + { } } } From 2d8b1e71526f471919447a5a51965ecfc9d48da0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 4 Dec 2025 13:58:41 -0500 Subject: [PATCH 259/308] Make button brighter on hover --- .../Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 2 +- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 5c2cc25157..02f28c2c45 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -249,7 +249,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } else { - box.FadeColour(IsHovered ? colours.Orange4 : colours.Orange3, 500, Easing.OutQuint); + box.FadeColour(IsHovered ? colours.Orange3.Lighten(0.2f) : colours.Orange3, 500, Easing.OutQuint); triangles.FadeColour(ColourInfo.GradientVertical(colours.Orange3.Lighten(0.2f), colours.Orange3), 500, Easing.OutQuint); } } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 305117283d..e17d639a1b 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -338,7 +338,7 @@ namespace osu.Game.Screens.Play private void load(OsuColour colours, AudioManager audio) { colourNormal = colours.Orange3; - colourHover = colours.Orange4; + colourHover = colours.Orange3.Lighten(0.2f); sampleConfirm = audio.Samples.Get(@"UI/submit-select"); From 8a9f60df68d2d32a6b61985af57ee54876a1348c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Dec 2025 16:45:37 +0900 Subject: [PATCH 260/308] Add failing test --- .../Matchmaking/TestSceneBeatmapSelectGrid.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 4759275efc..79ee4886a9 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -235,6 +235,22 @@ namespace osu.Game.Tests.Visual.Matchmaking }); } + [Test] + public void TestRollAnimationFinalRandom() + { + AddStep("play animation", () => + { + (long[] candidateItems, _) = pickRandomItems(5); + + candidateItems = candidateItems.Append(-1).ToArray(); + long finalItem = items.First(i => !candidateItems.Contains(i.ID)).ID; + + grid.RollAndDisplayFinalBeatmap(candidateItems, -1, finalItem); + }); + + AddWaitStep("wait for animation", 10); + } + private (long[] candidateItems, long finalItem) pickRandomItems(int count) { long[] candidateItems = items.Select(it => it.ID).ToArray(); From f595a470596adb8e83c97ab74777034d945919b5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Dec 2025 16:50:33 +0900 Subject: [PATCH 261/308] Fix quick play crash when presenting random selection --- .../Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 5e8975410f..a3ee24d479 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect this.Delay(ARRANGE_DELAY) .Schedule(() => ArrangeItemsForRollAnimation()) .Delay(arrange_duration) - .Schedule(() => PlayRollAnimation(gameplayItemId, roll_duration)) + .Schedule(() => PlayRollAnimation(candidateItemId, roll_duration)) .Delay(roll_duration + present_beatmap_delay) .Schedule(() => PresentRolledBeatmap(candidateItemId, gameplayItemId)); } From fed9564b4077ef3b5ee43142b8b54f253994a0d7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Dec 2025 17:26:55 +0900 Subject: [PATCH 262/308] Fix "kicked" users not being marked as quit --- .../Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index 4b97400ebe..ce14d0bb19 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -78,6 +78,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match client.MatchRoomStateChanged += onRoomStateChanged; client.UserJoined += onUserJoined; client.UserLeft += onUserLeft; + client.UserKicked += onUserLeft; if (client.Room != null) { @@ -207,6 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match client.MatchRoomStateChanged -= onRoomStateChanged; client.UserJoined -= onUserJoined; client.UserLeft -= onUserLeft; + client.UserKicked -= onUserLeft; } } From 66ebce8c120affd2ee54b8c4b8d7e45a2e722b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Dec 2025 15:04:59 +0100 Subject: [PATCH 263/308] Fix failing tests after disallowing object placements before first timing point --- .../Editor/TestSceneNotePlacementBlueprint.cs | 34 +++++++++++-------- .../Editor/TestSceneSliderDrawing.cs | 8 ++++- .../Visual/Editing/TestSceneEditorSaving.cs | 2 +- .../Editing/TestScenePlacementBlueprint.cs | 8 ++++- .../Editing/TestSceneTimelineSelection.cs | 10 ++++-- .../Visual/PlacementBlueprintTestScene.cs | 2 ++ 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs index 0cb9639cd1..a18b765233 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; using osu.Game.Rulesets.Edit; @@ -16,6 +17,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; using osuTK.Input; @@ -36,21 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestPlaceBeforeCurrentTimeDownwards() { + AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get().Seek(200)); AddStep("move mouse before current time", () => - { - var column = this.ChildrenOfType().Single(); - InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100)); - }); - - AddStep("click", () => InputManager.Click(MouseButton.Left)); - - AddAssert("note start time < 0", () => getNote().StartTime < 0); - } - - [Test] - public void TestPlaceAfterCurrentTimeDownwards() - { - AddStep("move mouse after current time", () => { var column = this.ChildrenOfType().Single(); InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100)); @@ -58,7 +47,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor AddStep("click", () => InputManager.Click(MouseButton.Left)); - AddAssert("note start time > 0", () => getNote().StartTime > 0); + AddAssert("note start time < 200", () => getNote().StartTime < 200); + } + + [Test] + public void TestPlaceAfterCurrentTimeDownwards() + { + AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get().Seek(200)); + AddStep("move mouse after current time", () => + { + var column = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(300)); + }); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("note start time > 200", () => getNote().StartTime > 200); } private Note getNote() => this.ChildrenOfType().FirstOrDefault()?.HitObject; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs index 0e36c1dc45..74474c0b6e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; @@ -22,7 +23,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [TestFixture] public partial class TestSceneSliderDrawing : TestSceneOsuEditor { - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = new TestBeatmap(ruleset, false); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + return beatmap; + } [Test] public void TestTouchInputPlaceHitCircleDirectly() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 7f40da5bab..66a039f36e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing double lastStarRating = 0; double lastLength = 0; - AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 600 })); + AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 600 })); AddStep("Change to placement mode", () => InputManager.Key(Key.Number2)); AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index ae20f5e5cf..822c045355 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets; @@ -27,7 +28,12 @@ namespace osu.Game.Tests.Visual.Editing { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = new TestBeatmap(ruleset, false); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + return beatmap; + } private GlobalActionContainer globalActionContainer => this.ChildrenOfType().Single(); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 229cb995d8..1915c00eb4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -27,7 +28,12 @@ namespace osu.Game.Tests.Visual.Editing { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = new TestBeatmap(ruleset, false); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); + return beatmap; + } private TimelineBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); @@ -80,7 +86,7 @@ namespace osu.Game.Tests.Visual.Editing { InputManager.Key(Key.Number1); blueprint = this.ChildrenOfType().First(); - InputManager.MoveMouseTo(blueprint); + InputManager.MoveMouseTo(blueprint, new Vector2(-1, 0)); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index a644936a16..b23fda8190 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -55,6 +56,7 @@ namespace osu.Game.Tests.Visual var playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo); playable.BeatmapInfo.Ruleset = rulesetInfo; playable.Difficulty.CircleSize = 2; + playable.ControlPointInfo.Add(0, new TimingControlPoint()); return playable; } From b1e27d842b99188a6e4337f8c3cc881cc8b96fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 5 Dec 2025 12:34:18 +0100 Subject: [PATCH 264/308] Ensure skip counter doesn't overflow the button --- .../OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 02f28c2c45..9e237483fe 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -303,6 +303,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Enabled.Value = false; return true; } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + countText.Scale = new Vector2(Math.Min(0.85f * aspect.DrawWidth / countText.DrawWidth, 1)); + } } } } From 6343bf7d298887daaedc16cd77c4eae476656a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 5 Dec 2025 12:35:26 +0100 Subject: [PATCH 265/308] Privatise setter --- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index e17d639a1b..0509c845f8 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Play private OsuClickableContainer button; private ButtonContainer buttonContainer; - protected Circle RemainingTimeBox; + protected Circle RemainingTimeBox { get; private set; } private double displayTime; private bool isClickable; From 8d33c35646a1e2ecef65d915ad93f74ee73142cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Dec 2025 20:50:07 +0900 Subject: [PATCH 266/308] 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 20c0f7cec5..cf8b91595f 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 c2796cf000..108d5760d5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 8e2230d14912fb9af70d1066213bd44fcf170302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 5 Dec 2025 13:05:51 +0100 Subject: [PATCH 267/308] Add xmldoc to confusing field I don't have any better ideas at this time. --- osu.Game/Screens/Play/SkipOverlay.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 0509c845f8..361de71103 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -50,7 +50,12 @@ namespace osu.Game.Screens.Play protected Circle RemainingTimeBox { get; private set; } private double displayTime; + + /// + /// Becomes when the overlay starts fading out. + /// private bool isClickable; + private bool skipQueued; [Resolved] From 1db4b897eb5f94ddae030f9e5ad89ecc90ea6d36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Dec 2025 21:27:11 +0900 Subject: [PATCH 268/308] Update tests to match new behaviour --- osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 8e671f331d..287b678b07 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Settings scrollToAndStartBinding("Increase volume"); AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft)); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); - checkBinding("Increase volume", "LShift"); + checkBinding("Increase volume", "Shift"); } [Test] @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft)); AddStep("press k", () => InputManager.Key(Key.K)); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); - checkBinding("Increase volume", "LShift-K"); + checkBinding("Increase volume", "Shift-K"); } [Test] From fbac5db964def0b076161f3fe35dc1949ec5b0eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Dec 2025 22:28:55 +0900 Subject: [PATCH 269/308] 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 cf8b91595f..fe5d204ceb 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 108d5760d5..58b0aa1c25 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From d1d76a76ba19eaa8ea1aac6b4a0bebae294f6079 Mon Sep 17 00:00:00 2001 From: Chirag Mahesh Date: Thu, 4 Dec 2025 16:40:58 +0000 Subject: [PATCH 270/308] Refactor trail scale update logic into a dedicated method --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 2 -- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 37ceb296b8..e84fb9e2d6 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -36,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One; - public Vector2 CurrentCursorScale => skinnableCursor.Scale; - /// /// The current rotation of the cursor. /// diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index df72f8be97..e04382d194 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -64,8 +64,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor var newScale = new Vector2(e.NewValue); rippleVisualiser.CursorScale = newScale; - if (cursorTrail.Drawable is CursorTrail trail) trail.CursorScale = newScale; + updateTrailScale(); }, true); + cursorTrail.OnSkinChanged += updateTrailScale; + } + + private void updateTrailScale() + { + if (cursorTrail.Drawable is CursorTrail trail) trail.CursorScale = new Vector2(ActiveCursor.CursorScale.Value); } private int downCount; @@ -86,7 +92,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { trail.NewPartScale = ActiveCursor.CurrentExpandedScale; trail.PartRotation = ActiveCursor.CurrentRotation; - trail.CursorScale = ActiveCursor.CurrentCursorScale; } } From 107098314a486ce8959020dd822223eab5ed5094 Mon Sep 17 00:00:00 2001 From: Chirag Mahesh Date: Fri, 5 Dec 2025 17:22:10 +0000 Subject: [PATCH 271/308] Move and refactor TestSceneGameplayCursorSizeChange into Ruleset Osu Tests --- .../TestSceneGameplayCursorSizeChange.cs | 52 ++++++++++++ .../TestSceneGameplayCursorSizeChange.cs | 85 ------------------- 2 files changed, 52 insertions(+), 85 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursorSizeChange.cs delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneGameplayCursorSizeChange.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursorSizeChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursorSizeChange.cs new file mode 100644 index 0000000000..27a83887dd --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursorSizeChange.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneGameplayCursorSizeChange : PlayerTestScene + { + private const float initial_cursor_size = 1f; + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + [Resolved] + private SkinManager? skins { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + if (skins != null) skins.CurrentSkinInfo.Value = skins.DefaultClassicSkin.SkinInfo; + } + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep($"Set gameplay cursor size: 1", () => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, initial_cursor_size)); + AddStep("resume player", () => Player.GameplayClockContainer.Start()); + AddUntilStep("clock running", () => Player.GameplayClockContainer.IsRunning); + } + + [Test] + public void TestPausedChangeCursorSize() + { + AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); + AddStep("move cursor to top left", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft)); + AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); + AddStep("move cursor to top right", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopRight)); + AddStep("press escape", () => InputManager.Key(Key.Escape)); + + AddSliderStep("cursor size", 0.1f, 2f, 1f, v => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, v)); + } + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false); + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayCursorSizeChange.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayCursorSizeChange.cs deleted file mode 100644 index 22b3a8e378..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayCursorSizeChange.cs +++ /dev/null @@ -1,85 +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.Allocation; -using osu.Framework.Screens; -using osu.Framework.Testing; -using osu.Game.Configuration; -using osu.Game.Rulesets; -using osu.Game.Skinning; -using osuTK.Input; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public partial class TestSceneGameplayCursorSizeChange : OsuPlayerTestScene - { - [Resolved] - private SkinManager? skins { get; set; } - - protected new PausePlayer Player => (PausePlayer)base.Player; - - [BackgroundDependencyLoader] - private void load() - { - if (skins != null) skins.CurrentSkinInfo.Value = skins.DefaultClassicSkin.SkinInfo; - } - - [SetUpSteps] - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("resume player", () => Player.GameplayClockContainer.Start()); - AddUntilStep("clock running", () => Player.GameplayClockContainer.IsRunning); - } - - [Test] - public void TestChangeCursorSize() - { - AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); - AddStep("move cursor to top left", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft)); - AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); - AddStep("move cursor to top right", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopRight)); - AddStep("press escape", () => InputManager.Key(Key.Escape)); - - for (float cursorSize = 0.4f; cursorSize <= 1.6f + 0.001f; cursorSize += 0.4f) - { - AddWaitStep("wait 2 seconds", 2); - float newCursorSize = cursorSize; - AddStep($"gameplay cursor size: {newCursorSize:F1}", () => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, newCursorSize)); - } - } - - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PausePlayer(); - - protected partial class PausePlayer : TestPlayer - { - public double LastPauseTime { get; private set; } - public double LastResumeTime { get; private set; } - - public override void OnEntering(ScreenTransitionEvent e) - { - base.OnEntering(e); - GameplayClockContainer.Stop(); - } - - private bool? isRunning; - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (GameplayClockContainer.IsRunning != isRunning) - { - isRunning = GameplayClockContainer.IsRunning; - - if (isRunning.Value) - LastResumeTime = GameplayClockContainer.CurrentTime; - else - LastPauseTime = GameplayClockContainer.CurrentTime; - } - } - } - } -} From d04029bcc7a0616c018ca0cf579866c6e53f48b7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 6 Dec 2025 03:24:17 +0900 Subject: [PATCH 272/308] Fix incorrect quick play download progress --- osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 0940fbdabb..9f35099516 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -520,6 +520,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private void onBeatmapAvailabilityChanged(MultiplayerRoomUser user, BeatmapAvailability availability) => Scheduler.Add(() => { + if (!user.Equals(RoomUser)) + return; + if (availability.State == DownloadState.Downloading) downloadProgressBar.FadeIn(200, Easing.OutPow10); else From 35fdc6f8b94784c83cd4d7040193c19810ed84bf Mon Sep 17 00:00:00 2001 From: Natelytle Date: Fri, 5 Dec 2025 22:51:00 -0500 Subject: [PATCH 273/308] Rank swap mod --- osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs index f1feb8153a..9af3eedf93 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray(); + public override bool Ranked => true; public void ApplyToBeatmap(IBeatmap beatmap) { From 2be50d917ac1fd47f8907a50817a45e13ae7d751 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 6 Dec 2025 13:23:16 +0900 Subject: [PATCH 274/308] Adjust vote-to-skip to be explicit about states --- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 2 +- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 13 ++++--------- .../Online/Multiplayer/OnlineMultiplayerClient.cs | 2 +- .../Multiplayer/MultiplayerSkipOverlay.cs | 2 +- .../Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index c91128401d..c94faf173c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -153,7 +153,7 @@ namespace osu.Game.Online.Multiplayer /// /// Signals that a user has requested to skip the beatmap intro. /// - Task UserVotedToSkipIntro(int userId); + Task UserVotedToSkipIntro(int userId, bool voted); /// /// Signals that the vote to skip the beatmap intro has passed. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 8a41c11ae6..209753ccd3 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -131,7 +131,7 @@ namespace osu.Game.Online.Multiplayer public event Action? MatchmakingItemDeselected; public event Action? MatchRoomStateChanged; - public event Action? UserVotedToSkipIntro; + public event Action? UserVotedToSkipIntro; public event Action? VoteToSkipIntroPassed; public event Action? BeatmapAvailabilityChanged; @@ -854,10 +854,6 @@ namespace osu.Game.Online.Multiplayer handleRoomRequest(() => { Debug.Assert(Room != null); - - foreach (var user in Room.Users) - user.VotedToSkipIntro = false; - GameplayStarted?.Invoke(); }); @@ -928,7 +924,7 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.UserVotedToSkipIntro(int userId) + Task IMultiplayerClient.UserVotedToSkipIntro(int userId, bool voted) { handleRoomRequest(() => { @@ -940,9 +936,8 @@ namespace osu.Game.Online.Multiplayer if (user == null) return; - user.VotedToSkipIntro = true; - - UserVotedToSkipIntro?.Invoke(userId); + user.VotedToSkipIntro = voted; + UserVotedToSkipIntro?.Invoke(userId, voted); }); return Task.CompletedTask; diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 1319578c06..7cfa036dcd 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded); connection.On(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved); connection.On(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged); - connection.On(nameof(IMultiplayerClient.UserVotedToSkipIntro), ((IMultiplayerClient)this).UserVotedToSkipIntro); + connection.On(nameof(IMultiplayerClient.UserVotedToSkipIntro), ((IMultiplayerClient)this).UserVotedToSkipIntro); connection.On(nameof(IMultiplayerClient.VoteToSkipIntroPassed), ((IMultiplayerClient)this).VoteToSkipIntroPassed); connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 9e237483fe..5a9c31b889 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) => Schedule(updateCount); - private void onUserVotedToSkipIntro(int userId) => Schedule(() => + private void onUserVotedToSkipIntro(int userId, bool voted) => Schedule(() => { FadingContent.TriggerShow(); updateCount(); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 38070d953e..2f8a3dc7a8 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -568,7 +568,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public async Task UserVoteToSkipIntro(int userId) { - await ((IMultiplayerClient)this).UserVotedToSkipIntro(userId).ConfigureAwait(false); + await ((IMultiplayerClient)this).UserVotedToSkipIntro(userId, true).ConfigureAwait(false); } protected override Task CreateRoomInternal(MultiplayerRoom room) From 4ae4c700ae3c1aafa02f1bc00f3a0d539728278c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 6 Dec 2025 17:36:16 +0900 Subject: [PATCH 275/308] Remove quick play round results scroll animation --- .../TestSceneRoundResultsScreen.cs | 14 +++- .../RoundResults/SubScreenRoundResults.cs | 66 ++++++++----------- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs index ff4e2c1932..d1800aca3f 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Utils; @@ -26,8 +27,15 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); + } - setupRequestHandler(); + [TestCase(2)] + [TestCase(4)] + [TestCase(8)] + [TestCase(16)] + public void TestDisplayScores(int scoreCount) + { + setupRequestHandler(scoreCount); AddStep("load screen", () => { @@ -40,7 +48,7 @@ namespace osu.Game.Tests.Visual.Matchmaking }); } - private void setupRequestHandler() + private void setupRequestHandler(int scoreCount) { AddStep("setup request handler", () => { @@ -71,7 +79,7 @@ namespace osu.Game.Tests.Visual.Matchmaking case IndexPlaylistScoresRequest index: var result = new IndexedMultiplayerScores(); - for (int i = 0; i < 8; ++i) + for (int i = 0; i < scoreCount; ++i) { result.Scores.Add(new MultiplayerScore { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs index 580d157a8b..d363be6cfb 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs @@ -31,8 +31,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults /// public partial class SubScreenRoundResults : MatchmakingSubScreen { - private const int panel_spacing = 5; - public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Hidden; public override Drawable? PlayersDisplayArea => null; @@ -51,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults [Resolved] private RulesetStore rulesets { get; set; } = null!; - private AutoScrollContainer scrollContainer = null!; + private PanelContainer panelContainer = null!; private LoadingSpinner loadingSpinner = null!; [BackgroundDependencyLoader] @@ -59,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults { InternalChildren = new Drawable[] { - scrollContainer = new AutoScrollContainer + panelContainer = new PanelContainer { RelativeSizeAxes = Axes.Both }, @@ -136,25 +134,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults private void setScores(ScoreInfo[] scores) => Scheduler.Add(() => { - Container panels; - - scrollContainer.Child = panels = new Container + panelContainer.ChildrenEnumerable = scores.Select(s => new RoundResultsScorePanel(s) { - 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); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); }); private partial class RoundResultsScorePanel : CompositeDrawable @@ -183,31 +167,37 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults } } - private partial class AutoScrollContainer : UserTrackingScrollContainer + private partial class PanelContainer : Container { - private const float initial_offset = -0.5f; - private const double scroll_duration = 20000; + protected override Container Content => flowContainer; - private double? scrollStartTime; + private readonly Container centreingContainer; + private readonly Container flowContainer; - public AutoScrollContainer() - : base(Direction.Horizontal) + public PanelContainer() { + InternalChild = new OsuScrollContainer(Direction.Horizontal) + { + RelativeSizeAxes = Axes.Both, + Child = centreingContainer = new Container + { + RelativeSizeAxes = Axes.Y, + Child = flowContainer = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Spacing = new Vector2(5) + } + } + }; } 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); - } + centreingContainer.Width = Math.Max(DrawWidth, flowContainer.DrawWidth); } } } From 1c10acba7662b6a64549e5c830d3ac57bac878fa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 6 Dec 2025 17:40:17 +0900 Subject: [PATCH 276/308] Allow score panel to animate --- .../RoundResults/SubScreenRoundResults.cs | 20 ++++--------------- osu.Game/Screens/Ranking/ScorePanel.cs | 4 ++-- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs index d363be6cfb..9dc283780a 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs @@ -23,6 +23,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Ranking; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults { @@ -145,26 +146,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults { public RoundResultsScorePanel(ScoreInfo score) { - AutoSizeAxes = Axes.Both; - InternalChild = new InstantSizingScorePanel(score); + Size = new Vector2(ScorePanel.CONTRACTED_WIDTH, ScorePanel.CONTRACTED_HEIGHT); + + InternalChild = new ScorePanel(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 PanelContainer : Container diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 85da1afe7b..72927ee6eb 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Ranking /// /// Height of the panel when contracted. /// - private const float contracted_height = 385; + public const float CONTRACTED_HEIGHT = 385; /// /// Width of the panel when expanded. @@ -259,7 +259,7 @@ namespace osu.Game.Screens.Ranking break; case PanelState.Contracted: - Size = new Vector2(CONTRACTED_WIDTH, contracted_height); + Size = new Vector2(CONTRACTED_WIDTH, CONTRACTED_HEIGHT); topLayerBackground.FadeColour(contracted_top_layer_colour, RESIZE_DURATION, Easing.OutQuint); middleLayerBackground.FadeColour(contracted_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint); From a96b024ac5780257f54539fa9a468684771c37bb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 6 Dec 2025 17:50:43 +0900 Subject: [PATCH 277/308] Make quick play chat retain focus after posting --- .../OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs index 6a01642907..98a08d6b17 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match resetPlaceholderText(); TextBox.HoldFocus = false; - TextBox.ReleaseFocusOnCommit = true; + TextBox.ReleaseFocusOnCommit = false; TextBox.Focus = () => TextBox.PlaceholderText = ChatStrings.InputPlaceholder; TextBox.FocusLost = resetPlaceholderText; From a6c001244ff44e140cfd1f5006fd370f4e2a20bd Mon Sep 17 00:00:00 2001 From: Chirag Mahesh Date: Sat, 6 Dec 2025 11:18:18 +0000 Subject: [PATCH 278/308] Redundant string interpolation --- .../TestSceneGameplayCursorSizeChange.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursorSizeChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursorSizeChange.cs index 27a83887dd..c94e575032 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursorSizeChange.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursorSizeChange.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests { base.SetUpSteps(); - AddStep($"Set gameplay cursor size: 1", () => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, initial_cursor_size)); + AddStep("Set gameplay cursor size: 1", () => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, initial_cursor_size)); AddStep("resume player", () => Player.GameplayClockContainer.Start()); AddUntilStep("clock running", () => Player.GameplayClockContainer.IsRunning); } From c23d6b7fd12bdc942408916163b6074e61489151 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 7 Dec 2025 02:11:26 +0900 Subject: [PATCH 279/308] Fix potentially unsafe quick play event handling --- 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 8a41c11ae6..9fca1d5780 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -1117,7 +1117,7 @@ namespace osu.Game.Online.Multiplayer Task IMatchmakingClient.MatchmakingItemSelected(int userId, long playlistItemId) { - Scheduler.Add(() => + handleRoomRequest(() => { MatchmakingItemSelected?.Invoke(userId, playlistItemId); RoomUpdated?.Invoke(); @@ -1128,7 +1128,7 @@ namespace osu.Game.Online.Multiplayer Task IMatchmakingClient.MatchmakingItemDeselected(int userId, long playlistItemId) { - Scheduler.Add(() => + handleRoomRequest(() => { MatchmakingItemDeselected?.Invoke(userId, playlistItemId); RoomUpdated?.Invoke(); From d6cd748d2ac6723b6653ee103a31060cc6c6f00e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 7 Dec 2025 23:26:40 +0900 Subject: [PATCH 280/308] Consider abandon time for user placements --- .../Matchmaking/MatchmakingRoomStateTest.cs | 29 +++++++++ .../MatchTypes/Matchmaking/MatchmakingUser.cs | 6 ++ .../Matchmaking/MatchmakingUserComparer.cs | 63 +++++++++++-------- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs index 5f82d22ae8..25766d4645 100644 --- a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs +++ b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.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.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; @@ -149,5 +150,33 @@ namespace osu.Game.Tests.Online.Matchmaking Assert.AreEqual(5, state.Users.GetOrAdd(5).Placement); Assert.AreEqual(6, state.Users.GetOrAdd(6).Placement); } + + [Test] + public void AbandonOrder() + { + var state = new MatchmakingRoomState(); + + state.AdvanceRound(); + state.RecordScores( + [ + new SoloScoreInfo { UserID = 1, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 2, TotalScore = 500 }, + ], placement_points); + + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + + state.Users.GetOrAdd(1).AbandonedAt = DateTimeOffset.Now; + state.RecordScores([], placement_points); + + Assert.AreEqual(2, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(2).Placement); + + state.Users.GetOrAdd(2).AbandonedAt = DateTimeOffset.Now - TimeSpan.FromMinutes(1); + state.RecordScores([], placement_points); + + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + } } } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs index ac97b114d8..94062d6024 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs @@ -36,5 +36,11 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking /// [Key(3)] public MatchmakingRoundList Rounds { get; set; } = new MatchmakingRoundList(); + + /// + /// The time at which this user abandoned the match. + /// + [Key(4)] + public DateTimeOffset? AbandonedAt { get; set; } } } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs index 74da6a9b2a..a81c49fe97 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs @@ -23,42 +23,53 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(y); - // X appears earlier in the list if it has more points. - if (x.Points > y.Points) - return -1; + int compare = compareAbandonedAt(x, y); + if (compare != 0) + return compare; - // Y appears earlier in the list if it has more points. - if (y.Points > x.Points) - return 1; + compare = comparePoints(x, y); + if (compare != 0) + return compare; - // Tiebreaker 1 (likely): From each user's point-of-view, their earliest and best placement. + compare = compareRoundPlacements(x, y); + if (compare != 0) + return compare; + + return compareUserIds(x, y); + } + + private int compareAbandonedAt(MatchmakingUser x, MatchmakingUser y) + { + DateTimeOffset xAbandonedAt = x.AbandonedAt ?? DateTimeOffset.MaxValue; + DateTimeOffset yAbandonedAt = y.AbandonedAt ?? DateTimeOffset.MaxValue; + return -xAbandonedAt.CompareTo(yAbandonedAt); + } + + private int comparePoints(MatchmakingUser x, MatchmakingUser y) + { + return -x.Points.CompareTo(y.Points); + } + + private int compareRoundPlacements(MatchmakingUser x, MatchmakingUser y) + { for (int r = 1; r <= rounds; r++) { - MatchmakingRound? xRound; - x.Rounds.RoundsDictionary.TryGetValue(r, out xRound); + x.Rounds.RoundsDictionary.TryGetValue(r, out var xRound); + y.Rounds.RoundsDictionary.TryGetValue(r, out var yRound); - MatchmakingRound? yRound; - y.Rounds.RoundsDictionary.TryGetValue(r, out yRound); + int xPlacement = xRound?.Placement ?? int.MaxValue; + int yPlacement = yRound?.Placement ?? int.MaxValue; - // 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); + int compare = xPlacement.CompareTo(yPlacement); if (compare != 0) return compare; } - // Tiebreaker 2 (unlikely): User ID. + return 0; + } + + private int compareUserIds(MatchmakingUser x, MatchmakingUser y) + { return x.UserId.CompareTo(y.UserId); } } From 4e4aa44a026ba2992f193e77b2e4f5b463788c2e Mon Sep 17 00:00:00 2001 From: rrex971 <75212090+rrex971@users.noreply.github.com> Date: Mon, 8 Dec 2025 02:17:35 +0530 Subject: [PATCH 281/308] Override sprite update method to handle alpha values > 1 like stable. If alpha exceeds 1 during a sprite's alpha transform like in a FadeTo(), it will set it to 0 mimicking stable's behavior. --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index e25c915d8b..69b49ba819 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -74,6 +74,13 @@ namespace osu.Game.Storyboards.Drawables public override bool IsPresent => !float.IsNaN(DrawPosition.X) && !float.IsNaN(DrawPosition.Y) && base.IsPresent; + // Match stable behavior with alpha values > 1 during interpolation (eg. overshoot easings like InOutElastic etc.) + protected override void Update() + { + base.Update(); + if (Alpha > 1) Alpha = 0; + } + [Resolved] private ISkinSource skin { get; set; } = null!; From f73307876e34d3a8da86c98d0eae3ddee52ae506 Mon Sep 17 00:00:00 2001 From: rrex971 <75212090+rrex971@users.noreply.github.com> Date: Mon, 8 Dec 2025 02:19:29 +0530 Subject: [PATCH 282/308] Also apply alpha logic to StoryboardAnimation sprites too. --- .../Storyboards/Drawables/DrawableStoryboardAnimation.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index f66f84af7a..3895101a51 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -89,6 +89,13 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = animation.EndTimeForDisplay; } + // Match stable behavior with alpha values > 1 during interpolation (eg. overshoot easings like InOutElastic etc.) + protected override void Update() + { + base.Update(); + if (Alpha > 1) Alpha = 0; + } + [Resolved] private ISkinSource skin { get; set; } From 8bb885a0dca60753b333f4c259a3af66f9f7bb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Dec 2025 09:54:46 +0100 Subject: [PATCH 283/308] Filter out more exceptions from being sent to sentry More or less covers the first page of client sentry issues sorted by volume, all of which is pretty much useless for anything because it's client-specific-failure noise. --- osu.Game/Online/API/APIAccess.cs | 2 +- osu.Game/Utils/SentryLogger.cs | 40 ++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 6694003b31..6fc897a8ff 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -603,7 +603,7 @@ namespace osu.Game.Online.API cancellationToken.Cancel(); } - private class WebRequestFlushedException : Exception + internal class WebRequestFlushedException : Exception { public WebRequestFlushedException(APIState state) : base($@"Request failed from flush operation (state {state})") diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 4f916f810e..aa1fd429a7 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -2,10 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,6 +21,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -220,20 +225,35 @@ namespace osu.Game.Utils } } + private static readonly HashSet ignored_io_exception_hresults = + [ + // see https://stackoverflow.com/a/9294382 for how these are synthesised + unchecked((int)0x80070020), // ERROR_SHARING_VIOLATION + unchecked((int)0x80070027), // ERROR_HANDLE_DISK_FULL + unchecked((int)0x80070070), // ERROR_DISK_FULL + ]; + private bool shouldSubmitException(Exception exception) { switch (exception) { - case IOException ioe: - // disk full exceptions, see https://stackoverflow.com/a/9294382 - const int hr_error_handle_disk_full = unchecked((int)0x80070027); - const int hr_error_disk_full = unchecked((int)0x80070070); + // disk I/O failures, invalid formats, etc. - if (ioe.HResult == hr_error_handle_disk_full || ioe.HResult == hr_error_disk_full) + case IOException ioe: + if (ignored_io_exception_hresults.Contains(ioe.HResult)) return false; break; + case UnauthorizedAccessException: + case SharpCompress.Common.InvalidFormatException: + return false; + + // connectivity failures + + case TimeoutException te: + return !te.Message.Contains(@"elapsed without receiving a message from the server"); + case WebException we: switch (we.Status) { @@ -243,6 +263,16 @@ namespace osu.Game.Utils } break; + + case WebSocketException: + case SocketException: + return false; + + // stuff that should really never make it to sentry + + case APIAccess.WebRequestFlushedException: + case TaskCanceledException: + return false; } return true; From 4c0522b7956ba14a7ab207f19661c6eea2a9c6cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Dec 2025 18:00:32 +0900 Subject: [PATCH 284/308] Update comment and fix formatting --- .../Drawables/DrawableStoryboardAnimation.cs | 10 ++++++++-- .../Storyboards/Drawables/DrawableStoryboardSprite.cs | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 3895101a51..41d5b768f3 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -89,13 +89,19 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = animation.EndTimeForDisplay; } - // Match stable behavior with alpha values > 1 during interpolation (eg. overshoot easings like InOutElastic etc.) protected override void Update() { base.Update(); + + // In stable, alpha transforms exceeding values of 1 would result in sprites disappearing from view. + // Over the years, storyboard(ers) have taken advantage of this to create "flicker" patterns. + // This is quite a common technique, so we are reproducing it here for now. + // + // NOTE TO FUTURE VISTIORS: If we do ever update the storyboard spec, we may want to move such flicker effects to their + // own transform type, and make this a legacy behaviour. It feels very flimsy. if (Alpha > 1) Alpha = 0; } - + [Resolved] private ISkinSource skin { get; set; } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 69b49ba819..9d2e7110da 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -74,10 +74,16 @@ namespace osu.Game.Storyboards.Drawables public override bool IsPresent => !float.IsNaN(DrawPosition.X) && !float.IsNaN(DrawPosition.Y) && base.IsPresent; - // Match stable behavior with alpha values > 1 during interpolation (eg. overshoot easings like InOutElastic etc.) protected override void Update() { base.Update(); + + // In stable, alpha transforms exceeding values of 1 would result in sprites disappearing from view. + // Over the years, storyboard(ers) have taken advantage of this to create "flicker" patterns. + // This is quite a common technique, so we are reproducing it here for now. + // + // NOTE TO FUTURE VISTIORS: If we do ever update the storyboard spec, we may want to move such flicker effects to their + // own transform type, and make this a legacy behaviour. It feels very flimsy. if (Alpha > 1) Alpha = 0; } From 84db28977970c5fec819290b225711e9c8b1d221 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Dec 2025 18:57:49 +0900 Subject: [PATCH 285/308] Use modulus instead of previous solution to match stable more closely --- .../Storyboards/Drawables/DrawableStoryboardAnimation.cs | 4 +++- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 41d5b768f3..4a61bf16fc 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -94,12 +94,14 @@ namespace osu.Game.Storyboards.Drawables base.Update(); // In stable, alpha transforms exceeding values of 1 would result in sprites disappearing from view. + // See https://github.com/peppy/osu-stable-reference/blob/08e3dafd525934cf48880b08e91c24ce4ad8b761/osu!/Graphics/Sprites/pSprite.cs#L413-L414 + // // Over the years, storyboard(ers) have taken advantage of this to create "flicker" patterns. // This is quite a common technique, so we are reproducing it here for now. // // NOTE TO FUTURE VISTIORS: If we do ever update the storyboard spec, we may want to move such flicker effects to their // own transform type, and make this a legacy behaviour. It feels very flimsy. - if (Alpha > 1) Alpha = 0; + if (Alpha > 1) Alpha %= 1; } [Resolved] diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 9d2e7110da..03138710cc 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -79,12 +79,14 @@ namespace osu.Game.Storyboards.Drawables base.Update(); // In stable, alpha transforms exceeding values of 1 would result in sprites disappearing from view. + // See https://github.com/peppy/osu-stable-reference/blob/08e3dafd525934cf48880b08e91c24ce4ad8b761/osu!/Graphics/Sprites/pSprite.cs#L413-L414 + // // Over the years, storyboard(ers) have taken advantage of this to create "flicker" patterns. // This is quite a common technique, so we are reproducing it here for now. // - // NOTE TO FUTURE VISTIORS: If we do ever update the storyboard spec, we may want to move such flicker effects to their + // NOTE TO FUTURE VISITORS: If we do ever update the storyboard spec, we may want to move such flicker effects to their // own transform type, and make this a legacy behaviour. It feels very flimsy. - if (Alpha > 1) Alpha = 0; + if (Alpha > 1) Alpha %= 1; } [Resolved] From 42b184f167724af9fa10be2079096dccb1338d7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Dec 2025 19:20:29 +0900 Subject: [PATCH 286/308] 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 fe5d204ceb..09c83cf37b 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 58b0aa1c25..aa5f4de8c9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 5c2df507141aa00af9a75516fda179b62b89d7f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Dec 2025 19:53:02 +0900 Subject: [PATCH 287/308] Add test coverage of weird storyboard sprite behaviour --- .../TestSceneDrawableStoryboardSprite.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 800857c973..494365a205 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -50,6 +50,34 @@ namespace osu.Game.Tests.Visual.Gameplay sprites.All(sprite => sprite.ChildrenOfType().All(s => s.Texture == null))); } + [Test] + public void TestSpriteFadeOverflowBehaviour() + { + AddStep("create sprite", () => SetContents(_ => + { + var layer = storyboard.GetLayer("Background"); + + var sprite = new StoryboardSprite(lookup_name, Anchor.TopLeft, new Vector2(256, 192)); + sprite.Commands.AddAlpha(Easing.None, Time.Current, Time.Current + 2000, 0, 2); + + layer.Elements.Clear(); + layer.Add(sprite); + + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + storyboard.CreateDrawable() + } + }; + })); + + AddUntilStep("sprite reached high opacity once", () => sprites.All(sprite => sprite.ChildrenOfType().All(s => s.Alpha > 0.8f))); + AddUntilStep("sprite reset to low opacity", () => sprites.All(sprite => sprite.ChildrenOfType().All(s => s.Alpha < 0.2f))); + AddUntilStep("sprite reached high opacity twice", () => sprites.All(sprite => sprite.ChildrenOfType().All(s => s.Alpha > 0.8f))); + } + [Test] public void TestLookupFromStoryboard() { From 691e8bcd05865a5bd877cb8199ef97da039c20ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Dec 2025 11:14:56 +0100 Subject: [PATCH 288/308] Fix storyboard samples not playing in editor Closes https://github.com/ppy/osu/issues/35954. --- osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs | 10 +++++++--- osu.Game/Screens/Edit/Editor.cs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs b/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs index 24b582b71b..07cb9f5be5 100644 --- a/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs +++ b/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs @@ -13,12 +13,14 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Screens.Edit; using osu.Game.Storyboards.Drawables; namespace osu.Game.Screens.Backgrounds { public partial class EditorBackgroundScreen : BackgroundScreen { + private readonly EditorBeatmap editorBeatmap; private readonly Container dimContainer; private CancellationTokenSource? cancellationTokenSource; @@ -36,8 +38,9 @@ namespace osu.Game.Screens.Backgrounds [Resolved] private IBindable beatmap { get; set; } = null!; - public EditorBackgroundScreen() + public EditorBackgroundScreen(EditorBeatmap editorBeatmap) { + this.editorBeatmap = editorBeatmap; InternalChild = dimContainer = new Container { RelativeSizeAxes = Axes.Both, @@ -58,10 +61,11 @@ namespace osu.Game.Screens.Backgrounds private IEnumerable createContent() => [ new BeatmapBackground(beatmap.Value) { RelativeSizeAxes = Axes.Both, }, - // this kooky container nesting is here because the storyboard needs a custom clock + // one reason for this kooky container nesting being here is that the storyboard needs a custom clock // but also needs it on an isolated-enough level that doesn't break screen stack expiry logic (which happens if the clock was put on `this`), // or doesn't make it literally impossible to fade the storyboard in/out in real time (which happens if the fade transforms were to be applied directly to the storyboard). - new Container + // another is that we need `EditorSkinProvidingContainer` so that storyboard sample lookups succeed. + new EditorSkinProvidingContainer(editorBeatmap) { RelativeSizeAxes = Axes.Both, Child = new DrawableStoryboard(beatmap.Value.Storyboard) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 05f74c8514..d40d36530a 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -480,7 +480,7 @@ namespace osu.Game.Screens.Edit [Resolved] private MusicController musicController { get; set; } - protected override BackgroundScreen CreateBackground() => new EditorBackgroundScreen(); + protected override BackgroundScreen CreateBackground() => new EditorBackgroundScreen(editorBeatmap); protected override void LoadComplete() { From c4f7dee82bec2e7f7d7339ffc2fe2b22cc5e3f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Dec 2025 05:40:48 +0100 Subject: [PATCH 289/308] Fix skin editor sometimes dropping anchor/origin specification on paste (#35957) * Add failing test for copy->paste not being idempotent * Ensure all elements on default skins use fixed anchors `UsesFixedAnchor` defaults to false, i.e. closest anchors. Combined with manual anchor / origin specs on some drawables, this would get default skins into impossible states wherein a drawable would use "closest anchor" but also explicitly specify anchor / origin that aren't closest, which horribly fails on attempting to copy and paste. Frankly shocked this has gone unnoticed for this long, and I regret not vetoing this "feature" more every time I see its tentacles spread to produce breakage of levels yet unseen. Does this commit contain major levels of suck? For sure. Do I have any better ideas that wouldn't consist of a multi-day rewrite or deletion of this "feature"? No. * Fix skin editor always applying closest anchor / origin on paste regardless of whether the component uses fixed anchor Self-explanatory. Should close https://github.com/ppy/osu/issues/29111 along with previous commit. --- .../Legacy/CatchLegacySkinTransformer.cs | 3 +++ .../Argon/ManiaArgonSkinTransformer.cs | 3 +++ .../Legacy/ManiaLegacySkinTransformer.cs | 3 +++ .../Legacy/OsuLegacySkinTransformer.cs | 3 +++ .../Argon/TaikoArgonSkinTransformer.cs | 3 +++ .../Default/TaikoTrianglesSkinTransformer.cs | 3 +++ .../Legacy/TaikoLegacySkinTransformer.cs | 3 +++ .../Visual/Gameplay/TestSceneSkinEditor.cs | 18 ++++++++++++++++++ osu.Game/Overlays/SkinEditor/SkinEditor.cs | 4 +++- osu.Game/Skinning/ArgonSkin.cs | 6 ++++++ osu.Game/Skinning/LegacySkin.cs | 6 ++++++ osu.Game/Skinning/TrianglesSkin.cs | 6 ++++++ 12 files changed, 60 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 4f9048b988..4704e83b76 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy leaderboard.Origin = Anchor.CentreLeft; leaderboard.X = 10; } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { Children = new Drawable[] diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index a71b8aa982..81cc52e925 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -57,6 +57,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon if (spectatorList != null) spectatorList.Position = new Vector2(36, -66); + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { new DrawableGameplayLeaderboard(), diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index addb96d2c3..690705664d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -122,6 +122,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy leaderboard.Origin = Anchor.CentreLeft; leaderboard.X = 10; } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { new LegacyManiaComboCounter(), diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 7118b6f95e..219e754dcc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -103,6 +103,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy leaderboard.Origin = Anchor.BottomLeft; leaderboard.Position = pos; } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { Children = new Drawable[] diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index b0a1c5d3f7..820af4ce54 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -51,6 +51,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon spectatorList.Anchor = Anchor.BottomLeft; spectatorList.Origin = Anchor.TopLeft; } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/TaikoTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/TaikoTrianglesSkinTransformer.cs index f627417889..1a73457c67 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/TaikoTrianglesSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/TaikoTrianglesSkinTransformer.cs @@ -51,6 +51,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default spectatorList.Origin = Anchor.TopLeft; spectatorList.Position = new Vector2(320, -280); } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 73d32a7933..a985bd362e 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -79,6 +79,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy spectatorList.Origin = Anchor.TopLeft; spectatorList.Position = pos; } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { new LegacyDefaultComboCounter(), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 97889eea4d..3b8e63c596 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; @@ -378,6 +379,23 @@ namespace osu.Game.Tests.Visual.Gameplay () => Is.EqualTo(3)); } + [Test] + public void TestCopyPasteIdempotency() + { + string state = null!; + AddStep("select everything", () => InputManager.Keys(PlatformAction.SelectAll)); + AddStep("dump state", () => + { + state = JsonConvert.SerializeObject(skinEditor.SelectedComponents.Cast().Select(s => s.CreateSerialisedInfo()).ToArray()); + }); + AddStep("copy", () => InputManager.Keys(PlatformAction.Copy)); + AddStep("delete", () => InputManager.Keys(PlatformAction.Delete)); + AddStep("paste", () => InputManager.Keys(PlatformAction.Paste)); + AddAssert("pasted state equals dumped", + () => JsonConvert.SerializeObject(skinEditor.SelectedComponents.Cast().Select(s => s.CreateSerialisedInfo()).ToArray()), + () => Is.EqualTo(state)); + } + private SkinnableContainer globalHUDTarget => Player.ChildrenOfType() .Single(c => c.Lookup.Lookup == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null); diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 823456dddd..2903c867f7 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -529,7 +529,9 @@ namespace osu.Game.Overlays.SkinEditor } SelectedComponents.Add(component); - SkinSelectionHandler.ApplyClosestAnchorOrigin(drawableComponent); + + if (!component.UsesFixedAnchor) + SkinSelectionHandler.ApplyClosestAnchorOrigin(drawableComponent); return true; } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 9e8fe4f617..6d922e1402 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -128,6 +128,9 @@ namespace osu.Game.Skinning if (spectatorList != null) spectatorList.Position = pos; + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { RelativeSizeAxes = Axes.Both, @@ -238,6 +241,9 @@ namespace osu.Game.Skinning keyCounter.Position = new Vector2(-(hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); } } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; } }) { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 6ff569869a..74b404a578 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -412,6 +412,9 @@ namespace osu.Game.Skinning leaderboard.Origin = Anchor.CentreLeft; leaderboard.X = 10; } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { new LegacyDefaultComboCounter(), @@ -448,6 +451,9 @@ namespace osu.Game.Skinning hitError.Origin = Anchor.CentreLeft; hitError.Rotation = -90; } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { Children = new Drawable[] diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 3881a5e970..ae3df35383 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -106,6 +106,9 @@ namespace osu.Game.Skinning spectatorList.Origin = Anchor.BottomLeft; spectatorList.Position = new Vector2(screen_edge_padding, -(song_progress_offset_height + screen_edge_padding)); } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { RelativeSizeAxes = Axes.Both, @@ -178,6 +181,9 @@ namespace osu.Game.Skinning keyCounter.Origin = Anchor.BottomRight; keyCounter.Position = new Vector2(-screen_edge_padding, -(song_progress_offset_height + screen_edge_padding)); } + + foreach (var d in container.OfType()) + d.UsesFixedAnchor = true; }) { Children = new Drawable[] From 86054497d0322f6fef04dc0a12b6b0f9b8cc1d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Dec 2025 05:42:15 +0100 Subject: [PATCH 290/308] Disable save replay on fail overlay when spectating (#35942) "Closes" https://github.com/ppy/osu/issues/35920. The button can't easily work anyway since it's not guaranteed that the spectating user has all of the frames of the replay (think entering spectate midway through a play). This matches the results screen in spectator too. --- osu.Game/Screens/Play/FailOverlay.cs | 2 +- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/SaveFailedScoreButton.cs | 13 +++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/FailOverlay.cs b/osu.Game/Screens/Play/FailOverlay.cs index 4a0a6f573c..f5f1bf37e6 100644 --- a/osu.Game/Screens/Play/FailOverlay.cs +++ b/osu.Game/Screens/Play/FailOverlay.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play { public partial class FailOverlay : GameplayMenuOverlay { - public Func>? SaveReplay; + public Func>? SaveReplay { get; init; } public override LocalisableString Header => GameplayMenuOverlayStrings.FailedHeader; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9988bbcd93..21e0d3c8b9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -316,7 +316,7 @@ namespace osu.Game.Screens.Play }, FailOverlay = new FailOverlay { - SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false), + SaveReplay = Configuration.AllowUserInteraction ? async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false) : null, OnRetry = Configuration.AllowUserInteraction ? () => Restart() : null, OnQuit = () => PerformExitWithConfirmation(), }, diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index e5c9e115d1..af323281bd 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -96,8 +96,17 @@ namespace osu.Game.Screens.Play break; default: - button.TooltipText = @"save score"; - button.Enabled.Value = true; + if (importFailedScore != null) + { + button.TooltipText = @"save score"; + button.Enabled.Value = true; + } + else + { + button.TooltipText = @"replay unavailable"; + button.Enabled.Value = false; + } + break; } }, true); From 095a67c24e57922f5e3c9190cc50d641f45ca1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Dec 2025 05:47:08 +0100 Subject: [PATCH 291/308] Fix dragging volume meter to adjust volume closing overlays if mouse is released outside of overlay content (#35940) * Add failing test * Fix dragging volume meter to adjust volume closing overlays if mouse is released outside of overlay content Fixes https://osu.ppy.sh/community/forums/topics/2159553. --- .../Navigation/TestSceneScreenNavigation.cs | 30 +++++++++++++++++++ osu.Game/Overlays/Volume/VolumeMeter.cs | 2 ++ 2 files changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8a0c9f561c..ac6352753f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -24,6 +24,7 @@ using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Notifications.WebSocket; @@ -33,6 +34,7 @@ using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Toolbar; +using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.Configuration; @@ -1331,6 +1333,34 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("still nothing selected", () => Game.Beatmap.IsDefault); } + [Test] + public void TestVolumeMeterDragDoesNotDismissFocusedOverlay() + { + AddStep("show beatmap overlay", () => Game.ShowBeatmapSet(1)); + AddUntilStep("beatmap overlay still visible", + () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, + () => Is.EqualTo(Visibility.Visible)); + AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); + AddStep("move to centre", () => InputManager.MoveMouseTo(Game)); + AddStep("alt-scroll down", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(-1); + InputManager.ReleaseKey(Key.AltLeft); + }); + AddUntilStep("wait for volume overlay to show", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddStep("start dragging meter", () => + { + InputManager.MoveMouseTo(Game.ChildrenOfType().First().ChildrenOfType().First()); + InputManager.PressButton(MouseButton.Left); + }); + AddStep("drag away", () => InputManager.MoveMouseTo(Game.ChildrenOfType().First().ChildrenOfType().First(), new Vector2(0, -100))); + AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("beatmap overlay still visible", + () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, + () => Is.EqualTo(Visibility.Visible)); + } + private Func playToResults() { var player = playToCompletion(); diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 9e0c599386..ed9c5c13ef 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -321,6 +321,8 @@ namespace osu.Game.Overlays.Volume private float dragDelta; + protected override bool OnMouseDown(MouseDownEvent e) => true; // handle to prevent drawables behind from potentially receiving the mouse down + protected override bool OnDragStart(DragStartEvent e) { dragDelta = 0; From 1faf02e8604a2d40e9a0cd2c2f0e9042046fd3cc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 11 Dec 2025 13:53:09 +0900 Subject: [PATCH 292/308] Update localisation analyser packages --- .config/dotnet-tools.json | 2 +- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6ec071be2f..17a371079a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2024.802.0", + "version": "2025.1208.0", "commands": [ "localisation" ] diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ee23bc7f72..ab2304dcfb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From f71eb4b9801e8792d61f59ff9c44b0cf22103691 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Dec 2025 16:00:13 +0900 Subject: [PATCH 293/308] Debounce track seeks only when track is playing This fixes the editor no longer seeking smoothly when paused. Closes https://github.com/ppy/osu/issues/35963. --- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- .../Edit/Components/Timelines/Summary/Parts/MarkerPart.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 28119615f3..eb4e2db5d1 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -228,7 +228,7 @@ namespace osu.Game.Overlays private void onSeek(double progress) { - if (lastSeekTime == null || Time.Current - lastSeekTime > TRACK_DRAG_SEEK_DEBOUNCE) + if (!musicController.IsPlaying || lastSeekTime == null || Time.Current - lastSeekTime > TRACK_DRAG_SEEK_DEBOUNCE) { musicController.SeekTo(progress); lastSeekTime = Time.Current; diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index b7453bf7f6..1783bfb1e5 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts double seekDestination = markerPos / DrawWidth * editorClock.TrackLength; marker.X = (float)seekDestination; - if (!instant && lastSeekTime != null && Time.Current - lastSeekTime < NowPlayingOverlay.TRACK_DRAG_SEEK_DEBOUNCE) + if (editorClock.IsRunning && !instant && lastSeekTime != null && Time.Current - lastSeekTime < NowPlayingOverlay.TRACK_DRAG_SEEK_DEBOUNCE) return; editorClock.SeekSmoothlyTo(seekDestination); From b30047def626257c1e6e4d336ef51b5a90f48c66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Dec 2025 17:55:12 +0900 Subject: [PATCH 294/308] Remove audio adjustments immediately on gameplay hotkey overlays Closes #22164. --- osu.Game/Overlays/HoldToConfirmOverlay.cs | 7 ++++++- osu.Game/Screens/Play/HotkeyExitOverlay.cs | 9 +++++++++ osu.Game/Screens/Play/HotkeyRetryOverlay.cs | 9 +++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs index 6cdbc6450d..b9c32f9f8c 100644 --- a/osu.Game/Overlays/HoldToConfirmOverlay.cs +++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs @@ -61,10 +61,15 @@ namespace osu.Game.Overlays audio.Samples.AddAdjustment(AdjustableProperty.Volume, audioVolume); } - protected override void Dispose(bool isDisposing) + protected void RemoveAudioAdjustments() { audio?.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume); audio?.Samples.RemoveAdjustment(AdjustableProperty.Volume, audioVolume); + } + + protected override void Dispose(bool isDisposing) + { + RemoveAudioAdjustments(); base.Dispose(isDisposing); } } diff --git a/osu.Game/Screens/Play/HotkeyExitOverlay.cs b/osu.Game/Screens/Play/HotkeyExitOverlay.cs index bcd9bd7cd6..0908e044d0 100644 --- a/osu.Game/Screens/Play/HotkeyExitOverlay.cs +++ b/osu.Game/Screens/Play/HotkeyExitOverlay.cs @@ -27,5 +27,14 @@ namespace osu.Game.Screens.Play AbortConfirm(); } + + protected override void Confirm() + { + base.Confirm(); + + // Not removing immediately can lead to delays due to async disposal. + // This is done here rather than in `Player` because it's simpler to handle. + RemoveAudioAdjustments(); + } } } diff --git a/osu.Game/Screens/Play/HotkeyRetryOverlay.cs b/osu.Game/Screens/Play/HotkeyRetryOverlay.cs index 11d0b4f84f..044aab7021 100644 --- a/osu.Game/Screens/Play/HotkeyRetryOverlay.cs +++ b/osu.Game/Screens/Play/HotkeyRetryOverlay.cs @@ -27,5 +27,14 @@ namespace osu.Game.Screens.Play AbortConfirm(); } + + protected override void Confirm() + { + base.Confirm(); + + // Not removing immediately can lead to delays due to async disposal. + // This is done here rather than in `Player` because it's simpler to handle. + RemoveAudioAdjustments(); + } } } From 6ce8b0a4bc7194286badb5d5f5d79c8445159620 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Dec 2025 18:54:38 +0900 Subject: [PATCH 295/308] Slightly delay song select leaderboard's loading placeholder to avoid flashing during local score retrieval Closes #35893. --- .../Screens/SelectV2/BeatmapLeaderboardWedge.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 6e0ffafa63..af41a528b3 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -407,15 +407,27 @@ namespace osu.Game.Screens.SelectV2 private LeaderboardState displayedState; + private ScheduledDelegate? loadingShowDelegate; + protected void SetState(LeaderboardState state) { if (state == displayedState) return; if (state == LeaderboardState.Retrieving) - loading.Show(); + { + // Slight delay so this doesn't display for a few silly frames for local score retrievals. + loadingShowDelegate ??= Scheduler.AddDelayed(() => loading.Show(), 200); + } else + { + loadingShowDelegate?.Cancel(); + loadingShowDelegate = null; + loading.Hide(); + } + + loading.Hide(); displayedState = state; From bbdd70c8431b1c95f4f7cd8bfad0414ffd82d1ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 11 Dec 2025 20:07:36 +0900 Subject: [PATCH 296/308] Always perform leave room sequence --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4f223cc1ab..59a26d5ba7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -319,9 +319,6 @@ namespace osu.Game.Online.Multiplayer public Task LeaveRoom() { - if (Room == null) - return Task.CompletedTask; - // The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled. // This includes the setting of Room itself along with the initial update of the room settings on join. joinCancellationSource?.Cancel(); From c17db2cdd0c3ce81a3648fe5a12e32ec88f4ed7b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 11 Dec 2025 20:07:44 +0900 Subject: [PATCH 297/308] Forcefully leave room on multiplayer exit --- osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index eb387b2664..b58041aa6f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -90,6 +90,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); } + public override bool OnExiting(ScreenExitEvent e) + { + if (base.OnExiting(e)) + return true; + + client.LeaveRoom().FireAndForget(); + return false; + } + protected override string ScreenTitle => "Multiplayer"; protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); From 79151ae5b4fb12249e850cb10baddb3f625a067d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 12 Dec 2025 15:46:00 +0900 Subject: [PATCH 298/308] Remove mention of exception that doesn't exist --- osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 169d5d1b83..836b9efb10 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer /// /// Request to leave the currently joined room. /// - /// If the user is not in a room. Task LeaveRoom(); /// From 1aff418981353f35dca3059b6a4179f7897ab005 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 12 Dec 2025 17:57:50 +0900 Subject: [PATCH 299/308] Reword waiting text --- osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs index 8eaa280794..19b75cecd7 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs @@ -403,7 +403,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Waiting for all players...", + Text = "Waiting for opponents...", Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), }, new LoadingSpinner From 7853abe8aa9b79504ebc0b1eb709737624ec2282 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 12 Dec 2025 18:02:20 +0900 Subject: [PATCH 300/308] Move to queue screen when clicking notification --- .../Matchmaking/Queue/QueueController.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index f72f26f26e..6d23dd4eb1 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -169,6 +169,20 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { Text = "Searching for opponents..."; + Activated = () => + { + performer?.PerformFromScreen(s => + { + if (s is ScreenIntro || s is ScreenQueue) + return; + + s.Push(new ScreenIntro()); + }, [typeof(ScreenIntro), typeof(ScreenQueue)]); + + // Closed when appropriate by SearchInForeground(). + return false; + }; + CompletionClickAction = () => { client.MatchmakingAcceptInvitation().FireAndForget(); From 1c463aa06045af1b2e4af82444e26034b292f54e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 12 Dec 2025 18:26:50 +0900 Subject: [PATCH 301/308] Automatically accept invitation in queue screen --- .../Matchmaking/Queue/ScreenQueue.cs | 34 ++----------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs index 19b75cecd7..e446a85344 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs @@ -354,37 +354,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue 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 SelectionButton(200) - { - DarkerColour = colours.YellowDark, - LighterColour = colours.YellowLight, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = () => - { - client.MatchmakingAcceptInvitation().FireAndForget(); - SetState(MatchmakingScreenState.AcceptedWaitingForRoom); - }, - Text = "Join match!", - } - } - }; + client.MatchmakingAcceptInvitation().FireAndForget(); + SetState(MatchmakingScreenState.AcceptedWaitingForRoom); + matchFoundSample?.Play(); musicController.DuckMomentarily(1250); break; From 881a35b3824e5620c293d7240cf5e288da91dbb4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 15 Dec 2025 03:54:09 -0500 Subject: [PATCH 302/308] Fix skip overlay potentially not allowing skipping --- .../Multiplayer/MultiplayerSkipOverlay.cs | 14 +++++++++-- osu.Game/Screens/Play/SkipOverlay.cs | 24 ++++++++++--------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 5a9c31b889..e44cb16f8e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -42,10 +42,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { } - protected override OsuClickableContainer CreateButton() => skipButton = new Button + protected override OsuClickableContainer CreateButton(IBindable inSkipPeriod) => skipButton = new Button { Anchor = Anchor.Centre, Origin = Anchor.Centre, + InSkipPeriod = { BindTarget = inSkipPeriod }, }; protected override void LoadComplete() @@ -119,6 +120,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public readonly BindableInt SkippedCount = new BindableInt(); public readonly BindableInt RequiredCount = new BindableInt(); + public readonly BindableBool InSkipPeriod = new BindableBool(); + + private readonly BindableBool clicked = new BindableBool(); [Resolved] private OsuColour colours { get; set; } = null!; @@ -201,11 +205,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SkippedCount.BindValueChanged(_ => updateCount()); RequiredCount.BindValueChanged(_ => updateCount(), true); + + InSkipPeriod.BindValueChanged(_ => updateEnabledState()); + clicked.BindValueChanged(_ => updateEnabledState(), true); + Enabled.BindValueChanged(_ => updateColours(), true); FinishTransforms(true); } + private void updateEnabledState() => Enabled.Value = InSkipPeriod.Value && !clicked.Value; + private void updateChevronsSpacing() { if (SkippedCount.Value > 0 && RequiredCount.Value > 1) @@ -300,7 +310,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.OnClick(e); - Enabled.Value = false; + clicked.Value = true; return true; } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 361de71103..f9752706ad 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -52,9 +53,9 @@ namespace osu.Game.Screens.Play private double displayTime; /// - /// Becomes when the overlay starts fading out. + /// Whether the gameplay clock is currently at the skippable period. /// - private bool isClickable; + private readonly BindableBool inSkipPeriod = new BindableBool(); private bool skipQueued; @@ -92,7 +93,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - button = CreateButton(), + button = CreateButton(inSkipPeriod), RemainingTimeBox = new Circle { Height = 5, @@ -106,10 +107,15 @@ namespace osu.Game.Screens.Play }; } - protected virtual OsuClickableContainer CreateButton() => new Button + /// + /// Creates a skip button. + /// + /// Whether the gameplay clock is currently at the skippable period. + protected virtual OsuClickableContainer CreateButton(IBindable inSkipPeriod) => new Button { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Enabled = { BindTarget = inSkipPeriod }, }; private const double fade_time = 300; @@ -187,17 +193,13 @@ namespace osu.Game.Screens.Play RemainingTimeBox.Width = (float)Interpolation.Lerp(RemainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); - isClickable = progress > 0; - - if (!isClickable) - button.Enabled.Value = false; - - buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden; + inSkipPeriod.Value = progress > 0; + buttonContainer.State.Value = inSkipPeriod.Value ? Visibility.Visible : Visibility.Hidden; } protected override bool OnMouseMove(MouseMoveEvent e) { - if (isClickable && !e.HasAnyButtonPressed) + if (inSkipPeriod.Value && !e.HasAnyButtonPressed) FadingContent.TriggerShow(); return base.OnMouseMove(e); From dcb6d712870c86956a21fbf4d788e0b51df20e59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Dec 2025 19:07:46 +0900 Subject: [PATCH 303/308] Adjust constant and documentation slightly --- osu.Game/Skinning/LegacyPerformancePointsCounter.cs | 2 +- osu.Game/Skinning/LegacySpriteText.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/LegacyPerformancePointsCounter.cs b/osu.Game/Skinning/LegacyPerformancePointsCounter.cs index e59a4a80b4..a71ecaabde 100644 --- a/osu.Game/Skinning/LegacyPerformancePointsCounter.cs +++ b/osu.Game/Skinning/LegacyPerformancePointsCounter.cs @@ -37,7 +37,7 @@ namespace osu.Game.Skinning } } - protected override LocalisableString FormatCount(int count) => count.ToString($@"0'{LegacySpriteText.PP}'"); + protected override LocalisableString FormatCount(int count) => count.ToString($@"0'{LegacySpriteText.PP_SUFFIX_CHAR}'"); protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score); } diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index dca258e6eb..e307577c2f 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -15,9 +15,9 @@ namespace osu.Game.Skinning public sealed partial class LegacySpriteText : OsuSpriteText { /// - /// The Private Use Area character representing performance points. + /// The Private Use Area character for internally representing the "pp" suffix for performance counters. /// - public const char PP = '\uebd9'; + public const char PP_SUFFIX_CHAR = '\uebd9'; public Vector2? MaxSizePerGlyph { get; init; } public bool FixedWidth { get; init; } @@ -28,7 +28,7 @@ namespace osu.Game.Skinning protected override char FixedWidthReferenceCharacter => '5'; - protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x', PP }; + protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x', PP_SUFFIX_CHAR }; // ReSharper disable once UnusedMember.Global // being unused is the point here @@ -121,7 +121,7 @@ namespace osu.Game.Skinning case '%': return "percent"; - case PP: + case PP_SUFFIX_CHAR: return "pp"; default: From 1142be45ec7a7527020c9ff82cc2d791b9064b94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Dec 2025 19:07:59 +0900 Subject: [PATCH 304/308] Update resources --- osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs | 2 +- osu.Game/Overlays/Rankings/RankingsScope.cs | 4 ++-- osu.Game/Overlays/RankingsOverlay.cs | 4 ++-- osu.Game/osu.Game.csproj | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index 0eaa6ce827..63f0d5eb76 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Rankings case RankingsScope.Performance: case RankingsScope.Score: case RankingsScope.Country: - case RankingsScope.Spotlights: + case RankingsScope.Playlists: return true; default: diff --git a/osu.Game/Overlays/Rankings/RankingsScope.cs b/osu.Game/Overlays/Rankings/RankingsScope.cs index 658732a1b1..6822783b04 100644 --- a/osu.Game/Overlays/Rankings/RankingsScope.cs +++ b/osu.Game/Overlays/Rankings/RankingsScope.cs @@ -17,8 +17,8 @@ namespace osu.Game.Overlays.Rankings [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeCountry))] Country, - [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeCharts))] - Spotlights, + [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypePlaylists))] + Playlists, [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeKudosu))] Kudosu, diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 6a32515cbc..71f0010d42 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays ruleset.BindValueChanged(_ => { - if (Header.Current.Value == RankingsScope.Spotlights) + if (Header.Current.Value == RankingsScope.Playlists) return; Scheduler.AddOnce(triggerTabChanged); @@ -99,7 +99,7 @@ namespace osu.Game.Overlays { lastRequest?.Cancel(); - if (Header.Current.Value == RankingsScope.Spotlights) + if (Header.Current.Value == RankingsScope.Playlists) { LoadDisplay(new SpotlightsLayout { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ab2304dcfb..a2dabe7a9f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 1e79c562403cf5a32e1ee6d31b2c9c7d95488b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Dec 2025 09:48:10 +0100 Subject: [PATCH 305/308] Fix replay fail indicator not using fail sample from beatmap skin Closes https://github.com/ppy/osu/issues/36003. The duplicated `RulesetSkinProvidingContainer` is unfortunate but it's either this or I start doing proxy shenanigans. --- osu.Game/Screens/Play/ReplayPlayer.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 1c583609d9..7fec8d6332 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -18,6 +18,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Skinning; using osu.Game.Users; namespace osu.Game.Screens.Play @@ -100,15 +101,18 @@ namespace osu.Game.Screens.Play playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - AddInternal(failIndicator = new ReplayFailIndicator(GameplayClockContainer) + AddInternal(new RulesetSkinProvidingContainer(GameplayState.Ruleset, GameplayState.Beatmap, Beatmap.Value.Skin) { - GoToResults = () => + Child = failIndicator = new ReplayFailIndicator(GameplayClockContainer) { - if (!this.IsCurrentScreen()) - return; + GoToResults = () => + { + if (!this.IsCurrentScreen()) + return; - ValidForResume = false; - this.Push(new SoloResultsScreen(Score.ScoreInfo)); + ValidForResume = false; + this.Push(new SoloResultsScreen(Score.ScoreInfo)); + } } }); } From 032912e62b1400570e493df5f8725fd52450d491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Dec 2025 11:16:13 +0100 Subject: [PATCH 306/308] Adjust message on successful report to match bancho Addresses https://github.com/ppy/osu/discussions/36004. Not adding localisation because the previous implementation was `.ToString()`ing anyway. Would have made the abuse e-mail a link but `mailto:` doesn't work with `MessageFormatter` and I don't want to go into that right now. The message *almost* matches stable. The "almost" is because it doesn't mention the `/ignore` chat command. I was just going to implement the command, but I went to check what it does, and backed away slowly because it has like weird scoping to chat, highlights, and PMs, so `nope.avi`. --- osu.Game/Overlays/Chat/ReportChatPopover.cs | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs index 265a17c799..42aa92f5c2 100644 --- a/osu.Game/Overlays/Chat/ReportChatPopover.cs +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -33,7 +33,27 @@ namespace osu.Game.Overlays.Chat { var request = new ChatReportRequest(message.Id, reason, comments); - request.Success += () => channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(UsersStrings.ReportThanks.ToString())); + request.Success += () => + { + string thanksMessage; + + switch (channelManager.CurrentChannel.Value.Type) + { + case ChannelType.PM: + thanksMessage = """ + Chat moderators have been alerted. You have reported a private message so they will not be able to read history to maintain your privacy. Please make sure to include as much details as you can. + You can submit a second report with more details if required, or contact abuse@ppy.sh if a user is being extremely offensive. + You can also block a user via the block button on their user profile, or by right-clicking on their name in the chat and selecting "Block". + """; + break; + + default: + thanksMessage = @"Chat moderators have been alerted. Thanks for your help."; + break; + } + + channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(thanksMessage)); + }; api.Queue(request); } From 07817dce70e8251e2ccfe73c2965c424a3eb42b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Dec 2025 14:28:08 +0900 Subject: [PATCH 307/308] Expose notification main content for external use --- osu.Game/Overlays/Notifications/Notification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 8a2a7cee81..dd4e1cb3b0 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -76,7 +76,7 @@ namespace osu.Game.Overlays.Notifications protected override Container Content => content; - protected Container MainContent; + public Container MainContent; private readonly DragContainer dragContainer; From 3b635f691958ebbceecbf5e07c6805a23b56feab Mon Sep 17 00:00:00 2001 From: AV <66049742+aviollaz@users.noreply.github.com> Date: Wed, 17 Dec 2025 02:40:08 -0300 Subject: [PATCH 308/308] Prevent mod track adjustments from modifying BGM speed in queue (#36027) * Fix(Matchmaking): Prevent mod track adjustments from applying BGM speed in queue * replaced modification of MusicController directly with a property change. * formatted --- osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs index e446a85344..8dcd8f58f5 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs @@ -43,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { public override bool ShowFooter => true; + public override bool? ApplyModTrackAdjustments => false; + private Container mainContent = null!; private MatchmakingScreenState state;