From cab0b3451f5f3b6e6cf374dc25b1d7309eb07cd1 Mon Sep 17 00:00:00 2001 From: dnfd1 <117837311+dnfd1@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:26:45 -0700 Subject: [PATCH 01/35] Override OD setting to set extended limits for mania EZ and HR --- osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs index 0817f8f9fc..9514f72fe0 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs @@ -7,5 +7,14 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDifficultyAdjust : ModDifficultyAdjust { + public override DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable + { + Precision = 0.1f, + MinValue = 0, + MaxValue = 10, + ExtendedMaxValue = 13.61f, + ExtendedMinValue = -14.93f, + ReadCurrentFromDifficulty = diff => diff.OverallDifficulty, + }; } } From 348713d83d86fb22219910d3782c46e1a7956e6a Mon Sep 17 00:00:00 2001 From: dnfd1 <117837311+dnfd1@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:27:49 -0700 Subject: [PATCH 02/35] Allow OD to be overrided --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index da5f5df200..c6eaa75e9e 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mods }; [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))] - public DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable + public virtual DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable { Precision = 0.1f, MinValue = 0, From faad1753a4f9e051fffb4c41d8e4f5f54f7c12e2 Mon Sep 17 00:00:00 2001 From: dnfd1 <117837311+dnfd1@users.noreply.github.com> Date: Thu, 9 Oct 2025 05:13:17 -0700 Subject: [PATCH 03/35] Round OD limits to -15 and 15 --- osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs index 9514f72fe0..c1c25ad62e 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs @@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Mods Precision = 0.1f, MinValue = 0, MaxValue = 10, - ExtendedMaxValue = 13.61f, - ExtendedMinValue = -14.93f, + ExtendedMaxValue = 15, + ExtendedMinValue = -15, ReadCurrentFromDifficulty = diff => diff.OverallDifficulty, }; } From 0d68e5eeeb305b46e73410a97a6ccd7d6f4014d9 Mon Sep 17 00:00:00 2001 From: dnfd1 <117837311+dnfd1@users.noreply.github.com> Date: Thu, 9 Oct 2025 05:21:08 -0700 Subject: [PATCH 04/35] add inline comment to explain larger limits --- osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs index c1c25ad62e..ce70fdf73a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs @@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods Precision = 0.1f, MinValue = 0, MaxValue = 10, + // Use larger extended limits for mania to include OD values that occur with EZ or HR enabled ExtendedMaxValue = 15, ExtendedMinValue = -15, ReadCurrentFromDifficulty = diff => diff.OverallDifficulty, From b600860540ed9dd8ffa535bfe1eb14b281201cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 12:42:36 +0200 Subject: [PATCH 05/35] Implement request & response for fetching logged in user's favourite beatmap sets --- .../Requests/GetMyFavouriteBeatmapSetsRequest.cs | 12 ++++++++++++ .../Responses/GetMyFavouriteBeatmapSetsResponse.cs | 13 +++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 osu.Game/Online/API/Requests/GetMyFavouriteBeatmapSetsRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/GetMyFavouriteBeatmapSetsResponse.cs diff --git a/osu.Game/Online/API/Requests/GetMyFavouriteBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/GetMyFavouriteBeatmapSetsRequest.cs new file mode 100644 index 0000000000..87a901c98e --- /dev/null +++ b/osu.Game/Online/API/Requests/GetMyFavouriteBeatmapSetsRequest.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetMyFavouriteBeatmapSetsRequest : APIRequest + { + protected override string Target => @"me/beatmapset-favourites"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/GetMyFavouriteBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/Responses/GetMyFavouriteBeatmapSetsResponse.cs new file mode 100644 index 0000000000..f728b8ea0b --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/GetMyFavouriteBeatmapSetsResponse.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class GetMyFavouriteBeatmapSetsResponse + { + [JsonProperty("beatmapset_ids")] + public int[] BeatmapSetIds { get; set; } = []; + } +} From 0f1bf35bd9131aa30ecce9ef39ccf50c96baf9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 12:48:41 +0200 Subject: [PATCH 06/35] Add favourite beatmap set tracking to `LocalUserInfo` --- osu.Game/Online/API/DummyAPIAccess.cs | 6 ++++++ osu.Game/Online/API/ILocalUserState.cs | 2 ++ osu.Game/Online/API/LocalUserState.cs | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index dbf5964416..c01d0ca480 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -238,10 +238,12 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); public BindableList Blocks { get; } = new BindableList(); + public BindableList FavouriteBeatmapSets { get; } = new BindableList(); IBindable ILocalUserState.User => User; IBindableList ILocalUserState.Friends => Friends; IBindableList ILocalUserState.Blocks => Blocks; + IBindableList ILocalUserState.FavouriteBeatmapSets => FavouriteBeatmapSets; public void UpdateFriends() { @@ -250,6 +252,10 @@ namespace osu.Game.Online.API public void UpdateBlocks() { } + + public void UpdateFavouriteBeatmapSets() + { + } } } } diff --git a/osu.Game/Online/API/ILocalUserState.cs b/osu.Game/Online/API/ILocalUserState.cs index 3ccec1c9ae..4c5cbcf197 100644 --- a/osu.Game/Online/API/ILocalUserState.cs +++ b/osu.Game/Online/API/ILocalUserState.cs @@ -11,8 +11,10 @@ namespace osu.Game.Online.API IBindable User { get; } IBindableList Friends { get; } IBindableList Blocks { get; } + IBindableList FavouriteBeatmapSets { get; } void UpdateFriends(); void UpdateBlocks(); + void UpdateFavouriteBeatmapSets(); } } diff --git a/osu.Game/Online/API/LocalUserState.cs b/osu.Game/Online/API/LocalUserState.cs index 5da9289d89..1359d62ae7 100644 --- a/osu.Game/Online/API/LocalUserState.cs +++ b/osu.Game/Online/API/LocalUserState.cs @@ -16,12 +16,14 @@ namespace osu.Game.Online.API public IBindable User => localUser; public IBindableList Friends => friends; public IBindableList Blocks => blocks; + public IBindableList FavouriteBeatmapSets => favouriteBeatmapSets; private readonly IAPIProvider api; private readonly Bindable localUser = new Bindable(createGuestUser()); private readonly BindableList friends = new BindableList(); private readonly BindableList blocks = new BindableList(); + private readonly BindableList favouriteBeatmapSets = new BindableList(); private readonly Bindable configStatus = new Bindable(); private readonly Bindable configSupporter = new Bindable(); @@ -62,6 +64,7 @@ namespace osu.Game.Online.API UpdateFriends(); UpdateBlocks(); + UpdateFavouriteBeatmapSets(); } public void ClearLocalUser() @@ -76,6 +79,7 @@ namespace osu.Game.Online.API configSupporter.Value = false; friends.Clear(); blocks.Clear(); + favouriteBeatmapSets.Clear(); }); } @@ -125,5 +129,23 @@ namespace osu.Game.Online.API api.Queue(blocksReq); } + + public void UpdateFavouriteBeatmapSets() + { + if (!api.IsLoggedIn) + return; + + var favouritesReq = new GetMyFavouriteBeatmapSetsRequest(); + favouritesReq.Success += res => + { + var existingBeatmapSets = favouriteBeatmapSets.ToHashSet(); + var updatedBeatmapSets = res.BeatmapSetIds.ToHashSet(); + + favouriteBeatmapSets.AddRange(updatedBeatmapSets.Except(existingBeatmapSets)); + favouriteBeatmapSets.RemoveAll(b => !updatedBeatmapSets.Contains(b)); + }; + + api.Queue(favouritesReq); + } } } From 6b56a0611b8ffa9890782b3f80f9bb3217f9b095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 13:04:50 +0200 Subject: [PATCH 07/35] Refetch list of user favourites on every change to favourites Is this lazy? Sure it is. Friends and blocks do the same thing, though, and I'm not overthinking this any more than I already have. Being smarter here would likely mean being more invasive with respect to listening in on all outgoing API requests and silently updating favourites on that basis. Which is "smart" but also complicated. --- osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs | 1 + osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs | 1 + osu.Game/Screens/Ranking/FavouriteButton.cs | 1 + osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs | 2 ++ 4 files changed, 5 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs index 0b2aaf0bc3..f1ec1d1965 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs @@ -62,6 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1)); SetLoading(false); + api.LocalUserState.UpdateFavouriteBeatmapSets(); }; favouriteRequest.Failure += e => { diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index eab394c8f6..215e521d42 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -74,6 +74,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons { favourited.Toggle(); loading.Hide(); + api.LocalUserState.UpdateFavouriteBeatmapSets(); }; request.Failure += e => diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index 019b80dde9..7f1c4e82cc 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -109,6 +109,7 @@ namespace osu.Game.Screens.Ranking Enabled.Value = true; loading.Hide(); + api.LocalUserState.UpdateFavouriteBeatmapSets(); }; favouriteRequest.Failure += e => { diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs index 2db3ed7613..62ac8a07b4 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs @@ -233,6 +233,8 @@ namespace osu.Game.Screens.SelectV2 // if the beatmap set reference changed under the callback, abort visual updates to avoid showing stale data if (onlineBeatmapSet == null || ReferenceEquals(beatmapSet, onlineBeatmapSet)) setBeatmapSet(beatmapSet, withHeartAnimation: hasFavourited); + + api.LocalUserState.UpdateFavouriteBeatmapSets(); }; favouriteRequest.Failure += e => { From 29787360ba5f45575623701774e31894ce87858c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 13:08:24 +0200 Subject: [PATCH 08/35] Change `BeatmapCarouselFilterGrouping` constructor params to required init properties --- .../BeatmapCarouselFilterGroupingTest.cs | 10 ++++++---- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 7 ++++++- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 20 ++++++------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index dcd7a5a8fc..0668c60825 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -368,10 +368,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private static async Task> runGrouping(GroupMode group, List beatmapSets) { - var groupingFilter = new BeatmapCarouselFilterGrouping( - () => new FilterCriteria { Group = group }, - () => new List(), - _ => new Dictionary()); + var groupingFilter = new BeatmapCarouselFilterGrouping + { + GetCriteria = () => new FilterCriteria { Group = group }, + GetCollections = () => new List(), + GetLocalUserTopRanks = _ => new Dictionary() + }; return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 6b78967b93..761fba80a6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -105,7 +105,12 @@ namespace osu.Game.Screens.SelectV2 { new BeatmapCarouselFilterMatching(() => Criteria!), new BeatmapCarouselFilterSorting(() => Criteria!), - grouping = new BeatmapCarouselFilterGrouping(() => Criteria!, GetAllCollections, GetBeatmapInfoGuidToTopRankMapping) + grouping = new BeatmapCarouselFilterGrouping + { + GetCriteria = () => Criteria!, + GetCollections = GetAllCollections, + GetLocalUserTopRanks = GetBeatmapInfoGuidToTopRankMapping + } }; AddInternal(loading = new LoadingLayer()); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index fa01343cbe..14de07ba24 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -39,17 +39,9 @@ namespace osu.Game.Screens.SelectV2 private Dictionary> setMap = new Dictionary>(); private Dictionary> groupMap = new Dictionary>(); - private readonly Func getCriteria; - private readonly Func> getCollections; - private readonly Func> getLocalUserTopRanks; - - public BeatmapCarouselFilterGrouping(Func getCriteria, Func> getCollections, - Func> getLocalUserTopRanks) - { - this.getCriteria = getCriteria; - this.getCollections = getCollections; - this.getLocalUserTopRanks = getLocalUserTopRanks; - } + public required Func GetCriteria { get; init; } + public required Func> GetCollections { get; init; } + public required Func> GetLocalUserTopRanks { get; init; } public async Task> Run(IEnumerable items, CancellationToken cancellationToken) { @@ -59,7 +51,7 @@ namespace osu.Game.Screens.SelectV2 var newSetMap = new Dictionary>(setMap.Count); var newGroupMap = new Dictionary>(groupMap.Count); - var criteria = getCriteria(); + var criteria = GetCriteria(); var newItems = new List(); BeatmapSetsGroupedTogether = ShouldGroupBeatmapsTogether(criteria); @@ -215,7 +207,7 @@ namespace osu.Game.Screens.SelectV2 case GroupMode.Collections: { - var collections = getCollections(); + var collections = GetCollections(); return getGroupsBy(b => defineGroupByCollection(b, collections), items); } @@ -224,7 +216,7 @@ namespace osu.Game.Screens.SelectV2 case GroupMode.RankAchieved: { - var topRankMapping = getLocalUserTopRanks(criteria); + var topRankMapping = GetLocalUserTopRanks(criteria); return getGroupsBy(b => defineGroupByRankAchieved(b, topRankMapping), items); } From 14d0982b6c6665b6d4ec5061b4317751bb2433cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 13:30:49 +0200 Subject: [PATCH 09/35] Implement grouping by favourites - Closes https://github.com/ppy/osu/issues/34494. - Supersedes / closes https://github.com/ppy/osu/pull/34744. --- .../BeatmapCarouselFilterGroupingTest.cs | 30 +++++++++++++++++-- osu.Game/Screens/Select/Filter/GroupMode.cs | 4 +-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 16 ++++++++-- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 17 +++++++++-- osu.Game/Screens/SelectV2/FilterControl.cs | 3 ++ 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 0668c60825..e439a18ded 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -366,13 +366,39 @@ namespace osu.Game.Tests.Visual.SongSelectV2 #endregion - private static async Task> runGrouping(GroupMode group, List beatmapSets) + #region Favourites grouping + + [Test] + public async Task TestFavouritesGrouping() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(s => s.OnlineID = 1, beatmapSets, out _); + addBeatmapSet(s => s.OnlineID = 21, beatmapSets, out var firstFavourite); + addBeatmapSet(s => s.OnlineID = 321, beatmapSets, out _); + addBeatmapSet(s => s.OnlineID = 4321, beatmapSets, out _); + addBeatmapSet(s => s.OnlineID = 54321, beatmapSets, out var secondFavourite); + + favouriteBeatmapSets = [21, 54321]; + + var results = await runGrouping(GroupMode.Favourites, beatmapSets); + assertGroup(results, 0, "Favourites", firstFavourite.Beatmaps.Concat(secondFavourite.Beatmaps), ref total); + assertTotal(results, total); + } + + #endregion + + private HashSet favouriteBeatmapSets = []; + + private async Task> runGrouping(GroupMode group, List beatmapSets) { var groupingFilter = new BeatmapCarouselFilterGrouping { GetCriteria = () => new FilterCriteria { Group = group }, GetCollections = () => new List(), - GetLocalUserTopRanks = _ => new Dictionary() + GetLocalUserTopRanks = _ => new Dictionary(), + GetFavouriteBeatmapSets = () => favouriteBeatmapSets, }; return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index 06d3a71b0f..e2bc1faae2 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.cs @@ -32,8 +32,8 @@ namespace osu.Game.Screens.Select.Filter [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Difficulty))] Difficulty, - // [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Favourites))] - // Favourites, + [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Favourites))] + Favourites, [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LastPlayed))] LastPlayed, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 761fba80a6..e55f64f847 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -28,6 +28,7 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Select; @@ -109,7 +110,8 @@ namespace osu.Game.Screens.SelectV2 { GetCriteria = () => Criteria!, GetCollections = GetAllCollections, - GetLocalUserTopRanks = GetBeatmapInfoGuidToTopRankMapping + GetLocalUserTopRanks = GetBeatmapInfoGuidToTopRankMapping, + GetFavouriteBeatmapSets = GetFavouriteBeatmapSets, } }; @@ -809,11 +811,14 @@ namespace osu.Game.Screens.SelectV2 #endregion - #region Database fetches for grouping support + #region Fetches for grouping support [Resolved] private RealmAccess realm { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + protected virtual List GetAllCollections() => realm.Run(r => r.All().AsEnumerable().Detach()); protected virtual Dictionary GetBeatmapInfoGuidToTopRankMapping(FilterCriteria criteria) => realm.Run(r => @@ -838,6 +843,13 @@ namespace osu.Game.Screens.SelectV2 return topRankMapping; }); + /// + /// Note that calling .ToHashSet() below has two purposes: + /// one being performance of contain checks in filtering code, + /// another being slightly better thread safety (as could be mutated during async filtering). + /// + protected HashSet GetFavouriteBeatmapSets() => api.LocalUserState.FavouriteBeatmapSets.ToHashSet(); + #endregion #region Drawable pooling diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 14de07ba24..159d8f137e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -42,6 +42,7 @@ namespace osu.Game.Screens.SelectV2 public required Func GetCriteria { get; init; } public required Func> GetCollections { get; init; } public required Func> GetLocalUserTopRanks { get; init; } + public required Func> GetFavouriteBeatmapSets { get; init; } public async Task> Run(IEnumerable items, CancellationToken cancellationToken) { @@ -220,9 +221,11 @@ namespace osu.Game.Screens.SelectV2 return getGroupsBy(b => defineGroupByRankAchieved(b, topRankMapping), items); } - // TODO: need implementation - // case GroupMode.Favourites: - // goto case GroupMode.None; + case GroupMode.Favourites: + { + var favouriteBeatmapSets = GetFavouriteBeatmapSets(); + return getGroupsBy(b => defineGroupByFavourites(b, favouriteBeatmapSets), items); + } default: throw new ArgumentOutOfRangeException(); @@ -429,6 +432,14 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(int.MaxValue, "Unplayed").Yield(); } + private IEnumerable defineGroupByFavourites(BeatmapInfo beatmap, HashSet favouriteBeatmapSets) + { + if (beatmap.BeatmapSet?.OnlineID > 0 && favouriteBeatmapSets.Contains(beatmap.BeatmapSet.OnlineID)) + return new GroupDefinition(0, "Favourites").Yield(); + + return []; + } + private record GroupMapping(GroupDefinition? Group, List ItemsInGroup); } } diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index c845a9e146..a90ac3a4e8 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -57,6 +57,7 @@ namespace osu.Game.Screens.SelectV2 private RealmAccess realm { get; set; } = null!; private IBindable localUser = null!; + private readonly IBindableList localUserFavouriteBeatmapSets = new BindableList(); public LocalisableString StatusText { @@ -186,6 +187,7 @@ namespace osu.Game.Screens.SelectV2 }; localUser = api.LocalUser.GetBoundCopy(); + localUserFavouriteBeatmapSets.BindTo(api.LocalUserState.FavouriteBeatmapSets); } protected override void LoadComplete() @@ -237,6 +239,7 @@ namespace osu.Game.Screens.SelectV2 }); localUser.BindValueChanged(_ => updateCriteria()); + localUserFavouriteBeatmapSets.BindCollectionChanged((_, _) => updateCriteria()); updateCriteria(); } From e240817087c4886de830322fc71d08f2fb2ddb2f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Oct 2025 18:03:28 +0900 Subject: [PATCH 10/35] Move quick play chat entirely to screen footer --- .../TestSceneMatchmakingChatDisplay.cs | 49 +++++++++++++++++++ .../Match/MatchmakingChatDisplay.cs | 20 ++++++++ .../Matchmaking/Match/ScreenMatchmaking.cs | 42 +++++++++------- 3 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs new file mode 100644 index 0000000000..d8e42cd946 --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingChatDisplay.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestSceneMatchmakingChatDisplay : ScreenTestScene + { + private MatchmakingChatDisplay? chat; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("add chat", () => + { + chat?.Expire(); + + ScreenFooter.Add(chat = new MatchmakingChatDisplay(new Room()) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(700, 130), + Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Alpha = 0 + }); + }); + + AddStep("show footer", () => ScreenFooter.Show()); + } + + [Test] + public void TestAppearDisappear() + { + AddStep("appear", () => chat!.Appear()); + AddWaitStep("wait for animation", 3); + + AddStep("disappear", () => chat!.Disappear()); + AddWaitStep("wait for animation", 3); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs index 4ff6a3cdf6..6a01642907 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input; @@ -66,5 +68,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public void OnReleased(KeyBindingReleaseEvent e) { } + + public void Appear() + { + FinishTransforms(); + + this.MoveToY(150f) + .FadeOut() + .MoveToY(0f, 240, Easing.OutCubic) + .FadeIn(240, Easing.OutCubic); + } + + public TransformSequence Disappear() + { + FinishTransforms(); + + return this.FadeOut(240, Easing.InOutCubic) + .MoveToY(150f, 240, Easing.InOutCubic); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 9292287c3c..56667822d2 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -29,10 +29,10 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Users; +using osuTK; using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match @@ -87,19 +87,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private MusicController music { get; set; } = null!; private readonly MultiplayerRoom room; + private readonly MatchmakingChatDisplay chat; private Sample? sampleStart; private CancellationTokenSource? downloadCheckCancellation; private int? lastDownloadCheckedBeatmapId; - private MatchChatDisplay chat = null!; - public ScreenMatchmaking(MultiplayerRoom room) { this.room = room; Activity.Value = new UserActivity.InLobby(room); Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; + + chat = new MatchmakingChatDisplay(new Room(room)) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(700, 130), + Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING }, + Alpha = 0 + }; } [BackgroundDependencyLoader] @@ -156,13 +164,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Width = 700, - Height = 130, - Padding = new MarginPadding { Bottom = row_padding }, - Child = chat = new MatchmakingChatDisplay(new Room(room)) - { - RelativeSizeAxes = Axes.Both, - } + Size = new Vector2(700, 130), + Margin = new MarginPadding { Bottom = row_padding } } ] } @@ -183,7 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); - Footer!.Add(chat.CreateProxy()); + Footer?.Add(chat); } private void onRoomUpdated() @@ -326,12 +329,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); + + chat.Appear(); beginHandlingTrack(); } public override void OnSuspending(ScreenTransitionEvent e) { - onLeaving(); + chat.Disappear(); + endHandlingTrack(); + base.OnSuspending(e); } @@ -347,7 +354,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match return true; } - onLeaving(); + chat.Disappear().Expire(); + endHandlingTrack(); + client.LeaveRoom().FireAndForget(); return false; } @@ -370,6 +379,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + + chat.Appear(); beginHandlingTrack(); if (e.Last is not MultiplayerPlayerLoader playerLoader) @@ -384,11 +395,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match client.ChangeState(MultiplayerUserState.Idle); } - private void onLeaving() - { - endHandlingTrack(); - } - /// /// Handles changes in the track to keep it looping while active. /// From a3c78de71077543213050b1dedee06312bc8dec4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Oct 2025 21:00:38 +0900 Subject: [PATCH 11/35] Move context menu from channel to chat overlay --- osu.Game/Overlays/Chat/DrawableChannel.cs | 28 +++++++++-------------- osu.Game/Overlays/ChatOverlay.cs | 7 +++++- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 2f0461eb40..ad327f4b28 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -12,7 +12,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Cursor; using osu.Game.Online.Chat; using osuTK.Graphics; @@ -49,25 +48,20 @@ namespace osu.Game.Overlays.Chat [BackgroundDependencyLoader] private void load() { - Child = new OsuContextMenuContainer + Child = scroll = new ChannelScrollContainer { + ScrollbarVisible = scrollbarVisible, RelativeSizeAxes = Axes.Both, - Masking = true, - Child = scroll = new ChannelScrollContainer + // Some chat lines have effects that slightly protrude to the bottom, + // which we do not want to mask away, hence the padding. + Padding = new MarginPadding { Bottom = 5 }, + Child = ChatLineFlow = new FillFlowContainer { - ScrollbarVisible = scrollbarVisible, - RelativeSizeAxes = Axes.Both, - // Some chat lines have effects that slightly protrude to the bottom, - // which we do not want to mask away, hence the padding. - Padding = new MarginPadding { Bottom = 5 }, - Child = ChatLineFlow = new FillFlowContainer - { - Padding = new MarginPadding { Left = 3, Right = 10 }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - } - }, + Padding = new MarginPadding { Left = 3, Right = 10 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + } }; newMessagesArrived(Channel.Messages); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 7f4ba3e2e2..e7422d6f86 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -19,6 +19,7 @@ using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online; @@ -142,9 +143,13 @@ namespace osu.Game.Overlays new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = currentChannelContainer = new Container + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, + Child = currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + } } }, loading = new LoadingLayer(true), From 613c20836242e8ea5711218db8b4754fa1cbaf0d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Oct 2025 21:26:33 +0900 Subject: [PATCH 12/35] Fix partially offscreen quick play chat context menu --- .../Matchmaking/Match/ScreenMatchmaking.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 56667822d2..95e3cb0236 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -186,7 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); - Footer?.Add(chat); + Footer?.Add(new ChatContainer(chat)); } private void onRoomUpdated() @@ -445,5 +445,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match client.LoadRequested -= onLoadRequested; } } + + // Contains the chat display and a context menu container for it. Shared lifetime with the chat display (expires along with it). + private partial class ChatContainer : CompositeDrawable + { + public override double LifetimeStart => chat.LifetimeStart; + public override double LifetimeEnd => chat.LifetimeEnd; + + private readonly MatchmakingChatDisplay chat; + + public ChatContainer(MatchmakingChatDisplay chat) + { + this.chat = chat; + + Anchor = Anchor.BottomRight; + Origin = Anchor.BottomRight; + + // This component is added to the screen footer which is only about 50px high. + // Therefore, it's given a large absolute size to give the context menu enough space to display correctly. + Size = new Vector2(700); + + InternalChild = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = chat + }; + } + } } } From f96be84c5749d83ac2d1aa7e0b8453ce513b1e7d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Oct 2025 22:23:05 +0900 Subject: [PATCH 13/35] Fix tests --- osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index d7f79d3e30..877dc7eaac 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -471,7 +471,7 @@ namespace osu.Game.Tests.Visual.Online public DrawableChannel DrawableChannel => InternalChildren.OfType().First(); - public ChannelScrollContainer ScrollContainer => (ChannelScrollContainer)((Container)DrawableChannel.Child).Child; + public ChannelScrollContainer ScrollContainer => DrawableChannel.ChildrenOfType().Single(); public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child; From 1af462b692e96aa3c2811fd3be2b1307bc8dc158 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Oct 2025 23:07:04 +0900 Subject: [PATCH 14/35] Add very simple countdown timer for quick play stages (#35433) --- .../Match/StageDisplay.TimerText.cs | 106 ++++++++++++++++++ .../Matchmaking/Match/StageDisplay.cs | 6 + .../Multiplayer/TestMultiplayerClient.cs | 2 +- 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.TimerText.cs diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.TimerText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.TimerText.cs new file mode 100644 index 0000000000..e2af3ef945 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.TimerText.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Matchmaking; +using osu.Game.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class StageDisplay + { + public partial class TimerText : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private OsuSpriteText text = null!; + + private DateTimeOffset countdownEndTime; + + public TimerText() + { + AutoSizeAxes = Axes.X; + Height = 18; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = text = new OsuSpriteText + { + Height = 18, + Spacing = new Vector2(-1, 0), + Font = OsuFont.Style.Heading2.With(fixedWidth: true), + AlwaysPresent = true, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.CountdownStarted += onCountdownStarted; + client.CountdownStopped += onCountdownStopped; + + if (client.Room != null) + { + foreach (var countdown in client.Room.ActiveCountdowns) + onCountdownStarted(countdown); + } + } + + protected override void Update() + { + base.Update(); + + TimeSpan remaining = countdownEndTime - DateTimeOffset.Now; + + text.Alpha = remaining.TotalSeconds > 0 ? 1f : 0.2f; + + if (remaining.TotalSeconds > 10) + text.Font = text.Font.With(weight: FontWeight.SemiBold); + else + text.Font = text.Font.With(weight: FontWeight.Bold); + + int minutes = (int)Math.Max(0, remaining.TotalMinutes); + int seconds = Math.Max(0, remaining.Seconds); + int ms = Math.Max(0, remaining.Milliseconds); + + text.Text = $"{minutes:00}:{seconds:00}.{ms:000}"; + } + + private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (countdown is MatchmakingStageCountdown) + countdownEndTime = DateTimeOffset.Now + countdown.TimeRemaining; + }); + + private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (countdown is not MatchmakingStageCountdown) + return; + + countdownEndTime = DateTimeOffset.Now; + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.CountdownStarted -= onCountdownStarted; + client.CountdownStopped -= onCountdownStopped; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs index e428e3b044..b45e8054a0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs @@ -72,6 +72,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Direction = FillDirection.Horizontal, }, }, + new TimerText + { + Y = -38, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, new StatusText { Y = 32, diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index bd16c36eec..5b2876a989 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -851,7 +851,7 @@ namespace osu.Game.Tests.Visual.Multiplayer await StartCountdown(new MatchmakingStageCountdown { Stage = stage, - TimeRemaining = TimeSpan.FromSeconds(10) + TimeRemaining = TimeSpan.FromSeconds(stage == MatchmakingStage.UserBeatmapSelect ? 30 : 10) }).ConfigureAwait(false); } From 72fa1553c317abfe27d76126d86bb9a05323d069 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Oct 2025 13:46:23 +0900 Subject: [PATCH 15/35] Add settings toggle for experimental BASS initialisation mode --- .../Sections/Audio/AudioDevicesSettings.cs | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index a71f2a6d29..4a9130db89 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -1,13 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using System.Collections.Generic; using System.Linq; +using osu.Framework; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -19,9 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio protected override LocalisableString Header => AudioSettingsStrings.AudioDevicesHeader; [Resolved] - private AudioManager audio { get; set; } + private AudioManager audio { get; set; } = null!; - private SettingsDropdown dropdown; + private SettingsDropdown dropdown = null!; + + private SettingsCheckbox? wasapiExperimental; [BackgroundDependencyLoader] private void load() @@ -32,9 +34,22 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { LabelText = AudioSettingsStrings.OutputDevice, Keywords = new[] { "speaker", "headphone", "output" } - } + }, }; + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + { + Add(wasapiExperimental = new SettingsCheckbox + { + LabelText = "Use experimental audio mode", + TooltipText = "This will attempt to initialise the WASAPI engine in a lower latency mode.", + Current = audio.UseExperimentalWasapi, + Keywords = new[] { "wasapi", "latency", "exclusive" } + }); + + wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty); + } + updateItems(); audio.OnNewDevice += onDeviceChanged; @@ -42,7 +57,21 @@ namespace osu.Game.Overlays.Settings.Sections.Audio dropdown.Current = audio.AudioDevice; } - private void onDeviceChanged(string name) => updateItems(); + private void onDeviceChanged(string _) + { + updateItems(); + + if (wasapiExperimental != null) + { + if (wasapiExperimental.Current.Value) + { + wasapiExperimental.SetNoticeText( + "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.", true); + } + else + wasapiExperimental.ClearNoticeText(); + } + } private void updateItems() { @@ -61,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio // functionality would require involved OS-specific code. dropdown.Items = deviceItems // Dropdown doesn't like null items. Somehow we are seeing some arrive here (see https://github.com/ppy/osu/issues/21271) - .Where(i => i != null) + .Where(i => i.IsNotNull()) .Distinct() .ToList(); } @@ -70,7 +99,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { base.Dispose(isDisposing); - if (audio != null) + if (audio.IsNotNull()) { audio.OnNewDevice -= onDeviceChanged; audio.OnLostDevice -= onDeviceChanged; From 79a76ce58734bd6a27be096c8dae19091566dbec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Oct 2025 17:35:48 +0900 Subject: [PATCH 16/35] Update AudioDevicesSettings.cs Co-authored-by: Dan Balasescu --- .../Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index 4a9130db89..b1c735e745 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio Add(wasapiExperimental = new SettingsCheckbox { LabelText = "Use experimental audio mode", - TooltipText = "This will attempt to initialise the WASAPI engine in a lower latency mode.", + TooltipText = "This will attempt to initialise the audio engine in a lower latency mode.", Current = audio.UseExperimentalWasapi, Keywords = new[] { "wasapi", "latency", "exclusive" } }); From 9ca47fc53a2d4c45f304e6de0d2118a44ce2bbb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Oct 2025 19:41:28 +0900 Subject: [PATCH 17/35] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f2853eaaa8..d05589ea8a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index be7df2f771..28faf49455 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 473fb5720ca3d49aeed40e307ab032ff4deb1b30 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 12:11:11 +0900 Subject: [PATCH 18/35] Disable Discord invites to quick play rooms --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 668f63b910..bbdb719b05 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -189,7 +189,7 @@ namespace osu.Desktop } // user party - if (!hideIdentifiableInformation && multiplayerClient.Room != null) + if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking) { MultiplayerRoom room = multiplayerClient.Room; From 765b9a20b5a738c31c697e3a07ffac2529bfcf5c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 12:13:06 +0900 Subject: [PATCH 19/35] Hide quick play room name in Discord rich presence --- osu.Game/Users/UserActivity.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index b7b6c6f366..86c84c0bb2 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -274,8 +274,16 @@ namespace osu.Game.Users public InLobby(MultiplayerRoom room) { - RoomID = room.RoomID; - RoomName = room.Settings.Name; + if (room.Settings.MatchType == MatchType.Matchmaking) + { + RoomID = -1; + RoomName = "Quick Play"; + } + else + { + RoomID = room.RoomID; + RoomName = room.Settings.Name; + } } [SerializationConstructor] From 08621c4cc900e0e2fcb370eb1efb5ea3c2ef40aa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 18:30:42 +0900 Subject: [PATCH 20/35] Refactor panel structure --- .../Matchmaking/Match/PlayerPanel.cs | 195 +++++++++--------- 1 file changed, 96 insertions(+), 99 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 1480e866a6..5f36e64dd9 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -48,6 +48,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public readonly MultiplayerRoomUser RoomUser; + /// + /// Perform an action in addition to showing the user's profile. + /// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX). + /// + public new Action? Action; + [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -81,6 +87,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match [Resolved] private MetadataClient? metadataClient { get; set; } + public readonly APIUser User; + private readonly Action viewProfile; + private OsuSpriteText rankText = null!; private OsuSpriteText scoreText = null!; @@ -91,33 +100,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private Container mainContent = null!; + private Box solidBackgroundLayer = null!; + private Drawable background = null!; + private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; - public PlayerPanelDisplayMode DisplayMode - { - get => displayMode; - set - { - displayMode = value; - if (IsLoaded) - updateLayout(false); - } - } - - public readonly APIUser User; - - /// - /// Perform an action in addition to showing the user's profile. - /// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX). - /// - public new Action? Action; - - protected Action ViewProfile { get; private set; } = null!; - - public Box SolidBackgroundLayer { get; private set; } = null!; - - protected Drawable? Background { get; private set; } - public PlayerPanel(MultiplayerRoomUser user) : base(HoverSampleSet.Button) { @@ -125,100 +112,99 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match User = user.User; RoomUser = user; + + base.Action = viewProfile = () => + { + Action?.Invoke(); + profileOverlay?.ShowUser(User); + }; } [BackgroundDependencyLoader] private void load() { - Add(SolidBackgroundLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider?.Background5 ?? colours.Gray1 - }); - - Background = new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colours.Gray7, - User = User - }; - if (Background != null) - Add(Background); - - base.Action = ViewProfile = () => - { - Action?.Invoke(); - profileOverlay?.ShowUser(User); - }; - Content.Masking = true; Content.CornerRadius = 10; Content.CornerExponent = 10; Content.Anchor = Anchor.Centre; Content.Origin = Anchor.Centre; - Add(new Container + Children = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Child = mainContent = new Container + solidBackgroundLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider?.Background5 ?? colours.Gray1 + }, + background = new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.Gray7, + User = User + }, + new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Children = new[] + Child = mainContent = new Container { - avatarPositionTarget = new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] { - Origin = Anchor.Centre, - Size = avatar_size, - Child = avatarJumpTarget = new Container + avatarPositionTarget = new Container { + Origin = Anchor.Centre, + Size = avatar_size, + Child = avatarJumpTarget = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Child = avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = Vector2.One + } + } + }, + rankText = new OsuSpriteText + { + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomCentre, + Blending = BlendingParameters.Additive, + Margin = new MarginPadding(4), + Text = "-", + Font = OsuFont.Style.Title.With(size: 55), + }, + username = new OsuSpriteText + { + Alpha = 0, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.Both, - Child = avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = Vector2.One - } + Text = User.Username, + Font = OsuFont.Style.Heading1, + }, + scoreText = new OsuSpriteText + { + Alpha = 0, + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Style.Heading2, + Text = "0 pts" } - }, - rankText = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomCentre, - Blending = BlendingParameters.Additive, - Margin = new MarginPadding(4), - Text = "-", - Font = OsuFont.Style.Title.With(size: 55), - }, - username = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = User.Username, - Font = OsuFont.Style.Heading1, - }, - scoreText = new OsuSpriteText - { - Alpha = 0, - Margin = new MarginPadding(10), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Font = OsuFont.Style.Heading2, - Text = "0 pts" } } } - }); + }; // Allow avatar to exist outside of masking for when it jumps around and stuff. AddInternal(avatar.CreateProxy()); @@ -240,6 +226,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match .FadeIn(200); } + public PlayerPanelDisplayMode DisplayMode + { + get => displayMode; + set + { + displayMode = value; + if (IsLoaded) + updateLayout(false); + } + } + private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal; private Vector2 avatarPosition @@ -276,16 +273,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match scoreText.Hide(); username.Hide(); - Background.FadeOut(200, Easing.OutQuint); - SolidBackgroundLayer.FadeOut(200, Easing.OutQuint); + background.FadeOut(200, Easing.OutQuint); + solidBackgroundLayer.FadeOut(200, Easing.OutQuint); this.ResizeTo(avatar_size, duration, Easing.OutPow10); break; case PlayerPanelDisplayMode.Horizontal: case PlayerPanelDisplayMode.Vertical: - Background.FadeIn(200); - SolidBackgroundLayer.FadeIn(200); + background.FadeIn(200); + solidBackgroundLayer.FadeIn(200); using (BeginDelayedSequence(100)) { @@ -420,7 +417,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { List items = new List { - new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, ViewProfile) + new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, viewProfile) }; if (User.Equals(api.LocalUser.Value)) From b7c07ad0e5a6c1482d353be70b79e4d52519cfa7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 19:04:16 +0900 Subject: [PATCH 21/35] Add support for marking panels as quit --- .../Matchmaking/TestScenePlayerPanel.cs | 6 + .../Matchmaking/Match/PlayerPanel.cs | 172 ++++++++++++------ 2 files changed, 118 insertions(+), 60 deletions(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index f64c7c9443..21567daabe 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -102,5 +102,11 @@ namespace osu.Game.Tests.Visual.Matchmaking { AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely()); } + + [Test] + public void TestQuit() + { + AddToggleStep("toggle quit", quit => panel.HasQuit = quit); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 5f36e64dd9..6884312f3d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private Drawable avatarPositionTarget = null!; private Drawable avatarJumpTarget = null!; - private MatchmakingAvatar avatar = null!; + private Drawable avatar = null!; private OsuSpriteText username = null!; private Container mainContent = null!; @@ -103,7 +103,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private Box solidBackgroundLayer = null!; private Drawable background = null!; + private OsuSpriteText quitText = null!; + private BufferedContainer backgroundQuitTarget = null!; + private BufferedContainer avatarQuitTarget = null!; + private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; + private bool hasQuit; public PlayerPanel(MultiplayerRoomUser user) : base(HoverSampleSet.Button) @@ -129,77 +134,99 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Content.Anchor = Anchor.Centre; Content.Origin = Anchor.Centre; - Children = new[] + Child = backgroundQuitTarget = new BufferedContainer { - solidBackgroundLayer = new Box + RelativeSizeAxes = Axes.Both, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider?.Background5 ?? colours.Gray1 - }, - background = new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colours.Gray7, - User = User - }, - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Child = mainContent = new Container + solidBackgroundLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider?.Background5 ?? colours.Gray1 + }, + background = new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.Gray7, + User = User + }, + new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Children = new[] + Child = mainContent = new Container { - avatarPositionTarget = new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] { - Origin = Anchor.Centre, - Size = avatar_size, - Child = avatarJumpTarget = new Container + quitText = new OsuSpriteText { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "QUIT", + Font = OsuFont.Default.With(weight: "Bold", size: 70), + Rotation = -22.5f, + Colour = OsuColour.Gray(0.3f), + Blending = BlendingParameters.Additive + }, + avatarPositionTarget = new Container + { + Origin = Anchor.Centre, + Size = avatar_size, + Child = avatarJumpTarget = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Child = avatar = new Container + { + RelativeSizeAxes = Axes.Both, + Child = avatarQuitTarget = new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Child = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = Vector2.One + } + } + }, + } + }, + rankText = new OsuSpriteText + { + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomCentre, + Blending = BlendingParameters.Additive, + Margin = new MarginPadding(4), + Text = "-", + Font = OsuFont.Style.Title.With(size: 55), + }, + username = new OsuSpriteText + { + Alpha = 0, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.Both, - Child = avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = Vector2.One - } + Text = User.Username, + Font = OsuFont.Style.Heading1, + }, + scoreText = new OsuSpriteText + { + Alpha = 0, + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Style.Heading2, + Text = "0 pts" } - }, - rankText = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomCentre, - Blending = BlendingParameters.Additive, - Margin = new MarginPadding(4), - Text = "-", - Font = OsuFont.Style.Title.With(size: 55), - }, - username = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = User.Username, - Font = OsuFont.Style.Heading1, - }, - scoreText = new OsuSpriteText - { - Alpha = 0, - Margin = new MarginPadding(10), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Font = OsuFont.Style.Heading2, - Text = "0 pts" } } } @@ -237,6 +264,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match } } + public bool HasQuit + { + get => hasQuit; + set + { + hasQuit = value; + if (IsLoaded) + updateLayout(false); + } + } + private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal; private Vector2 avatarPosition @@ -304,11 +342,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match rankText.MoveTo(horizontal ? new Vector2(-40, -20) : new Vector2(-70, 0), duration, Easing.OutPow10); username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10); scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10); + quitText.MoveTo(horizontal ? new Vector2(40, 0) : new Vector2(0, 40), duration, Easing.OutPow10); break; default: throw new ArgumentOutOfRangeException(); } + + if (HasQuit) + { + backgroundQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10); + avatarQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10); + quitText.FadeIn(duration, Easing.OutPow10); + } + else + { + backgroundQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10); + avatarQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10); + quitText.FadeOut(duration, Easing.OutPow10); + } } protected override bool OnHover(HoverEvent e) From bb578d254dc7f74900bca069b4d8cb8eb17e191c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 19:06:55 +0900 Subject: [PATCH 22/35] Mark panels as quit instead of removing --- .../Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index 510698f46e..9fb5d258a8 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => { - panels.Single(p => p.RoomUser.Equals(user)).Expire(); + panels.Single(p => p.RoomUser.Equals(user)).HasQuit = true; updateDisplay(); }); From 98eb29c43d75c52efbf6492ecf6ded84e8c59e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Oct 2025 10:35:48 +0100 Subject: [PATCH 23/35] Add failing test --- .../TestSceneSongSelectFiltering.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 076d84479a..eeeb6f7297 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -88,6 +88,33 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("selection unchanged", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.Last())); } + [Test] + public void TestFilterSingleResult_ReselectedAfterRulesetSwitches() + { + LoadSongSelect(); + + ImportBeatmapForRuleset(0); + ImportBeatmapForRuleset(0); + + AddStep("disable converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + AddStep("set filter text", () => filterTextBox.Current.Value = $"\"{Beatmaps.GetAllUsableBeatmapSets().Last().Metadata.Title}\""); + + AddWaitStep("wait for debounce", 5); + AddUntilStep("wait for filter", () => !Carousel.IsFiltering); + AddUntilStep("selection is second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.First())); + + AddStep("select last difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapSetInfo.Beatmaps.Last())); + AddUntilStep("selection is last difficulty of second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.Last())); + + ChangeRuleset(1); + AddUntilStep("wait for filter", () => !Carousel.IsFiltering); + AddUntilStep("selection is default", () => Beatmap.IsDefault); + + ChangeRuleset(0); + AddUntilStep("wait for filter", () => !Carousel.IsFiltering); + AddUntilStep("selection is last difficulty of second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.Last())); + } + [Test] public void TestFilterOnResumeAfterChange() { From e61ae7ab8a0e68dafb83d43575770ea8c3bc4206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Oct 2025 11:06:38 +0100 Subject: [PATCH 24/35] Fix single filtered selection not being reselected after being filtered away Closes https://github.com/ppy/osu/issues/35003. Bit dodgy to use `CurrentSelectionItem` for this. Ideally I would use the global `Beatmap.IsDefault`, but I kind of don't want to violate the rule that `BeatmapCarousel` shouldn't have direct access to the global beatmap. And this seems to work, so... maybe fine to use until it doesn't? --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d6bd9c1db1..5e84ba0722 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -561,8 +561,19 @@ namespace osu.Game.Screens.SelectV2 var beatmaps = items.Select(i => i.Model).OfType(); - if (beatmaps.Any(b => b.Equals(CurrentSelection as GroupedBeatmap))) + // do not request recommended selection if the user already had selected a difficulty within the single filtered beatmap set, + // as it could change the difficulty that will be selected + var preexistingSelection = beatmaps.FirstOrDefault(b => b.Equals(CurrentSelection as GroupedBeatmap)); + + if (preexistingSelection != null) + { + // the selection might not have an item associated with it, if it was fully filtered away previously + // in this case, request to reselect it + if (CurrentSelectionItem == null) + RequestSelection(preexistingSelection); + return; + } RequestRecommendedSelection(beatmaps); } From f8769d2e443d28228aae377c6152b7fef1375a7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Oct 2025 22:59:02 +0900 Subject: [PATCH 25/35] Fix WASAPI settings notice text not displaying on startup --- .../Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index b1c735e745..5b5617bae0 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -50,11 +50,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty); } - updateItems(); - audio.OnNewDevice += onDeviceChanged; audio.OnLostDevice += onDeviceChanged; dropdown.Current = audio.AudioDevice; + + onDeviceChanged(string.Empty); } private void onDeviceChanged(string _) From 3c37ac17184370c695f0fd79a7641232147e5cf9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Oct 2025 23:19:28 +0900 Subject: [PATCH 26/35] Fix clipped outline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Matchmaking/Match/MatchmakingAvatar.cs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs index 53db2114c7..e0f46d89f0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { RelativeSizeAxes = Axes.Both, Depth = float.MaxValue, - Padding = new MarginPadding(-2), Child = new FastCircle { RelativeSizeAxes = Axes.Both, @@ -50,20 +49,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match }); } - AddInternal(new CircularContainer + AddInternal(new Container { + Padding = new MarginPadding(2), RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] + Child = new CircularContainer { - new Box + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4.LightSlateGray, - }, - new ClickableAvatar(user, true) - { - RelativeSizeAxes = Axes.Both, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.LightSlateGray, + }, + new ClickableAvatar(user, true) + { + RelativeSizeAxes = Axes.Both, + } } } }); From ce3b8bc77b55bb5164a5668b4f7c084745e55131 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 15:07:37 +0900 Subject: [PATCH 27/35] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d05589ea8a..8917bc9339 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 28faf49455..7e219e4b1d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 8b2b6517ca8417a3c8cbb723ea6899cedffb819f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 15:41:40 +0900 Subject: [PATCH 28/35] Fix regression of avatar animation --- osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 6884312f3d..fa4c8a11b9 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -185,6 +185,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match RelativeSizeAxes = Axes.Both, Child = avatar = new Container { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Child = avatarQuitTarget = new BufferedContainer { From 0205cf0fb99f550d5926702c2142cb9f91b96790 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 15:43:25 +0900 Subject: [PATCH 29/35] Render frame buffers at a higher resolution to fix blurry for now --- osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index fa4c8a11b9..0d5f36585c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -136,6 +136,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Child = backgroundQuitTarget = new BufferedContainer { + FrameBufferScale = new Vector2(1.5f), RelativeSizeAxes = Axes.Both, Children = new[] { @@ -188,8 +189,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, + // Needs to be re-buffered as the avatar is proxied outside of the parent buffered container. Child = avatarQuitTarget = new BufferedContainer { + FrameBufferScale = new Vector2(1.5f), RelativeSizeAxes = Axes.Both, Child = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) { From 22f11b6fa536ee2d74855c1bfb01bee4f4fc6f43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 16:30:31 +0900 Subject: [PATCH 30/35] Update test in line with new quit panel behaviour --- .../Visual/Matchmaking/TestScenePlayerPanelOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs index d5ab571a7d..c2b2b95d55 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs @@ -118,9 +118,12 @@ namespace osu.Game.Tests.Visual.Matchmaking }); AddUntilStep("two panels displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddAssert("no panels quit", () => this.ChildrenOfType().Count(p => p.HasQuit), () => Is.EqualTo(0)); AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); - AddUntilStep("one panel displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + + AddUntilStep("one panel quit", () => this.ChildrenOfType().Count(p => p.HasQuit), () => Is.EqualTo(1)); + AddAssert("two panels still displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] From 960170808715ea93d7a48496d59876fb13949bf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Oct 2025 18:28:58 +0900 Subject: [PATCH 31/35] Fix quit text on avatar only mode, fix avatar fade --- .../OnlinePlay/Matchmaking/Match/PlayerPanel.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 0d5f36585c..e86a546533 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -354,20 +354,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match throw new ArgumentOutOfRangeException(); } + // quit text doesn't fit on avataronly mode. + if (HasQuit && displayMode != PlayerPanelDisplayMode.AvatarOnly) + quitText.FadeIn(duration, Easing.OutPow10); + else + quitText.FadeOut(duration, Easing.OutPow10); + if (HasQuit) { backgroundQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10); avatarQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10); - quitText.FadeIn(duration, Easing.OutPow10); } else { backgroundQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10); avatarQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10); - quitText.FadeOut(duration, Easing.OutPow10); } } + protected override void Update() + { + base.Update(); + + // Not sure why this is required but it is. + avatarQuitTarget.Alpha = Alpha; + } + protected override bool OnHover(HoverEvent e) { Content.ScaleTo(1.03f, 2000, Easing.OutPow10); From cbe7da99adc9578ab1fe0161d93fdf9a28d8b0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Oct 2025 04:14:37 +0100 Subject: [PATCH 32/35] Fix screen footer overlay content being pushed to right during fade-out (#35481) * Apply some renames & drawable names for visualiser Optional but really helps me make heads of tails as to what anything is here. Like really, multiple variations of `footerContent` inside a `ScreenFooter` class, with zero elaboration that it's really content to do with *overlays*... * Fix screen footer overlay content being pushed to right during fade-out - Closes https://github.com/ppy/osu/issues/35203 - Supersedes / closes https://github.com/ppy/osu/pull/35468 --- osu.Game/Screens/Footer/ScreenFooter.cs | 38 ++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 777ec1790c..5dbc7a55ab 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Footer private Box background = null!; private FillFlowContainer buttonsFlow = null!; - private Container footerContentContainer = null!; + private Container overlayContentContainer = null!; private Container hiddenButtonsContainer = null!; private LogoTrackingContainer logoTrackingContainer = null!; @@ -102,6 +102,7 @@ namespace osu.Game.Screens.Footer { buttonsFlow = new FillFlowContainer { + Name = "Visible footer buttons", Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Y = ScreenFooterButton.CORNER_RADIUS, @@ -109,8 +110,9 @@ namespace osu.Game.Screens.Footer Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both, }, - footerContentContainer = new Container + overlayContentContainer = new Container { + Name = "Overlay-provided extra content", RelativeSizeAxes = Axes.Both, Y = -OsuGame.SCREEN_EDGE_MARGIN, }, @@ -126,6 +128,7 @@ namespace osu.Game.Screens.Footer }, hiddenButtonsContainer = new Container { + Name = "Hidden footer buttons", Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, Y = ScreenFooterButton.CORNER_RADIUS, Anchor = Anchor.BottomLeft, @@ -234,11 +237,11 @@ namespace osu.Game.Screens.Footer public ShearedOverlayContainer? ActiveOverlay { get; private set; } - private VisibilityContainer? activeFooterContent; + private VisibilityContainer? activeOverlayContent; private readonly List temporarilyHiddenButtons = new List(); - public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent) + public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? overlayContent) { if (ActiveOverlay != null) { @@ -267,12 +270,12 @@ namespace osu.Game.Screens.Footer updateColourScheme(overlay.ColourProvider.Hue); - footerContent = overlay.CreateFooterContent(); - activeFooterContent = footerContent; - var content = footerContent; + overlayContent = overlay.CreateFooterContent(); + activeOverlayContent = overlayContent; + var content = overlayContent; if (content != null) - footerContentContainer.Child = content; + overlayContentContainer.Child = content; if (temporarilyHiddenButtons.Count > 0) this.Delay(60).Schedule(() => content?.Show()); @@ -287,15 +290,19 @@ namespace osu.Game.Screens.Footer if (ActiveOverlay == null) return; - Debug.Assert(activeFooterContent != null); - activeFooterContent.Hide(); + Debug.Assert(activeOverlayContent != null); + activeOverlayContent.Hide(); - double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current; + double timeUntilRun = activeOverlayContent.LatestTransformEndTime - Time.Current; for (int i = 0; i < temporarilyHiddenButtons.Count; i++) { var button = temporarilyHiddenButtons[i]; hiddenButtonsContainer.Remove(button, false); + // temporarily bypass autosize on the X axis to prevent the buttons taking space + // immediately upon being moved back to the flow. + // this prevents the overlay content jumping to the right during its fade-out. + button.BypassAutoSizeAxes = Axes.X; buttonsFlow.Add(button); makeButtonAppearFromBottom(button, 0); @@ -305,8 +312,13 @@ namespace osu.Game.Screens.Footer updateColourScheme(OverlayColourScheme.Aquamarine.GetHue()); - activeFooterContent.Delay(timeUntilRun).Expire(); - activeFooterContent = null; + activeOverlayContent.Delay(timeUntilRun).Schedule(() => + { + // overlay content is done displaying, re-enable autosize on all active buttons + foreach (var button in buttonsFlow) + button.BypassAutoSizeAxes = Axes.None; + }).Expire(); + activeOverlayContent = null; ActiveOverlay = null; } From b4fd7ec10ffa81a4d887dda0578dfbf6bfede334 Mon Sep 17 00:00:00 2001 From: De4n <55669793+tadatomix@users.noreply.github.com> Date: Wed, 29 Oct 2025 06:18:00 +0300 Subject: [PATCH 33/35] Add a keycounter that has been actually used in `Triangles` skin (#35491) --- .../Visual/Gameplay/TestSceneSkinnableKeyCounter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs index 098f8e3246..8e9df5b2bf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs @@ -35,7 +35,9 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); - protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay(); + protected override Drawable CreateArgonImplementation() => new ArgonKeyCounterDisplay(); + + protected override Drawable CreateDefaultImplementation() => new DefaultKeyCounterDisplay(); protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay(); } From 050c10cec25a63e5c4cfc076c448b56474997874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Oct 2025 04:18:23 +0100 Subject: [PATCH 34/35] Ensure all invocations of spectator server hub methods have their errors observed (#35488) Fell out when attempting https://github.com/ppy/osu-server-spectator/pull/346. Functionally, if a true non-`HubException` is produced via an invocation of a spectator server hub method, this doesn't really do much - the error will still log as 'unobserved' due to the default handler, it will still show up on sentry, etc. The only difference is that it'll get handled via the continuation installed in `FireAndForget()` rather than the `TaskScheduler.UnobservedTaskException` event. The only real case where this is relevant is when the server throws `HubException`s, which will now instead bubble up to a more human-readable form. Which is relevant to the aforementioned PR because that one makes any hub method potentially throw a `HubException` if the client version is too old. Obviously this does nothing for the existing old clients. --- osu.Game/Online/Metadata/OnlineMetadataClient.cs | 8 ++++---- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- osu.Game/Online/Spectator/SpectatorClient.cs | 9 +++++---- .../OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs | 2 +- osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 2 +- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 2 +- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 6402962e85..75b0187388 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -89,13 +89,13 @@ namespace osu.Game.Online.Metadata userStatus.BindValueChanged(status => { if (localUser.Value is not GuestUser) - UpdateStatus(status.NewValue); + UpdateStatus(status.NewValue).FireAndForget(); }, true); userActivity.BindValueChanged(activity => { if (localUser.Value is not GuestUser) - UpdateActivity(activity.NewValue); + UpdateActivity(activity.NewValue).FireAndForget(); }, true); } @@ -121,8 +121,8 @@ namespace osu.Game.Online.Metadata if (localUser.Value is not GuestUser) { - UpdateActivity(userActivity.Value); - UpdateStatus(userStatus.Value); + UpdateActivity(userActivity.Value).FireAndForget(); + UpdateStatus(userStatus.Value).FireAndForget(); } if (lastQueueId.Value >= 0) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a58d433e7d..44cbbafe72 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -201,7 +201,7 @@ namespace osu.Game.Online.Multiplayer if (!connected.NewValue) { if (Room != null) - LeaveRoom(); + LeaveRoom().FireAndForget(); MatchmakingQueueLeft?.Invoke(); } @@ -560,7 +560,7 @@ namespace osu.Game.Online.Multiplayer return; if (user.Equals(LocalUser)) - LeaveRoom(); + LeaveRoom().FireAndForget(); handleUserLeft(user, UserKicked); }); diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 7f09fbdc9e..f245e8cf3a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -203,7 +204,7 @@ namespace osu.Game.Online.Spectator Task IStatefulUserHubClient.DisconnectRequested() { - Schedule(() => DisconnectInternal()); + Schedule(() => DisconnectInternal().FireAndForget()); return Task.CompletedTask; } @@ -290,7 +291,7 @@ namespace osu.Game.Online.Spectator else currentState.State = SpectatedUserState.Quit; - EndPlayingInternal(currentState); + EndPlayingInternal(currentState).FireAndForget(); }); } @@ -304,7 +305,7 @@ namespace osu.Game.Online.Spectator return; } - WatchUserInternal(userId); + WatchUserInternal(userId).FireAndForget(); } public void StopWatchingUser(int userId) @@ -321,7 +322,7 @@ namespace osu.Game.Online.Spectator watchedUsersRefCounts.Remove(userId); watchedUserStates.Remove(userId); - StopWatchingUserInternal(userId); + StopWatchingUserInternal(userId).FireAndForget(); }); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 95e3cb0236..527b1ba243 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match return; } - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); } /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 0b06a16d98..eb387b2664 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.LocalUser != null); if (client.LocalUser.State == MultiplayerUserState.Results) - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); } protected override string ScreenTitle => "Multiplayer"; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index bbac86fd2d..16c6a46a9c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -618,7 +618,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer updateGameplayState(); if (client.LocalUser.State == MultiplayerUserState.Ready) - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); break; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index a001863780..56120120d5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { loadingDisplay.Show(); - client.ChangeState(MultiplayerUserState.ReadyForGameplay); + client.ChangeState(MultiplayerUserState.ReadyForGameplay).FireAndForget(); } // This will pause the clock, pending the gameplay started callback from the server. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 200e6a715d..fb9343c519 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -296,7 +296,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // On a manual exit, set the player back to idle unless gameplay has finished. // Of note, this doesn't cover exiting using alt-f4 or menu home option. if (multiplayerClient.Room.State != MultiplayerRoomState.Open) - multiplayerClient.ChangeState(MultiplayerUserState.Idle); + multiplayerClient.ChangeState(MultiplayerUserState.Idle).FireAndForget(); return base.OnBackButton(); } From 4e76bd0f240e5cd8350e33f5753b253e0ca05033 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Oct 2025 13:58:20 +0900 Subject: [PATCH 35/35] Play sound when match is available even when queueing in background (#35496) --- .../Matchmaking/Queue/QueueController.cs | 99 +++++++++++++------ 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 40ac0e5777..3b9fc145d6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -32,11 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue [Resolved] private INotificationOverlay? notifications { get; set; } - [Resolved] - private IPerformFromScreenRunner? performer { get; set; } - - private ProgressNotification? backgroundNotification; - private Notification? readyNotification; + private BackgroundQueueNotification? backgroundNotification; private bool isBackgrounded; protected override void LoadComplete() @@ -118,27 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue if (backgroundNotification != null) return; - notifications?.Post(backgroundNotification = new ProgressNotification - { - Text = "Searching for opponents...", - CompletionTarget = n => notifications.Post(readyNotification = n), - CompletionText = "Your match is ready! Click to join.", - CompletionClickAction = () => - { - client.MatchmakingAcceptInvitation().FireAndForget(); - performer?.PerformFromScreen(s => s.Push(new IntroScreen())); - - closeNotifications(); - return true; - }, - CancelRequested = () => - { - client.MatchmakingLeaveQueue().FireAndForget(); - - closeNotifications(); - return true; - } - }); + notifications?.Post(backgroundNotification = new BackgroundQueueNotification()); } private void closeNotifications() @@ -146,13 +124,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue if (backgroundNotification != null) { backgroundNotification.State = ProgressNotificationState.Cancelled; - backgroundNotification.Close(false); + backgroundNotification.CloseAll(); + backgroundNotification = null; } - - readyNotification?.Close(false); - - backgroundNotification = null; - readyNotification = null; } protected override void Dispose(bool isDisposing) @@ -168,5 +142,66 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue client.MatchmakingRoomReady -= onMatchmakingRoomReady; } } + + private partial class BackgroundQueueNotification : ProgressNotification + { + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Notification? foundNotification; + private Sample? matchFoundSample; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + Text = "Searching for opponents..."; + + CompletionClickAction = () => + { + client.MatchmakingAcceptInvitation().FireAndForget(); + performer?.PerformFromScreen(s => s.Push(new IntroScreen())); + + Close(false); + return true; + }; + + CancelRequested = () => + { + client.MatchmakingLeaveQueue().FireAndForget(); + return true; + }; + + matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found"); + } + + protected override Notification CreateCompletionNotification() + { + // Playing here means it will play even if notification overlay is hidden. + // + // If we add support for the completion notification to be processed during gameplay, + // this can be moved inside the `MatchFoundNotification` implementation. + matchFoundSample?.Play(); + + return foundNotification = new MatchFoundNotification + { + Activated = CompletionClickAction, + Text = "Your match is ready! Click to join.", + }; + } + + public void CloseAll() + { + foundNotification?.Close(false); + Close(false); + } + + public partial class MatchFoundNotification : ProgressCompletionNotification + { + // for future use. + } + } } }