1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-17 06:02:55 +08:00

Add basic detached beatmap store

This commit is contained in:
Dean Herbert 2024-08-27 17:37:15 +09:00
parent 321e509f11
commit 466ed5de78
No known key found for this signature in database
3 changed files with 129 additions and 81 deletions

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using Realms;
namespace osu.Game.Database
{
// TODO: handle realm migration
public partial class DetachedBeatmapStore : Component
{
private readonly ManualResetEventSlim loaded = new ManualResetEventSlim();
private List<BeatmapSetInfo> originalBeatmapSetsDetached = new List<BeatmapSetInfo>();
private IDisposable? subscriptionSets;
/// <summary>
/// Track GUIDs of all sets in realm to allow handling deletions.
/// </summary>
private readonly List<Guid> realmBeatmapSets = new List<Guid>();
[Resolved]
private RealmAccess realm { get; set; } = null!;
public IReadOnlyList<BeatmapSetInfo> GetDetachedBeatmaps()
{
if (!loaded.Wait(60000))
Logger.Error(new TimeoutException("Beatmaps did not load in an acceptable time"), $"{nameof(DetachedBeatmapStore)} fell over");
return originalBeatmapSetsDetached;
}
[BackgroundDependencyLoader]
private void load()
{
subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged);
}
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
if (changes == null)
{
if (originalBeatmapSetsDetached.Count > 0 && sender.Count == 0)
{
// 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.
//
// Note that due to the catch-up logic below, once operations are restored we will still be in a roughly
// correct state. The only things that this return will change is the carousel will not empty *during* the blocking
// operation.
return;
}
originalBeatmapSetsDetached = sender.Detach();
realmBeatmapSets.Clear();
realmBeatmapSets.AddRange(sender.Select(r => r.ID));
loaded.Set();
return;
}
HashSet<Guid> setsRequiringUpdate = new HashSet<Guid>();
HashSet<Guid> setsRequiringRemoval = new HashSet<Guid>();
foreach (int i in changes.DeletedIndices.OrderDescending())
{
Guid id = realmBeatmapSets[i];
setsRequiringRemoval.Add(id);
setsRequiringUpdate.Remove(id);
realmBeatmapSets.RemoveAt(i);
}
foreach (int i in changes.InsertedIndices)
{
Guid id = sender[i].ID;
setsRequiringRemoval.Remove(id);
setsRequiringUpdate.Add(id);
realmBeatmapSets.Insert(i, id);
}
foreach (int i in changes.NewModifiedIndices)
setsRequiringUpdate.Add(sender[i].ID);
// 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)
{
base.Dispose(isDisposing);
subscriptionSets?.Dispose();
}
private IQueryable<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
}
}

View File

@ -1141,6 +1141,7 @@ namespace osu.Game
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
loadComponentSingleFile(new DetachedBeatmapStore(), Add, true);
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener());

View File

@ -76,8 +76,6 @@ namespace osu.Game.Screens.Select
private CarouselBeatmapSet? selectedBeatmapSet;
private List<BeatmapSetInfo> originalBeatmapSetsDetached = new List<BeatmapSetInfo>();
/// <summary>
/// Raised when the <see cref="SelectedBeatmapInfo"/> is changed.
/// </summary>
@ -109,6 +107,9 @@ namespace osu.Game.Screens.Select
[Cached]
protected readonly CarouselScrollContainer Scroll;
[Resolved]
private DetachedBeatmapStore detachedBeatmapStore { get; set; } = null!;
private readonly NoResultsPlaceholder noResultsPlaceholder;
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Items.OfType<CarouselBeatmapSet>();
@ -128,9 +129,7 @@ namespace osu.Game.Screens.Select
private void loadBeatmapSets(IEnumerable<BeatmapSetInfo> beatmapSets)
{
originalBeatmapSetsDetached = beatmapSets.Detach();
if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet))
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
selectedBeatmapSet = null;
var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo;
@ -139,7 +138,7 @@ namespace osu.Game.Screens.Select
if (beatmapsSplitOut)
{
var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b =>
var carouselBeatmapSets = beatmapSets.SelectMany(s => s.Beatmaps).Select(b =>
{
return createCarouselSet(new BeatmapSetInfo(new[] { b })
{
@ -153,7 +152,7 @@ namespace osu.Game.Screens.Select
}
else
{
var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType<CarouselBeatmapSet>();
var carouselBeatmapSets = beatmapSets.Select(createCarouselSet).OfType<CarouselBeatmapSet>();
newRoot.AddItems(carouselBeatmapSets);
}
@ -230,7 +229,7 @@ namespace osu.Game.Screens.Select
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, AudioManager audio)
private void load(OsuConfigManager config, AudioManager audio, DetachedBeatmapStore beatmapStore)
{
spinSample = audio.Samples.Get("SongSelect/random-spin");
randomSelectSample = audio.Samples.Get(@"SongSelect/select-random");
@ -246,18 +245,13 @@ namespace osu.Game.Screens.Select
// 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
// thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time).
realm.Run(r => loadBeatmapSets(getBeatmapSets(r)));
loadBeatmapSets(detachedBeatmapStore.GetDetachedBeatmaps());
}
}
[Resolved]
private RealmAccess realm { get; set; } = null!;
/// <summary>
/// Track GUIDs of all sets in realm to allow handling deletions.
/// </summary>
private readonly List<Guid> realmBeatmapSets = new List<Guid>();
protected override void LoadComplete()
{
base.LoadComplete();
@ -266,6 +260,8 @@ namespace osu.Game.Screens.Select
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<Guid> setsRequiringUpdate = new HashSet<Guid>();
private readonly HashSet<Guid> setsRequiringRemoval = new HashSet<Guid>();
@ -275,65 +271,6 @@ namespace osu.Game.Screens.Select
if (loadedTestBeatmaps)
return;
if (changes == null)
{
realmBeatmapSets.Clear();
realmBeatmapSets.AddRange(sender.Select(r => r.ID));
if (originalBeatmapSetsDetached.Count > 0 && sender.Count == 0)
{
// 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.
//
// Note that due to the catch-up logic below, once operations are restored we will still be in a roughly
// correct state. The only things that this return will change is the carousel will not empty *during* the blocking
// operation.
return;
}
// Do a full two-way check for missing (or incorrectly present) beatmaps.
// Let's assume that the worst that can happen is deletions or additions.
setsRequiringRemoval.Clear();
setsRequiringUpdate.Clear();
foreach (Guid id in realmBeatmapSets)
{
if (!root.BeatmapSetsByID.ContainsKey(id))
setsRequiringUpdate.Add(id);
}
foreach (Guid id in root.BeatmapSetsByID.Keys)
{
if (!realmBeatmapSets.Contains(id))
setsRequiringRemoval.Add(id);
}
}
else
{
foreach (int i in changes.DeletedIndices.OrderDescending())
{
Guid id = realmBeatmapSets[i];
setsRequiringRemoval.Add(id);
setsRequiringUpdate.Remove(id);
realmBeatmapSets.RemoveAt(i);
}
foreach (int i in changes.InsertedIndices)
{
Guid id = sender[i].ID;
setsRequiringRemoval.Remove(id);
setsRequiringUpdate.Add(id);
realmBeatmapSets.Insert(i, id);
}
foreach (int i in changes.NewModifiedIndices)
setsRequiringUpdate.Add(sender[i].ID);
}
Scheduler.AddOnce(processBeatmapChanges);
}
@ -425,8 +362,6 @@ namespace osu.Game.Screens.Select
invalidateAfterChange();
}
private IQueryable<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{
removeBeatmapSet(beatmapSet.ID);
@ -438,8 +373,6 @@ namespace osu.Game.Screens.Select
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets))
return;
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID);
foreach (var set in existingSets)
{
foreach (var beatmap in set.Beatmaps)
@ -465,9 +398,6 @@ namespace osu.Game.Screens.Select
{
beatmapSet = beatmapSet.Detach();
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID);
originalBeatmapSetsDetached.Add(beatmapSet);
var newSets = new List<CarouselBeatmapSet>();
if (beatmapsSplitOut)
@ -766,7 +696,7 @@ namespace osu.Game.Screens.Select
if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut)
{
beatmapsSplitOut = activeCriteria.SplitOutDifficulties;
loadBeatmapSets(originalBeatmapSetsDetached);
loadBeatmapSets(detachedBeatmapStore.GetDetachedBeatmaps());
return;
}