1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-23 02:22:55 +08:00

Use bindable list implementation

This commit is contained in:
Dean Herbert 2024-08-27 18:13:52 +09:00
parent 466ed5de78
commit 4d42274771
No known key found for this signature in database
2 changed files with 78 additions and 83 deletions

View File

@ -2,10 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -13,42 +13,36 @@ using Realms;
namespace osu.Game.Database namespace osu.Game.Database
{ {
// TODO: handle realm migration
public partial class DetachedBeatmapStore : Component public partial class DetachedBeatmapStore : Component
{ {
private readonly ManualResetEventSlim loaded = new ManualResetEventSlim(); private readonly ManualResetEventSlim loaded = new ManualResetEventSlim();
private List<BeatmapSetInfo> originalBeatmapSetsDetached = new List<BeatmapSetInfo>(); private readonly BindableList<BeatmapSetInfo> detachedBeatmapSets = new BindableList<BeatmapSetInfo>();
private IDisposable? subscriptionSets; private IDisposable? realmSubscription;
/// <summary>
/// Track GUIDs of all sets in realm to allow handling deletions.
/// </summary>
private readonly List<Guid> realmBeatmapSets = new List<Guid>();
[Resolved] [Resolved]
private RealmAccess realm { get; set; } = null!; private RealmAccess realm { get; set; } = null!;
public IReadOnlyList<BeatmapSetInfo> GetDetachedBeatmaps() public IBindableList<BeatmapSetInfo> GetDetachedBeatmaps()
{ {
if (!loaded.Wait(60000)) if (!loaded.Wait(60000))
Logger.Error(new TimeoutException("Beatmaps did not load in an acceptable time"), $"{nameof(DetachedBeatmapStore)} fell over"); Logger.Error(new TimeoutException("Beatmaps did not load in an acceptable time"), $"{nameof(DetachedBeatmapStore)} fell over");
return originalBeatmapSetsDetached; return detachedBeatmapSets.GetBoundCopy();
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); realmSubscription = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged);
} }
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes) private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{ {
if (changes == null) if (changes == null)
{ {
if (originalBeatmapSetsDetached.Count > 0 && sender.Count == 0) if (detachedBeatmapSets.Count > 0 && sender.Count == 0)
{ {
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm. // Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
// Additionally, user should not be at song select when realm is blocking all operations in the first place. // Additionally, user should not be at song select when realm is blocking all operations in the first place.
@ -59,57 +53,29 @@ namespace osu.Game.Database
return; return;
} }
originalBeatmapSetsDetached = sender.Detach(); detachedBeatmapSets.Clear();
detachedBeatmapSets.AddRange(sender.Detach());
realmBeatmapSets.Clear();
realmBeatmapSets.AddRange(sender.Select(r => r.ID));
loaded.Set(); loaded.Set();
return; return;
} }
HashSet<Guid> setsRequiringUpdate = new HashSet<Guid>();
HashSet<Guid> setsRequiringRemoval = new HashSet<Guid>();
foreach (int i in changes.DeletedIndices.OrderDescending()) foreach (int i in changes.DeletedIndices.OrderDescending())
{ detachedBeatmapSets.RemoveAt(i);
Guid id = realmBeatmapSets[i];
setsRequiringRemoval.Add(id);
setsRequiringUpdate.Remove(id);
realmBeatmapSets.RemoveAt(i);
}
foreach (int i in changes.InsertedIndices) foreach (int i in changes.InsertedIndices)
{ {
Guid id = sender[i].ID; detachedBeatmapSets.Insert(i, sender[i].Detach());
setsRequiringRemoval.Remove(id);
setsRequiringUpdate.Add(id);
realmBeatmapSets.Insert(i, id);
} }
foreach (int i in changes.NewModifiedIndices) foreach (int i in changes.NewModifiedIndices)
setsRequiringUpdate.Add(sender[i].ID); detachedBeatmapSets.ReplaceRange(i, 1, new[] { sender[i].Detach() });
// deletions
foreach (Guid g in setsRequiringRemoval)
originalBeatmapSetsDetached.RemoveAll(set => set.ID == g);
// updates
foreach (Guid g in setsRequiringUpdate)
{
originalBeatmapSetsDetached.RemoveAll(set => set.ID == g);
originalBeatmapSetsDetached.Add(fetchFromID(g)!);
}
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
subscriptionSets?.Dispose(); realmSubscription?.Dispose();
} }
private IQueryable<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected); private IQueryable<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -21,6 +22,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
@ -108,7 +110,12 @@ namespace osu.Game.Screens.Select
protected readonly CarouselScrollContainer Scroll; protected readonly CarouselScrollContainer Scroll;
[Resolved] [Resolved]
private DetachedBeatmapStore detachedBeatmapStore { get; set; } = null!; private RealmAccess realm { get; set; } = null!;
[Resolved]
private DetachedBeatmapStore? detachedBeatmapStore { get; set; }
private IBindableList<BeatmapSetInfo> detachedBeatmapSets = null!;
private readonly NoResultsPlaceholder noResultsPlaceholder; private readonly NoResultsPlaceholder noResultsPlaceholder;
@ -165,12 +172,6 @@ namespace osu.Game.Screens.Select
applyActiveCriteria(false); applyActiveCriteria(false);
if (loadedTestBeatmaps)
{
invalidateAfterChange();
BeatmapSetsLoaded = true;
}
// Restore selection // Restore selection
if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates)) if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates))
{ {
@ -179,6 +180,12 @@ namespace osu.Game.Screens.Select
if (found != null) if (found != null)
found.State.Value = CarouselItemState.Selected; found.State.Value = CarouselItemState.Selected;
} }
Schedule(() =>
{
invalidateAfterChange();
BeatmapSetsLoaded = true;
});
} }
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>(); private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
@ -194,7 +201,6 @@ namespace osu.Game.Screens.Select
private CarouselRoot root; private CarouselRoot root;
private IDisposable? subscriptionSets;
private IDisposable? subscriptionBeatmaps; private IDisposable? subscriptionBeatmaps;
private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100); private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
@ -245,32 +251,62 @@ namespace osu.Game.Screens.Select
// This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons // This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons
// we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update // we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update
// thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time). // thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time).
loadBeatmapSets(detachedBeatmapStore.GetDetachedBeatmaps()); detachedBeatmapSets = detachedBeatmapStore!.GetDetachedBeatmaps();
detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged);
loadBeatmapSets(detachedBeatmapSets);
} }
} }
[Resolved]
private RealmAccess realm { get; set; } = null!;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged);
subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All<BeatmapInfo>().Where(b => !b.Hidden), beatmapsChanged); subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All<BeatmapInfo>().Where(b => !b.Hidden), beatmapsChanged);
} }
private IQueryable<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected); private readonly HashSet<BeatmapSetInfo> setsRequiringUpdate = new HashSet<BeatmapSetInfo>();
private readonly HashSet<BeatmapSetInfo> setsRequiringRemoval = new HashSet<BeatmapSetInfo>();
private readonly HashSet<Guid> setsRequiringUpdate = new HashSet<Guid>(); private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
private readonly HashSet<Guid> setsRequiringRemoval = new HashSet<Guid>();
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{ {
// If loading test beatmaps, avoid overwriting with realm subscription callbacks. // If loading test beatmaps, avoid overwriting with realm subscription callbacks.
if (loadedTestBeatmaps) if (loadedTestBeatmaps)
return; return;
var newBeatmapSets = changed.NewItems!.Cast<BeatmapSetInfo>();
var newBeatmapSetIDs = newBeatmapSets.Select(s => s.ID).ToHashSet();
var oldBeatmapSets = changed.OldItems!.Cast<BeatmapSetInfo>();
var oldBeatmapSetIDs = oldBeatmapSets.Select(s => s.ID).ToHashSet();
switch (changed.Action)
{
case NotifyCollectionChangedAction.Add:
setsRequiringRemoval.RemoveWhere(s => newBeatmapSetIDs.Contains(s.ID));
setsRequiringUpdate.AddRange(newBeatmapSets);
break;
case NotifyCollectionChangedAction.Remove:
setsRequiringUpdate.RemoveWhere(s => oldBeatmapSetIDs.Contains(s.ID));
setsRequiringRemoval.AddRange(oldBeatmapSets);
break;
case NotifyCollectionChangedAction.Replace:
setsRequiringUpdate.AddRange(newBeatmapSets);
break;
case NotifyCollectionChangedAction.Move:
setsRequiringUpdate.AddRange(newBeatmapSets);
break;
case NotifyCollectionChangedAction.Reset:
setsRequiringRemoval.Clear();
setsRequiringUpdate.Clear();
loadBeatmapSets(detachedBeatmapSets);
break;
}
Scheduler.AddOnce(processBeatmapChanges); Scheduler.AddOnce(processBeatmapChanges);
} }
@ -282,9 +318,10 @@ namespace osu.Game.Screens.Select
{ {
try try
{ {
foreach (var set in setsRequiringRemoval) removeBeatmapSet(set); // TODO: chekc whether we still need beatmap sets by ID
foreach (var set in setsRequiringRemoval) removeBeatmapSet(set.ID);
foreach (var set in setsRequiringUpdate) updateBeatmapSet(fetchFromID(set)!); foreach (var set in setsRequiringUpdate) updateBeatmapSet(set);
if (setsRequiringRemoval.Count > 0 && SelectedBeatmapInfo != null) if (setsRequiringRemoval.Count > 0 && SelectedBeatmapInfo != null)
{ {
@ -302,7 +339,7 @@ namespace osu.Game.Screens.Select
// This relies on the full update operation being in a single transaction, so please don't change that. // This relies on the full update operation being in a single transaction, so please don't change that.
foreach (var set in setsRequiringUpdate) foreach (var set in setsRequiringUpdate)
{ {
foreach (var beatmapInfo in fetchFromID(set)!.Beatmaps) foreach (var beatmapInfo in set.Beatmaps)
{ {
if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) continue; if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) continue;
@ -317,7 +354,7 @@ namespace osu.Game.Screens.Select
// If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. // 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. // Let's attempt to follow set-level selection anyway.
SelectBeatmap(fetchFromID(setsRequiringUpdate.First())!.Beatmaps.First()); SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First());
} }
} }
} }
@ -353,7 +390,7 @@ namespace osu.Game.Screens.Select
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets)
&& existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID))
{ {
updateBeatmapSet(beatmapSet.Detach()); updateBeatmapSet(beatmapSet);
changed = true; changed = true;
} }
} }
@ -383,21 +420,14 @@ namespace osu.Game.Screens.Select
} }
} }
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{ {
beatmapSet = beatmapSet.Detach(); updateBeatmapSet(beatmapSet);
invalidateAfterChange();
Schedule(() => });
{
updateBeatmapSet(beatmapSet);
invalidateAfterChange();
});
}
private void updateBeatmapSet(BeatmapSetInfo beatmapSet) private void updateBeatmapSet(BeatmapSetInfo beatmapSet)
{ {
beatmapSet = beatmapSet.Detach();
var newSets = new List<CarouselBeatmapSet>(); var newSets = new List<CarouselBeatmapSet>();
if (beatmapsSplitOut) if (beatmapsSplitOut)
@ -696,7 +726,7 @@ namespace osu.Game.Screens.Select
if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut)
{ {
beatmapsSplitOut = activeCriteria.SplitOutDifficulties; beatmapsSplitOut = activeCriteria.SplitOutDifficulties;
loadBeatmapSets(detachedBeatmapStore.GetDetachedBeatmaps()); loadBeatmapSets(detachedBeatmapSets);
return; return;
} }
@ -1245,7 +1275,6 @@ namespace osu.Game.Screens.Select
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
subscriptionSets?.Dispose();
subscriptionBeatmaps?.Dispose(); subscriptionBeatmaps?.Dispose();
} }
} }