diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index b63c1e2888..682c4a7d26 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -700,9 +700,17 @@ namespace osu.Game.Database if (lookupSucceeded) { Debug.Assert(result != null); - beatmap.Metadata.UserTags.Clear(); - beatmap.Metadata.UserTags.AddRange(result.UserTags); - return beatmap.Metadata.UserTags.Any(); + + var userTags = result.UserTags.ToHashSet(); + + if (!userTags.SetEquals(beatmap.Metadata.UserTags)) + { + beatmap.Metadata.UserTags.Clear(); + beatmap.Metadata.UserTags.AddRange(userTags); + return true; + } + + return false; } Logger.Log(@$"Could not find {beatmap.GetDisplayString()} in local cache while backpopulating missing user tags"); diff --git a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs index 9652cb9fbc..486dfbe255 100644 --- a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs +++ b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs @@ -13,6 +13,7 @@ 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 { @@ -57,46 +58,59 @@ namespace osu.Game.Screens.SelectV2 return; } - var tagsById = (onlineBeatmapSet.RelatedTags ?? []).ToDictionary(t => t.Id); - var onlineBeatmaps = onlineBeatmapSet.Beatmaps.ToDictionary(b => b.OnlineID); - await realm.WriteAsync(r => - { - var beatmapSet = r.All().Where(b => b.OnlineID == id); - - foreach (var dbBeatmapSet in beatmapSet) - { - dbBeatmapSet.Status = onlineBeatmapSet.Status; - - foreach (var dbBeatmap in dbBeatmapSet.Beatmaps) - { - 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); - } - } - } - }).ConfigureAwait(true); + await realm.WriteAsync(r => updateRealmBeatmapSet(r, onlineBeatmapSet)).ConfigureAwait(true); tcs.SetResult(onlineBeatmapSet); }; request.Failure += tcs.SetException; api.Queue(request); return tcs.Task; } + + private static void updateRealmBeatmapSet(Realm r, APIBeatmapSet onlineBeatmapSet) + { + var tagsById = (onlineBeatmapSet.RelatedTags ?? []).ToDictionary(t => t.Id); + var onlineBeatmaps = onlineBeatmapSet.Beatmaps.ToDictionary(b => b.OnlineID); + + var dbBeatmapSets = r.All().Where(b => b.OnlineID == onlineBeatmapSet.OnlineID); + + foreach (var dbBeatmapSet in dbBeatmapSets) + { + // note that every single write to realm models is preceded by a guard, even if it technically would write the same value back. + // the reason this matters is that doing so avoids triggering realm subscription callbacks. + // unfortunately in terms of subscriptions realm treats *every* write to any realm object as a modification, + // even if the write was redundant and had no observable effect. + + if (dbBeatmapSet.Status != onlineBeatmapSet.Status) + dbBeatmapSet.Status = onlineBeatmapSet.Status; + + foreach (var dbBeatmap in dbBeatmapSet.Beatmaps) + { + if (onlineBeatmaps.TryGetValue(dbBeatmap.OnlineID, out var onlineBeatmap)) + { + // compare `BeatmapUpdaterMetadataLookup` + if (dbBeatmap.OnlineMD5Hash != onlineBeatmap.MD5Hash) + dbBeatmap.OnlineMD5Hash = onlineBeatmap.MD5Hash; + + if (dbBeatmap.LastOnlineUpdate != onlineBeatmap.LastUpdated) + dbBeatmap.LastOnlineUpdate = onlineBeatmap.LastUpdated; + + if (dbBeatmap.MatchesOnlineVersion && dbBeatmap.Status != onlineBeatmap.Status) + dbBeatmap.Status = onlineBeatmap.Status; + + HashSet userTags = onlineBeatmap.TopTags? + .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId))) + .Where(t => t.relatedTag != null) + .Select(t => t.relatedTag!.Name) + .ToHashSet() ?? []; + + if (!userTags.SetEquals(dbBeatmap.Metadata.UserTags)) + { + dbBeatmap.Metadata.UserTags.Clear(); + dbBeatmap.Metadata.UserTags.AddRange(userTags); + } + } + } + } + } } }