From 3666e4c3320090bb4efc1bbb54ab252f2a390b57 Mon Sep 17 00:00:00 2001 From: Kian Masri Date: Tue, 10 Dec 2024 09:50:48 -0700 Subject: [PATCH 001/129] 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/129] 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/129] 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 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 004/129] 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 afdebcf188392efd4494661b19140bc4c9cf5946 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 22 Oct 2025 16:35:29 +0300 Subject: [PATCH 005/129] 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 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 006/129] 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 7b55b9e4f285483d4aa3a9563d64d6394c8ecda7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 9 Nov 2025 02:07:13 +0300 Subject: [PATCH 007/129] 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 008/129] 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 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 009/129] 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 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 010/129] 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 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 011/129] 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 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 012/129] 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 e349a597bae4618cc474505348d00f74c3325bd6 Mon Sep 17 00:00:00 2001 From: marvin Date: Mon, 17 Nov 2025 07:29:27 +0100 Subject: [PATCH 013/129] 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 014/129] 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 015/129] 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 016/129] 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 017/129] 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 018/129] 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 019/129] 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 020/129] 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 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 021/129] 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 022/129] 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 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 023/129] 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 277f4268db271250a43cdb46bcdd45ae7bb98b9d Mon Sep 17 00:00:00 2001 From: marvin Date: Tue, 18 Nov 2025 19:33:59 +0100 Subject: [PATCH 024/129] 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 025/129] 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 026/129] 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 027/129] 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 028/129] 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 029/129] 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 030/129] 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 031/129] 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 032/129] 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 033/129] 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 034/129] 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 035/129] 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 036/129] 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 037/129] 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 038/129] 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 039/129] 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 040/129] 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 041/129] 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 042/129] 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 043/129] 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 044/129] 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 045/129] 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 046/129] 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 047/129] 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 048/129] 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 049/129] 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 050/129] 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 051/129] 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 052/129] 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 053/129] 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 054/129] 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 055/129] 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 056/129] 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 057/129] 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 058/129] 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 059/129] 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 060/129] 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 061/129] 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 062/129] 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 063/129] 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 064/129] 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 065/129] 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 066/129] 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 067/129] 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 068/129] 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 069/129] 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 070/129] 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 071/129] 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 072/129] 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 073/129] 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 074/129] 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 075/129] 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 076/129] 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 077/129] 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 078/129] 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 079/129] 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 080/129] 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 081/129] 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 082/129] 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 083/129] 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 084/129] 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 2c9fc32756e543113f72042e6eb9ec454c43541c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Nov 2025 16:39:39 +0900 Subject: [PATCH 085/129] 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 086/129] 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 087/129] 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 088/129] 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 089/129] 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 090/129] 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 091/129] 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 092/129] 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 093/129] 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 094/129] 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 095/129] 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 096/129] 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 097/129] 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 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 098/129] 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 099/129] 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 100/129] 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 101/129] 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 82f4406c7972e7de01b06ea89a91034da02838ef Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 30 Nov 2025 05:14:02 -0500 Subject: [PATCH 102/129] 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 103/129] 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 104/129] 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 105/129] 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 106/129] 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 107/129] 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 108/129] 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 109/129] 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 110/129] 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 111/129] 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 112/129] 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 113/129] 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 114/129] 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 115/129] 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 116/129] 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 117/129] 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 118/129] 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 119/129] 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 120/129] 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 121/129] 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 122/129] 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 123/129] 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 d04029bcc7a0616c018ca0cf579866c6e53f48b7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 6 Dec 2025 03:24:17 +0900 Subject: [PATCH 124/129] 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 125/129] 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 4ae4c700ae3c1aafa02f1bc00f3a0d539728278c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 6 Dec 2025 17:36:16 +0900 Subject: [PATCH 126/129] 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 127/129] 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 128/129] 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 d6cd748d2ac6723b6653ee103a31060cc6c6f00e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 7 Dec 2025 23:26:40 +0900 Subject: [PATCH 129/129] 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); } }