diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index fe7eee701f5f..53529d592b4a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -301,6 +301,9 @@ private void beatmapSetsChanged(IRealmCollection sender, ChangeS if (loadedTestBeatmaps) return; + var setsRequiringUpdate = new HashSet(); + var setsRequiringRemoval = new HashSet(); + if (changes == null) { // During initial population, we must manually account for the fact that our original query was done on an async thread. @@ -314,67 +317,80 @@ private void beatmapSetsChanged(IRealmCollection sender, ChangeS foreach (var id in realmSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) - updateBeatmapSet(realm.Realm.Find(id)!.Detach()); + setsRequiringUpdate.Add(realm.Realm.Find(id)!.Detach()); } foreach (var id in root.BeatmapSetsByID.Keys) { if (!realmSets.Contains(id)) - removeBeatmapSet(id); + setsRequiringRemoval.Add(id); } - - invalidateAfterChange(); - BeatmapSetsLoaded = true; - return; } + else + { + foreach (int i in changes.NewModifiedIndices) + setsRequiringUpdate.Add(sender[i].Detach()); - foreach (int i in changes.NewModifiedIndices) - updateBeatmapSet(sender[i].Detach()); - - foreach (int i in changes.InsertedIndices) - updateBeatmapSet(sender[i].Detach()); + foreach (int i in changes.InsertedIndices) + setsRequiringUpdate.Add(sender[i].Detach()); + } - if (changes.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null) + // All local operations must be scheduled. + // + // If we don't schedule, beatmaps getting changed while song select is suspended (ie. last played being updated) + // will cause unexpected sounds and operations to occur in the background. + Schedule(() => { - // If SelectedBeatmapInfo is non-null, the set should also be non-null. - Debug.Assert(SelectedBeatmapSet != null); - - // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. - // When an update occurs, the previous beatmap set is either soft or hard deleted. - // Check if the current selection was potentially deleted by re-querying its validity. - bool selectedSetMarkedDeleted = sender.Realm.Find(SelectedBeatmapSet.ID)?.DeletePending != false; + try + { + foreach (var set in setsRequiringRemoval) + removeBeatmapSet(set); - int[] modifiedAndInserted = changes.NewModifiedIndices.Concat(changes.InsertedIndices).ToArray(); + foreach (var set in setsRequiringUpdate) + updateBeatmapSet(set); - if (selectedSetMarkedDeleted && modifiedAndInserted.Any()) - { - // If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices. - // This relies on the full update operation being in a single transaction, so please don't change that. - foreach (int i in modifiedAndInserted) + if (changes?.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null) { - var beatmapSetInfo = sender[i]; + // If SelectedBeatmapInfo is non-null, the set should also be non-null. + Debug.Assert(SelectedBeatmapSet != null); - foreach (var beatmapInfo in beatmapSetInfo.Beatmaps) - { - if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) - continue; + // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. + // When an update occurs, the previous beatmap set is either soft or hard deleted. + // Check if the current selection was potentially deleted by re-querying its validity. + bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID)?.DeletePending != false); - // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. - if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName) + if (selectedSetMarkedDeleted && setsRequiringUpdate.Any()) + { + // If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices. + // This relies on the full update operation being in a single transaction, so please don't change that. + foreach (var set in setsRequiringUpdate) { - SelectBeatmap(beatmapInfo); - return; + foreach (var beatmapInfo in set.Beatmaps) + { + if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) + continue; + + // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. + if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName) + { + SelectBeatmap(beatmapInfo); + return; + } + } } + + // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. + // Let's attempt to follow set-level selection anyway. + SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First()); } } - - // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. - // Let's attempt to follow set-level selection anyway. - SelectBeatmap(sender[modifiedAndInserted.First()].Beatmaps.First()); } - } - - invalidateAfterChange(); + finally + { + BeatmapSetsLoaded = true; + invalidateAfterChange(); + } + }); } private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes)