From 9354aba1f6f8e280bb696fe39b81511120c0ace6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Jul 2025 13:23:16 +0200 Subject: [PATCH 1/2] Add population of online status related properties to `RealmPopulatingOnlineLookupSource` --- .../RealmPopulatingOnlineLookupSource.cs | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs index c2ede24a5d..95e5568bcd 100644 --- a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs +++ b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs @@ -13,7 +13,6 @@ using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using Realms; namespace osu.Game.Screens.SelectV2 { @@ -25,6 +24,7 @@ namespace osu.Game.Screens.SelectV2 /// This component is designed to locally persist potentially-volatile online information such as: /// /// user tags assigned to difficulties of a beatmap, + /// the beatmap's , /// guest mappers assigned to difficulties of a beatmap, /// the local user's best score on a given beatmap. /// @@ -54,20 +54,34 @@ namespace osu.Game.Screens.SelectV2 var onlineBeatmaps = onlineBeatmapSet.Beatmaps.ToDictionary(b => b.OnlineID); realm.Write(r => { - foreach (var dbBeatmap in r.All().Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.OnlineID)} == $0", id)) + var beatmapSet = r.All().Where(b => b.OnlineID == id); + + foreach (var dbBeatmapSet in beatmapSet) { - if (onlineBeatmaps.TryGetValue(dbBeatmap.OnlineID, out var onlineBeatmap)) + dbBeatmapSet.Status = onlineBeatmapSet.Status; + + foreach (var dbBeatmap in dbBeatmapSet.Beatmaps) { - string[] userTagsArray = onlineBeatmap.TopTags? - .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId))) - .Where(t => t.relatedTag != null) - // see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria - .OrderByDescending(t => t.topTag.VoteCount) - .ThenBy(t => t.relatedTag!.Name) - .Select(t => t.relatedTag!.Name) - .ToArray() ?? []; - dbBeatmap.Metadata.UserTags.Clear(); - dbBeatmap.Metadata.UserTags.AddRange(userTagsArray); + if (onlineBeatmaps.TryGetValue(dbBeatmap.OnlineID, out var onlineBeatmap)) + { + // compare `BeatmapUpdaterMetadataLookup` + dbBeatmap.OnlineMD5Hash = onlineBeatmap.MD5Hash; + dbBeatmap.LastOnlineUpdate = onlineBeatmap.LastUpdated; + + if (dbBeatmap.MatchesOnlineVersion) + dbBeatmap.Status = onlineBeatmap.Status; + + string[] userTagsArray = onlineBeatmap.TopTags? + .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId))) + .Where(t => t.relatedTag != null) + // see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria + .OrderByDescending(t => t.topTag.VoteCount) + .ThenBy(t => t.relatedTag!.Name) + .Select(t => t.relatedTag!.Name) + .ToArray() ?? []; + dbBeatmap.Metadata.UserTags.Clear(); + dbBeatmap.Metadata.UserTags.AddRange(userTagsArray); + } } } }); From 079bba7d3e73249cec37924f81e00115e70b84ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Jul 2025 13:36:24 +0200 Subject: [PATCH 2/2] Use `RealmPopulatingOnlineLookupSource` in title wedge This sort of guarantees that the wedge displays the latest online status of the map. --- .../TestSceneBeatmapTitleWedge.cs | 4 ++ .../Screens/SelectV2/BeatmapTitleWedge.cs | 50 +++++++++++++------ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index efd9f6a5cd..f081f6d9b9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -43,6 +43,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private APIBeatmapSet? currentOnlineSet; + [Cached] + private RealmPopulatingOnlineLookupSource lookupSource = new RealmPopulatingOnlineLookupSource(); + [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { @@ -55,6 +58,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddRange(new Drawable[] { + lookupSource, new Container { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 28031f12fc..f8ee500928 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -8,18 +8,19 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; @@ -69,10 +70,14 @@ namespace osu.Game.Screens.SelectV2 private LocalisationManager localisation { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } = null!; + private RealmPopulatingOnlineLookupSource onlineLookupSource { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; private APIBeatmapSet? currentOnlineBeatmapSet; - private GetBeatmapSetRequest? currentRequest; + private CancellationTokenSource? cancellationTokenSource; + private Task? currentFetchTask; private FillFlowContainer statisticsFlow = null!; @@ -291,28 +296,27 @@ namespace osu.Game.Screens.SelectV2 { var beatmapSetInfo = working.Value.BeatmapSetInfo; - currentRequest?.Cancel(); - currentRequest = null; + cancellationTokenSource?.Cancel(); currentOnlineBeatmapSet = null; if (beatmapSetInfo.OnlineID >= 1) { - // todo: consider introducing a BeatmapSetLookupCache for caching benefits. - currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID); - currentRequest.Failure += _ => updateOnlineDisplay(); - currentRequest.Success += s => + cancellationTokenSource = new CancellationTokenSource(); + currentFetchTask = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID); + currentFetchTask.ContinueWith(t => { - currentOnlineBeatmapSet = s; - updateOnlineDisplay(); - }; - - api.Queue(currentRequest); + if (t.IsCompletedSuccessfully) + currentOnlineBeatmapSet = t.GetResultSafely(); + if (t.Exception != null) + Logger.Log($"Error when fetching online beatmap set: {t.Exception}", LoggingTarget.Network); + Scheduler.AddOnce(updateOnlineDisplay); + }); } } private void updateOnlineDisplay() { - if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) + if (currentFetchTask?.IsCompleted == false) { playCount.Value = null; favouriteButton.SetLoading(); @@ -322,6 +326,20 @@ namespace osu.Game.Screens.SelectV2 var onlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID); playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1); favouriteButton.SetBeatmapSet(currentOnlineBeatmapSet); + + // the online fetch may have also updated the beatmap's status. + // this needs to be checked against the *local* beatmap model rather than the online one, because it's not known here whether the status change has occurred or not + // (think scenarios like the beatmap being locally modified). + // it also has to be handled explicitly like this because the working beatmap's `BeatmapInfo` will not receive these updates due to being detached + // (and because of https://github.com/ppy/osu/blob/4b73afd1957a9161e2956fc4191c8114d9958372/osu.Game/Screens/SelectV2/SongSelect.cs#L487-L488 + // which prevents working beatmap refetches caused by changes to the realm model of perceived low importance). + var status = realm.Run(r => + { + var refetchedBeatmap = r.Find(working.Value.BeatmapInfo.ID); + return refetchedBeatmap?.Status; + }); + if (status != null) + statusPill.Status = status.Value; } } }