Create a generic archive download manager (#5000)

Create a generic archive download manager

Co-authored-by: Dean Herbert <pe@ppy.sh>
This commit is contained in:
Dean Herbert 2019-06-26 21:24:36 +09:00 committed by GitHub
commit 1c9b6e7af3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 330 additions and 244 deletions

View File

@ -108,7 +108,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>();
// ReSharper disable once AccessToModifiedClosure
manager.ItemAdded += (_, __) => Interlocked.Increment(ref itemAddRemoveFireCount);
manager.ItemAdded += _ => Interlocked.Increment(ref itemAddRemoveFireCount);
manager.ItemRemoved += _ => Interlocked.Increment(ref itemAddRemoveFireCount);
var imported = await LoadOszIntoOsu(osu);

View File

@ -21,7 +21,6 @@ using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
namespace osu.Game.Beatmaps
@ -29,7 +28,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
public partial class BeatmapManager : ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
{
/// <summary>
/// Fired when a single difficulty has been hidden.
@ -41,16 +40,6 @@ namespace osu.Game.Beatmaps
/// </summary>
public event Action<BeatmapInfo> BeatmapRestored;
/// <summary>
/// Fired when a beatmap download begins.
/// </summary>
public event Action<DownloadBeatmapSetRequest> BeatmapDownloadBegan;
/// <summary>
/// Fired when a beatmap download is interrupted, due to user cancellation or other failures.
/// </summary>
public event Action<DownloadBeatmapSetRequest> BeatmapDownloadFailed;
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
@ -66,22 +55,17 @@ namespace osu.Game.Beatmaps
private readonly BeatmapStore beatmaps;
private readonly IAPIProvider api;
private readonly AudioManager audioManager;
private readonly GameHost host;
private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
private readonly BeatmapUpdateQueue updateQueue;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null)
: base(storage, contextFactory, new BeatmapStore(contextFactory), host)
: base(storage, contextFactory, api, new BeatmapStore(contextFactory), host)
{
this.rulesets = rulesets;
this.api = api;
this.audioManager = audioManager;
this.host = host;
@ -94,6 +78,9 @@ namespace osu.Game.Beatmaps
updateQueue = new BeatmapUpdateQueue(api);
}
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
{
if (archive != null)
@ -158,87 +145,6 @@ namespace osu.Game.Beatmaps
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
}
/// <summary>
/// Downloads a beatmap.
/// This will post notifications tracking progress.
/// </summary>
/// <param name="beatmapSetInfo">The <see cref="BeatmapSetInfo"/> to be downloaded.</param>
/// <param name="noVideo">Whether the beatmap should be downloaded without video. Defaults to false.</param>
/// <returns>Downloading can happen</returns>
public bool Download(BeatmapSetInfo beatmapSetInfo, bool noVideo = false)
{
var existing = GetExistingDownload(beatmapSetInfo);
if (existing != null || api == null) return false;
var downloadNotification = new DownloadNotification
{
Text = $"Downloading {beatmapSetInfo}",
};
var request = new DownloadBeatmapSetRequest(beatmapSetInfo, noVideo);
request.DownloadProgressed += progress =>
{
downloadNotification.State = ProgressNotificationState.Active;
downloadNotification.Progress = progress;
};
request.Success += filename =>
{
Task.Factory.StartNew(async () =>
{
// This gets scheduled back to the update thread, but we want the import to run in the background.
await Import(downloadNotification, filename);
currentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning);
};
request.Failure += error =>
{
BeatmapDownloadFailed?.Invoke(request);
if (error is OperationCanceledException) return;
downloadNotification.State = ProgressNotificationState.Cancelled;
Logger.Error(error, "Beatmap download failed!");
currentDownloads.Remove(request);
};
downloadNotification.CancelRequested += () =>
{
request.Cancel();
currentDownloads.Remove(request);
downloadNotification.State = ProgressNotificationState.Cancelled;
return true;
};
currentDownloads.Add(request);
PostNotification?.Invoke(downloadNotification);
// don't run in the main api queue as this is a long-running task.
Task.Factory.StartNew(() =>
{
try
{
request.Perform(api);
}
catch
{
// no need to handle here as exceptions will filter down to request.Failure above.
}
}, TaskCreationOptions.LongRunning);
BeatmapDownloadBegan?.Invoke(request);
return true;
}
/// <summary>
/// Get an existing download request if it exists.
/// </summary>
/// <param name="beatmap">The <see cref="BeatmapSetInfo"/> whose download request is wanted.</param>
/// <returns>The <see cref="DownloadBeatmapSetRequest"/> object if it exists, or null.</returns>
public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID);
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>
@ -406,22 +312,6 @@ namespace osu.Game.Beatmaps
protected override Track GetTrack() => null;
}
private class DownloadNotification : ProgressNotification
{
public override bool IsImportant => false;
protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification
{
Activated = CompletionClickAction,
Text = CompletionText
};
private class SilencedProgressCompletionNotification : ProgressCompletionNotification
{
public override bool IsImportant => false;
}
}
private class BeatmapUpdateQueue
{
private readonly IAPIProvider api;

View File

@ -31,12 +31,10 @@ namespace osu.Game.Database
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : ArchiveModelManager, ICanAcceptFiles
public abstract class ArchiveModelManager<TModel, TFileModel> : ArchiveModelManager, ICanAcceptFiles, IModelManager<TModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : INamedFileInfo, new()
{
public delegate void ItemAddedDelegate(TModel model, bool existing);
/// <summary>
/// Set an endpoint for notifications to be posted to.
/// </summary>
@ -46,7 +44,7 @@ namespace osu.Game.Database
/// Fired when a new <see cref="TModel"/> becomes available in the database.
/// This is not guaranteed to run on the update thread.
/// </summary>
public event ItemAddedDelegate ItemAdded;
public event Action<TModel> ItemAdded;
/// <summary>
/// Fired when a <see cref="TModel"/> is removed from the database.
@ -67,56 +65,12 @@ namespace osu.Game.Database
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private ArchiveImportIPCChannel ipc;
private readonly List<Action> queuedEvents = new List<Action>();
/// <summary>
/// Allows delaying of outwards events until an operation is confirmed (at a database level).
/// </summary>
private bool delayingEvents;
/// <summary>
/// Begin delaying outwards events.
/// </summary>
private void delayEvents() => delayingEvents = true;
/// <summary>
/// Flush delayed events and disable delaying.
/// </summary>
/// <param name="perform">Whether the flushed events should be performed.</param>
private void flushEvents(bool perform)
{
Action[] events;
lock (queuedEvents)
{
events = queuedEvents.ToArray();
queuedEvents.Clear();
}
if (perform)
{
foreach (var a in events)
a.Invoke();
}
delayingEvents = false;
}
private void handleEvent(Action a)
{
if (delayingEvents)
lock (queuedEvents)
queuedEvents.Add(a);
else
a.Invoke();
}
protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore, IIpcHost importHost = null)
{
ContextFactory = contextFactory;
ModelStore = modelStore;
ModelStore.ItemAdded += item => handleEvent(() => ItemAdded?.Invoke(item, false));
ModelStore.ItemAdded += item => handleEvent(() => ItemAdded?.Invoke(item));
ModelStore.ItemRemoved += s => handleEvent(() => ItemRemoved?.Invoke(s));
Files = new FileStore(contextFactory, storage);
@ -345,8 +299,6 @@ namespace osu.Game.Database
{
Undelete(existing);
LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
handleEvent(() => ItemAdded?.Invoke(existing, true));
// existing item will be used; rollback new import and exit early.
rollback();
flushEvents(true);
@ -632,6 +584,54 @@ namespace osu.Game.Database
throw new InvalidFormatException($"{path} is not a valid archive");
}
#region Event handling / delaying
private readonly List<Action> queuedEvents = new List<Action>();
/// <summary>
/// Allows delaying of outwards events until an operation is confirmed (at a database level).
/// </summary>
private bool delayingEvents;
/// <summary>
/// Begin delaying outwards events.
/// </summary>
private void delayEvents() => delayingEvents = true;
/// <summary>
/// Flush delayed events and disable delaying.
/// </summary>
/// <param name="perform">Whether the flushed events should be performed.</param>
private void flushEvents(bool perform)
{
Action[] events;
lock (queuedEvents)
{
events = queuedEvents.ToArray();
queuedEvents.Clear();
}
if (perform)
{
foreach (var a in events)
a.Invoke();
}
delayingEvents = false;
}
private void handleEvent(Action a)
{
if (delayingEvents)
lock (queuedEvents)
queuedEvents.Add(a);
else
a.Invoke();
}
#endregion
}
public abstract class ArchiveModelManager

View File

@ -0,0 +1,135 @@
// 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 Humanizer;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Online.API;
using osu.Game.Overlays.Notifications;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace osu.Game.Database
{
/// <summary>
/// An <see cref="ArchiveModelManager{TModel, TFileModel}"/> that has the ability to download models using an <see cref="IAPIProvider"/> and
/// import them into the store.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class DownloadableArchiveModelManager<TModel, TFileModel> : ArchiveModelManager<TModel, TFileModel>, IModelDownloader<TModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : INamedFileInfo, new()
{
public event Action<ArchiveDownloadRequest<TModel>> DownloadBegan;
public event Action<ArchiveDownloadRequest<TModel>> DownloadFailed;
private readonly IAPIProvider api;
private readonly List<ArchiveDownloadRequest<TModel>> currentDownloads = new List<ArchiveDownloadRequest<TModel>>();
private readonly MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore;
protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore, IIpcHost importHost = null)
: base(storage, contextFactory, modelStore, importHost)
{
this.api = api;
this.modelStore = modelStore;
}
/// <summary>
/// Creates the download request for this <see cref="TModel"/>.
/// </summary>
/// <param name="model">The <see cref="TModel"/> to be downloaded.</param>
/// <param name="minimiseDownloadSize">Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle.</param>
/// <returns>The request object.</returns>
protected abstract ArchiveDownloadRequest<TModel> CreateDownloadRequest(TModel model, bool minimiseDownloadSize);
/// <summary>
/// Begin a download for the requested <see cref="TModel"/>.
/// </summary>
/// <param name="model">The <see cref="TModel"/> to be downloaded.</param>
/// <param name="minimiseDownloadSize">Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle.</param>
/// <returns>Whether the download was started.</returns>
public bool Download(TModel model, bool minimiseDownloadSize = false)
{
if (!canDownload(model)) return false;
var request = CreateDownloadRequest(model, minimiseDownloadSize);
DownloadNotification notification = new DownloadNotification
{
Text = $"Downloading {request.Model}",
};
request.DownloadProgressed += progress =>
{
notification.State = ProgressNotificationState.Active;
notification.Progress = progress;
};
request.Success += filename =>
{
Task.Factory.StartNew(async () =>
{
// This gets scheduled back to the update thread, but we want the import to run in the background.
await Import(notification, filename);
currentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning);
};
request.Failure += error =>
{
DownloadFailed?.Invoke(request);
if (error is OperationCanceledException) return;
notification.State = ProgressNotificationState.Cancelled;
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
currentDownloads.Remove(request);
};
notification.CancelRequested += () =>
{
request.Cancel();
currentDownloads.Remove(request);
notification.State = ProgressNotificationState.Cancelled;
return true;
};
currentDownloads.Add(request);
PostNotification?.Invoke(notification);
Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning);
DownloadBegan?.Invoke(request);
return true;
}
public virtual bool IsAvailableLocally(TModel model) => modelStore.ConsumableItems.Any(m => m.Equals(model) && !m.DeletePending);
public ArchiveDownloadRequest<TModel> GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model));
private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null;
private class DownloadNotification : ProgressNotification
{
public override bool IsImportant => false;
protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification
{
Activated = CompletionClickAction,
Text = CompletionText
};
private class SilencedProgressCompletionNotification : ProgressCompletionNotification
{
public override bool IsImportant => false;
}
}
}
}

View File

@ -0,0 +1,48 @@
// 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 osu.Game.Online.API;
using System;
namespace osu.Game.Database
{
/// <summary>
/// Represents a <see cref="IModelManager{TModel}"/> that can download new models from an external source.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
public interface IModelDownloader<TModel> : IModelManager<TModel>
where TModel : class
{
/// <summary>
/// Fired when a <see cref="TModel"/> download begins.
/// </summary>
event Action<ArchiveDownloadRequest<TModel>> DownloadBegan;
/// <summary>
/// Fired when a <see cref="TModel"/> download is interrupted, either due to user cancellation or failure.
/// </summary>
event Action<ArchiveDownloadRequest<TModel>> DownloadFailed;
/// <summary>
/// Checks whether a given <see cref="TModel"/> is already available in the local store.
/// </summary>
/// <param name="model">The <see cref="TModel"/> whose existence needs to be checked.</param>
/// <returns>Whether the <see cref="TModel"/> exists.</returns>
bool IsAvailableLocally(TModel model);
/// <summary>
/// Begin a download for the requested <see cref="TModel"/>.
/// </summary>
/// <param name="model">The <see cref="TModel"/> to be downloaded.</param>
/// <param name="minimiseDownloadSize">Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle..</param>
/// <returns>Whether the download was started.</returns>
bool Download(TModel model, bool minimiseDownloadSize);
/// <summary>
/// Gets an existing <see cref="TModel"/> download request if it exists.
/// </summary>
/// <param name="model">The <see cref="TModel"/> whose request is wanted.</param>
/// <returns>The <see cref="ArchiveDownloadRequest{TModel}"/> object if it exists, otherwise null.</returns>
ArchiveDownloadRequest<TModel> GetExistingDownload(TModel model);
}
}

View File

@ -0,0 +1,19 @@
// 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;
namespace osu.Game.Database
{
/// <summary>
/// Represents a model manager that publishes events when <see cref="TModel"/>s are added or removed.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
public interface IModelManager<out TModel>
where TModel : class
{
event Action<TModel> ItemAdded;
event Action<TModel> ItemRemoved;
}
}

View File

@ -0,0 +1,24 @@
// 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;
namespace osu.Game.Online.API
{
public abstract class ArchiveDownloadRequest<TModel> : APIDownloadRequest
where TModel : class
{
public readonly TModel Model;
public float Progress;
public event Action<float> DownloadProgressed;
protected ArchiveDownloadRequest(TModel model)
{
Model = model;
Progressed += (current, total) => DownloadProgressed?.Invoke(Progress = (float)current / total);
}
}
}

View File

@ -2,28 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using System;
namespace osu.Game.Online.API.Requests
{
public class DownloadBeatmapSetRequest : APIDownloadRequest
public class DownloadBeatmapSetRequest : ArchiveDownloadRequest<BeatmapSetInfo>
{
public readonly BeatmapSetInfo BeatmapSet;
public float Progress;
public event Action<float> DownloadProgressed;
private readonly bool noVideo;
public DownloadBeatmapSetRequest(BeatmapSetInfo set, bool noVideo)
: base(set)
{
this.noVideo = noVideo;
BeatmapSet = set;
Progressed += (current, total) => DownloadProgressed?.Invoke(Progress = (float)current / total);
}
protected override string Target => $@"beatmapsets/{BeatmapSet.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}";
protected override string Target => $@"beatmapsets/{Model.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}";
}
}

View File

@ -183,7 +183,7 @@ namespace osu.Game
}
BeatmapManager.ItemRemoved += i => ScoreManager.Delete(getBeatmapScores(i), true);
BeatmapManager.ItemAdded += (i, existing) => ScoreManager.Undelete(getBeatmapScores(i), true);
BeatmapManager.ItemAdded += i => ScoreManager.Undelete(getBeatmapScores(i), true);
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));

View File

@ -7,7 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API;
namespace osu.Game.Overlays.Direct
{
@ -47,9 +47,9 @@ namespace osu.Game.Overlays.Direct
attachDownload(beatmaps.GetExistingDownload(setInfo.NewValue));
}, true);
beatmaps.BeatmapDownloadBegan += download =>
beatmaps.DownloadBegan += download =>
{
if (download.BeatmapSet.OnlineBeatmapSetID == BeatmapSet.Value?.OnlineBeatmapSetID)
if (download.Model.OnlineBeatmapSetID == BeatmapSet.Value?.OnlineBeatmapSetID)
attachDownload(download);
};
@ -65,7 +65,7 @@ namespace osu.Game.Overlays.Direct
if (beatmaps != null)
{
beatmaps.BeatmapDownloadBegan -= attachDownload;
beatmaps.DownloadBegan -= attachDownload;
beatmaps.ItemAdded -= setAdded;
}
@ -76,9 +76,9 @@ namespace osu.Game.Overlays.Direct
#endregion
private DownloadBeatmapSetRequest attachedRequest;
private ArchiveDownloadRequest<BeatmapSetInfo> attachedRequest;
private void attachDownload(DownloadBeatmapSetRequest request)
private void attachDownload(ArchiveDownloadRequest<BeatmapSetInfo> request)
{
if (attachedRequest != null)
{
@ -118,7 +118,7 @@ namespace osu.Game.Overlays.Direct
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
private void setAdded(BeatmapSetInfo s, bool existing) => setDownloadStateFromManager(s, DownloadState.LocallyAvailable);
private void setAdded(BeatmapSetInfo s) => setDownloadStateFromManager(s, DownloadState.LocallyAvailable);
private void setRemoved(BeatmapSetInfo s) => setDownloadStateFromManager(s, DownloadState.NotDownloaded);

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps, IBindable<WorkingBeatmap> beatmap)
{
beatmaps.GetAllUsableBeatmapSets().ForEach(b => addBeatmapSet(b, false));
beatmaps.GetAllUsableBeatmapSets().ForEach(addBeatmapSet);
beatmaps.ItemAdded += addBeatmapSet;
beatmaps.ItemRemoved += removeBeatmapSet;
@ -83,11 +83,8 @@ namespace osu.Game.Overlays.Music
beatmapBacking.ValueChanged += _ => updateSelectedSet();
}
private void addBeatmapSet(BeatmapSetInfo obj, bool existing) => Schedule(() =>
private void addBeatmapSet(BeatmapSetInfo obj) => Schedule(() =>
{
if (existing)
return;
var newItem = new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) };
items.Add(newItem);

View File

@ -218,15 +218,9 @@ namespace osu.Game.Overlays
beatmapSets.Insert(index, beatmapSetInfo);
}
private void handleBeatmapAdded(BeatmapSetInfo obj, bool existing)
{
if (existing)
return;
private void handleBeatmapAdded(BeatmapSetInfo set) => Schedule(() => beatmapSets.Add(set));
Schedule(() => beatmapSets.Add(obj));
}
private void handleBeatmapRemoved(BeatmapSetInfo obj) => Schedule(() => beatmapSets.RemoveAll(s => s.ID == obj.ID));
private void handleBeatmapRemoved(BeatmapSetInfo set) => Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID));
protected override void LoadComplete()
{

View File

@ -82,13 +82,7 @@ namespace osu.Game.Overlays.Settings.Sections
private void itemRemoved(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != s.ID).ToArray());
private void itemAdded(SkinInfo s, bool existing)
{
if (existing)
return;
Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(s).ToArray());
}
private void itemAdded(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(s).ToArray());
protected override void Dispose(bool isDisposing)
{

View File

@ -54,11 +54,8 @@ namespace osu.Game.Screens.Multi.Match.Components
hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID) != null;
}
private void beatmapAdded(BeatmapSetInfo model, bool existing)
private void beatmapAdded(BeatmapSetInfo model)
{
if (Beatmap.Value == null)
return;
if (model.Beatmaps.Any(b => b.OnlineBeatmapID == Beatmap.Value.OnlineBeatmapID))
Schedule(() => hasBeatmap = true);
}

View File

@ -203,7 +203,7 @@ namespace osu.Game.Screens.Multi.Match
/// <summary>
/// Handle the case where a beatmap is imported (and can be used by this match).
/// </summary>
private void beatmapAdded(BeatmapSetInfo model, bool existing) => Schedule(() =>
private void beatmapAdded(BeatmapSetInfo model) => Schedule(() =>
{
if (Beatmap.Value != beatmapManager.DefaultBeatmap)
return;

View File

@ -148,40 +148,37 @@ namespace osu.Game.Screens.Select
});
}
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet)
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{
Schedule(() =>
int? previouslySelectedID = null;
CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID);
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
if (existingSet?.State?.Value == CarouselItemState.Selected)
previouslySelectedID = selectedBeatmap?.Beatmap.ID;
var newSet = createCarouselSet(beatmapSet);
if (existingSet != null)
root.RemoveChild(existingSet);
if (newSet == null)
{
int? previouslySelectedID = null;
CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID);
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
if (existingSet?.State?.Value == CarouselItemState.Selected)
previouslySelectedID = selectedBeatmap?.Beatmap.ID;
var newSet = createCarouselSet(beatmapSet);
if (existingSet != null)
root.RemoveChild(existingSet);
if (newSet == null)
{
itemsCache.Invalidate();
return;
}
root.AddChild(newSet);
applyActiveCriteria(false, false);
//check if we can/need to maintain our current selection.
if (previouslySelectedID != null)
select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet);
itemsCache.Invalidate();
Schedule(() => BeatmapSetsChanged?.Invoke());
});
}
return;
}
root.AddChild(newSet);
applyActiveCriteria(false, false);
//check if we can/need to maintain our current selection.
if (previouslySelectedID != null)
select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet);
itemsCache.Invalidate();
Schedule(() => BeatmapSetsChanged?.Invoke());
});
/// <summary>
/// Selects a given beatmap on the carousel.

View File

@ -578,7 +578,7 @@ namespace osu.Game.Screens.Select
}
}
private void onBeatmapSetAdded(BeatmapSetInfo s, bool existing) => Carousel.UpdateBeatmapSet(s);
private void onBeatmapSetAdded(BeatmapSetInfo s) => Carousel.UpdateBeatmapSet(s);
private void onBeatmapSetRemoved(BeatmapSetInfo s) => Carousel.RemoveBeatmapSet(s);
private void onBeatmapRestored(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID));
private void onBeatmapHidden(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID));