// Copyright (c) ppy Pty Ltd . 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.IO; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Stores; #nullable enable namespace osu.Game.Beatmaps { /// /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] public class BeatmapManager : IModelManager, IModelFileManager, IModelImporter, IWorkingBeatmapCache, IDisposable { public ITrackStore BeatmapTrackStore { get; } private readonly BeatmapModelManager beatmapModelManager; private readonly WorkingBeatmapCache workingBeatmapCache; private readonly BeatmapOnlineLookupQueue? onlineBeatmapLookupQueue; private readonly RealmContextFactory contextFactory; public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false) { this.contextFactory = contextFactory; if (performOnlineLookups) onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); var userResources = new RealmFileStore(contextFactory, storage).Store; BeatmapTrackStore = audioManager.GetTrackStore(userResources); beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, onlineBeatmapLookupQueue); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); workingBeatmapCache.BeatmapManager = beatmapModelManager; beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; } protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap? defaultBeatmap, GameHost? host) { return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host); } protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) => new BeatmapModelManager(contextFactory, storage, onlineLookupQueue); /// /// Create a new . /// public WorkingBeatmap CreateNew(RulesetInfo ruleset, APIUser user) { var metadata = new BeatmapMetadata { Author = new RealmUser { OnlineID = user.OnlineID, Username = user.Username, } }; var set = new BeatmapSetInfo { Beatmaps = { new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(), Ruleset = ruleset, Metadata = metadata, WidescreenStoryboard = true, SamplesMatchPlaybackRate = true, } } }; var imported = beatmapModelManager.Import(set).GetResultSafely()?.Value; return GetWorkingBeatmap(imported?.Beatmaps.First()); } /// /// Fired when a single difficulty has been hidden. /// public event Action? BeatmapHidden; /// /// Fired when a single difficulty has been restored. /// public event Action? BeatmapRestored; /// /// Delete a beatmap difficulty. /// /// The beatmap difficulty to hide. public void Hide(BeatmapInfo beatmapInfo) { using (var realm = contextFactory.CreateContext()) using (var transaction = realm.BeginWrite()) { beatmapInfo.Hidden = true; transaction.Commit(); BeatmapHidden?.Invoke(beatmapInfo); } } /// /// Restore a beatmap difficulty. /// /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmapInfo) { using (var realm = contextFactory.CreateContext()) using (var transaction = realm.BeginWrite()) { beatmapInfo.Hidden = false; transaction.Commit(); BeatmapRestored?.Invoke(beatmapInfo); } } /// /// Returns a list of all usable s. /// /// A list of available . public List GetAllUsableBeatmapSets() { using (var context = contextFactory.CreateContext()) return context.All().Where(b => !b.DeletePending).Detach(); } /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. public List> QueryBeatmapSets(Expression> query) { using (var context = contextFactory.CreateContext()) { return context.All() .Where(b => !b.DeletePending) .Where(query) .ToLive(contextFactory); } } /// /// Perform a lookup query on available s. /// /// The query. /// The first result for the provided query, or null if no results were found. public ILive? QueryBeatmapSet(Expression> query) { using (var context = contextFactory.CreateContext()) return context.All().FirstOrDefault(query)?.ToLive(contextFactory); } /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. public IQueryable QueryBeatmaps(Expression> query) { using (var context = contextFactory.CreateContext()) return context.All().Where(query); } #region Delegation to BeatmapModelManager (methods which previously existed locally). /// /// Perform a lookup query on available s. /// /// The query. /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query)?.Detach(); // TODO: move detach to usages? /// /// Saves an file against a given . /// /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin? beatmapSkin = null) => beatmapModelManager.Save(info, beatmapContent, beatmapSkin); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. /// public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap; /// /// Fired when a notification should be presented to the user. /// public Action PostNotification { set => beatmapModelManager.PostNotification = value; } #endregion #region Implementation of IModelManager public bool IsAvailableLocally(BeatmapSetInfo model) { return beatmapModelManager.IsAvailableLocally(model); } public void Update(BeatmapSetInfo item) { beatmapModelManager.Update(item); } public bool Delete(BeatmapSetInfo item) { return beatmapModelManager.Delete(item); } public void Delete(List items, bool silent = false) { beatmapModelManager.Delete(items, silent); } public void Undelete(List items, bool silent = false) { beatmapModelManager.Undelete(items, silent); } public void Undelete(BeatmapSetInfo item) { beatmapModelManager.Undelete(item); } #endregion #region Implementation of ICanAcceptFiles public Task Import(params string[] paths) { return beatmapModelManager.Import(paths); } public Task Import(params ImportTask[] tasks) { return beatmapModelManager.Import(tasks); } public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { return beatmapModelManager.Import(notification, tasks); } public Task?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(task, lowPriority, cancellationToken); } public Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(archive, lowPriority, cancellationToken); } public Task?> Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); } public IEnumerable HandledExtensions => beatmapModelManager.HandledExtensions; #endregion #region Implementation of IWorkingBeatmapCache public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); public WorkingBeatmap GetWorkingBeatmap(ILive? importedBeatmap) { WorkingBeatmap working = workingBeatmapCache.GetWorkingBeatmap(null); importedBeatmap?.PerformRead(b => working = workingBeatmapCache.GetWorkingBeatmap(b)); return working; } void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo); void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo); #endregion #region Implementation of IModelFileManager public void ReplaceFile(BeatmapSetInfo model, RealmNamedFileUsage file, Stream contents) { beatmapModelManager.ReplaceFile(model, file, contents); } public void DeleteFile(BeatmapSetInfo model, RealmNamedFileUsage file) { beatmapModelManager.DeleteFile(model, file); } public void AddFile(BeatmapSetInfo model, Stream contents, string filename) { beatmapModelManager.AddFile(model, contents, filename); } #endregion #region Implementation of IDisposable public void Dispose() { onlineBeatmapLookupQueue?.Dispose(); } #endregion #region Implementation of IPostImports public Action>>? PostImport { set => beatmapModelManager.PostImport = value; } #endregion } }