2019-01-24 08:43:03 +00:00
// 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.
2018-04-13 09:19:50 +00:00
2021-09-30 07:45:32 +00:00
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 ;
2021-11-09 08:27:07 +00:00
using osu.Framework.Audio.Track ;
2022-01-03 08:31:12 +00:00
using osu.Framework.Extensions ;
2021-09-30 07:45:32 +00:00
using osu.Framework.IO.Stores ;
using osu.Framework.Platform ;
2020-10-16 05:39:02 +00:00
using osu.Framework.Testing ;
2021-09-30 07:45:32 +00:00
using osu.Game.Database ;
using osu.Game.IO.Archives ;
2021-11-22 06:52:55 +00:00
using osu.Game.Models ;
2021-09-30 07:45:32 +00:00
using osu.Game.Online.API ;
2021-11-04 09:02:44 +00:00
using osu.Game.Online.API.Requests.Responses ;
2021-09-30 07:45:32 +00:00
using osu.Game.Overlays.Notifications ;
using osu.Game.Rulesets ;
using osu.Game.Skinning ;
2021-12-01 07:10:50 +00:00
using osu.Game.Stores ;
2018-04-13 09:19:50 +00:00
2021-12-15 05:26:38 +00:00
#nullable enable
2018-04-13 09:19:50 +00:00
namespace osu.Game.Beatmaps
{
/// <summary>
2021-09-30 06:43:49 +00:00
/// Handles general operations related to global beatmap management.
2018-04-13 09:19:50 +00:00
/// </summary>
2020-10-16 05:39:02 +00:00
[ExcludeFromDynamicCompile]
2021-11-24 03:16:08 +00:00
public class BeatmapManager : IModelManager < BeatmapSetInfo > , IModelFileManager < BeatmapSetInfo , RealmNamedFileUsage > , IModelImporter < BeatmapSetInfo > , IWorkingBeatmapCache , IDisposable
2018-04-13 09:19:50 +00:00
{
2021-11-09 08:27:07 +00:00
public ITrackStore BeatmapTrackStore { get ; }
2021-09-30 07:45:32 +00:00
private readonly BeatmapModelManager beatmapModelManager ;
2021-09-30 09:21:16 +00:00
2021-09-30 07:45:32 +00:00
private readonly WorkingBeatmapCache workingBeatmapCache ;
2021-12-15 05:26:38 +00:00
private readonly BeatmapOnlineLookupQueue ? onlineBeatmapLookupQueue ;
private readonly RealmContextFactory contextFactory ;
2021-09-30 07:45:32 +00:00
2022-01-11 12:36:34 +00:00
public BeatmapManager ( Storage storage , RealmContextFactory contextFactory , RulesetStore rulesets , IAPIProvider ? api , AudioManager audioManager , IResourceStore < byte [ ] > gameResources , GameHost ? host = null , WorkingBeatmap ? defaultBeatmap = null , bool performOnlineLookups = false )
2018-04-13 09:19:50 +00:00
{
2021-12-15 05:26:38 +00:00
this . contextFactory = contextFactory ;
2022-01-11 12:36:34 +00:00
2021-12-14 10:47:11 +00:00
if ( performOnlineLookups )
2022-01-11 12:36:34 +00:00
{
if ( api = = null )
throw new ArgumentNullException ( nameof ( api ) , "API must be provided if online lookups are required." ) ;
2021-12-14 10:47:11 +00:00
onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue ( api , storage ) ;
2022-01-11 12:36:34 +00:00
}
2021-12-14 10:47:11 +00:00
2021-12-01 07:10:50 +00:00
var userResources = new RealmFileStore ( contextFactory , storage ) . Store ;
2021-11-09 08:27:07 +00:00
BeatmapTrackStore = audioManager . GetTrackStore ( userResources ) ;
2021-12-14 10:47:11 +00:00
beatmapModelManager = CreateBeatmapModelManager ( storage , contextFactory , rulesets , onlineBeatmapLookupQueue ) ;
2021-11-09 08:27:07 +00:00
workingBeatmapCache = CreateWorkingBeatmapCache ( audioManager , gameResources , userResources , defaultBeatmap , host ) ;
2021-09-30 07:45:32 +00:00
2021-10-06 03:05:30 +00:00
beatmapModelManager . WorkingBeatmapCache = workingBeatmapCache ;
2018-07-19 04:41:09 +00:00
}
2021-12-15 05:26:38 +00:00
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache ( AudioManager audioManager , IResourceStore < byte [ ] > resources , IResourceStore < byte [ ] > storage , WorkingBeatmap ? defaultBeatmap , GameHost ? host )
2021-11-09 08:27:07 +00:00
{
return new WorkingBeatmapCache ( BeatmapTrackStore , audioManager , resources , storage , defaultBeatmap , host ) ;
}
2021-09-30 07:45:32 +00:00
2021-12-15 05:26:38 +00:00
protected virtual BeatmapModelManager CreateBeatmapModelManager ( Storage storage , RealmContextFactory contextFactory , RulesetStore rulesets , BeatmapOnlineLookupQueue ? onlineLookupQueue ) = >
2021-12-14 10:47:11 +00:00
new BeatmapModelManager ( contextFactory , storage , onlineLookupQueue ) ;
2021-09-30 07:45:32 +00:00
/// <summary>
/// Create a new <see cref="WorkingBeatmap"/>.
/// </summary>
2021-11-04 09:02:44 +00:00
public WorkingBeatmap CreateNew ( RulesetInfo ruleset , APIUser user )
2021-09-30 07:45:32 +00:00
{
var metadata = new BeatmapMetadata
{
2021-11-22 06:52:55 +00:00
Author = new RealmUser
{
OnlineID = user . OnlineID ,
Username = user . Username ,
}
2021-09-30 07:45:32 +00:00
} ;
2022-01-11 09:17:13 +00:00
var beatmapSet = new BeatmapSetInfo
2021-09-30 07:45:32 +00:00
{
2021-11-24 04:25:52 +00:00
Beatmaps =
2021-09-30 07:45:32 +00:00
{
new BeatmapInfo
{
2022-01-18 13:57:39 +00:00
Difficulty = new BeatmapDifficulty ( ) ,
2021-09-30 07:45:32 +00:00
Ruleset = ruleset ,
Metadata = metadata ,
WidescreenStoryboard = true ,
SamplesMatchPlaybackRate = true ,
}
}
} ;
2022-01-11 09:17:13 +00:00
foreach ( BeatmapInfo b in beatmapSet . Beatmaps )
b . BeatmapSet = beatmapSet ;
var imported = beatmapModelManager . Import ( beatmapSet ) . GetResultSafely ( ) ;
2021-09-30 10:33:12 +00:00
2022-01-11 07:30:55 +00:00
if ( imported = = null )
throw new InvalidOperationException ( "Failed to import new beatmap" ) ;
return imported . PerformRead ( s = > GetWorkingBeatmap ( s . Beatmaps . First ( ) ) ) ;
2021-09-30 07:45:32 +00:00
}
2021-12-15 05:26:38 +00:00
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>
/// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
public void Hide ( BeatmapInfo beatmapInfo )
2021-11-05 09:05:31 +00:00
{
2021-12-15 05:26:38 +00:00
using ( var realm = contextFactory . CreateContext ( ) )
using ( var transaction = realm . BeginWrite ( ) )
{
2022-01-11 13:55:00 +00:00
if ( ! beatmapInfo . IsManaged )
beatmapInfo = realm . Find < BeatmapInfo > ( beatmapInfo . ID ) ;
2021-12-15 05:26:38 +00:00
beatmapInfo . Hidden = true ;
transaction . Commit ( ) ;
}
2021-11-05 09:05:31 +00:00
}
2021-09-30 07:45:32 +00:00
/// <summary>
2021-12-15 05:26:38 +00:00
/// Restore a beatmap difficulty.
2021-09-30 07:45:32 +00:00
/// </summary>
2021-12-15 05:26:38 +00:00
/// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
public void Restore ( BeatmapInfo beatmapInfo )
2021-11-05 09:05:31 +00:00
{
2021-12-15 05:26:38 +00:00
using ( var realm = contextFactory . CreateContext ( ) )
using ( var transaction = realm . BeginWrite ( ) )
{
2022-01-11 13:55:00 +00:00
if ( ! beatmapInfo . IsManaged )
beatmapInfo = realm . Find < BeatmapInfo > ( beatmapInfo . ID ) ;
2021-12-15 05:26:38 +00:00
beatmapInfo . Hidden = false ;
transaction . Commit ( ) ;
}
2021-11-05 09:05:31 +00:00
}
2021-09-30 07:45:32 +00:00
2022-01-11 14:04:36 +00:00
public void RestoreAll ( )
{
using ( var realm = contextFactory . CreateContext ( ) )
using ( var transaction = realm . BeginWrite ( ) )
{
foreach ( var beatmap in realm . All < BeatmapInfo > ( ) . Where ( b = > b . Hidden ) )
beatmap . Hidden = false ;
transaction . Commit ( ) ;
}
}
2021-09-30 07:45:32 +00:00
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
2021-12-16 11:11:45 +00:00
public List < BeatmapSetInfo > GetAllUsableBeatmapSets ( )
2021-12-15 06:24:26 +00:00
{
using ( var context = contextFactory . CreateContext ( ) )
2022-01-07 05:17:22 +00:00
return context . All < BeatmapSetInfo > ( ) . Where ( b = > ! b . DeletePending ) . Detach ( ) ;
2021-12-15 06:24:26 +00:00
}
2021-09-30 07:45:32 +00:00
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
2021-12-15 06:24:26 +00:00
public ILive < BeatmapSetInfo > ? QueryBeatmapSet ( Expression < Func < BeatmapSetInfo , bool > > query )
{
using ( var context = contextFactory . CreateContext ( ) )
2021-12-17 09:45:22 +00:00
return context . All < BeatmapSetInfo > ( ) . FirstOrDefault ( query ) ? . ToLive ( contextFactory ) ;
2021-12-15 06:24:26 +00:00
}
2021-09-30 07:45:32 +00:00
2021-12-16 11:11:45 +00:00
#region Delegation to BeatmapModelManager ( methods which previously existed locally ) .
2021-09-30 07:45:32 +00:00
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
2022-01-07 05:17:22 +00:00
public BeatmapInfo ? QueryBeatmap ( Expression < Func < BeatmapInfo , bool > > query ) = > beatmapModelManager . QueryBeatmap ( query ) ? . Detach ( ) ;
2021-09-30 07:45:32 +00:00
2021-12-15 06:24:26 +00:00
/// <summary>
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
/// </summary>
/// <param name="info">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
public virtual void Save ( BeatmapInfo info , IBeatmap beatmapContent , ISkin ? beatmapSkin = null ) = >
beatmapModelManager . Save ( info , beatmapContent , beatmapSkin ) ;
2021-09-30 07:45:32 +00:00
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
2021-11-15 09:46:11 +00:00
public IWorkingBeatmap DefaultBeatmap = > workingBeatmapCache . DefaultBeatmap ;
2021-09-30 07:45:32 +00:00
/// <summary>
/// Fired when a notification should be presented to the user.
/// </summary>
2021-09-30 09:21:16 +00:00
public Action < Notification > PostNotification
{
2021-11-25 08:23:46 +00:00
set = > beatmapModelManager . PostNotification = value ;
2021-09-30 09:21:16 +00:00
}
2021-09-30 07:45:32 +00:00
#endregion
#region Implementation of IModelManager < BeatmapSetInfo >
2021-09-30 09:21:16 +00:00
public bool IsAvailableLocally ( BeatmapSetInfo model )
{
return beatmapModelManager . IsAvailableLocally ( model ) ;
}
2021-09-30 07:45:32 +00:00
public bool Delete ( BeatmapSetInfo item )
{
return beatmapModelManager . Delete ( item ) ;
}
public void Delete ( List < BeatmapSetInfo > items , bool silent = false )
{
beatmapModelManager . Delete ( items , silent ) ;
}
2022-01-11 13:57:47 +00:00
public void Delete ( Expression < Func < BeatmapSetInfo , bool > > ? filter = null , bool silent = false )
{
using ( var context = contextFactory . CreateContext ( ) )
{
2022-01-11 16:18:11 +00:00
var items = context . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending & & ! s . Protected ) ;
2022-01-11 13:57:47 +00:00
if ( filter ! = null )
items = items . Where ( filter ) ;
beatmapModelManager . Delete ( items . ToList ( ) , silent ) ;
}
}
2022-01-11 14:04:36 +00:00
public void UndeleteAll ( )
{
using ( var context = contextFactory . CreateContext ( ) )
beatmapModelManager . Undelete ( context . All < BeatmapSetInfo > ( ) . Where ( s = > s . DeletePending ) . ToList ( ) ) ;
}
2021-09-30 07:45:32 +00:00
public void Undelete ( List < BeatmapSetInfo > 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 ) ;
}
2021-09-30 10:33:12 +00:00
public Task < IEnumerable < ILive < BeatmapSetInfo > > > Import ( ProgressNotification notification , params ImportTask [ ] tasks )
2021-09-30 07:45:32 +00:00
{
return beatmapModelManager . Import ( notification , tasks ) ;
}
2021-12-15 05:26:38 +00:00
public Task < ILive < BeatmapSetInfo > ? > Import ( ImportTask task , bool lowPriority = false , CancellationToken cancellationToken = default )
2021-09-30 07:45:32 +00:00
{
return beatmapModelManager . Import ( task , lowPriority , cancellationToken ) ;
}
2021-12-15 05:26:38 +00:00
public Task < ILive < BeatmapSetInfo > ? > Import ( ArchiveReader archive , bool lowPriority = false , CancellationToken cancellationToken = default )
2021-09-30 07:45:32 +00:00
{
return beatmapModelManager . Import ( archive , lowPriority , cancellationToken ) ;
}
2021-12-15 05:26:38 +00:00
public Task < ILive < BeatmapSetInfo > ? > Import ( BeatmapSetInfo item , ArchiveReader ? archive = null , bool lowPriority = false , CancellationToken cancellationToken = default )
2021-09-30 07:45:32 +00:00
{
return beatmapModelManager . Import ( item , archive , lowPriority , cancellationToken ) ;
}
public IEnumerable < string > HandledExtensions = > beatmapModelManager . HandledExtensions ;
#endregion
#region Implementation of IWorkingBeatmapCache
2022-01-18 15:49:16 +00:00
public WorkingBeatmap GetWorkingBeatmap ( BeatmapInfo ? importedBeatmap )
{
// Detached sets don't come with files.
// If we seem to be missing files, now is a good time to re-fetch.
if ( importedBeatmap ? . BeatmapSet ? . Files . Count = = 0 )
{
using ( var realm = contextFactory . CreateContext ( ) )
{
var refetch = realm . Find < BeatmapInfo > ( importedBeatmap . ID ) ? . Detach ( ) ;
if ( refetch ! = null )
importedBeatmap = refetch ;
}
}
return workingBeatmapCache . GetWorkingBeatmap ( importedBeatmap ) ;
}
2021-09-30 07:45:32 +00:00
2021-12-15 06:24:26 +00:00
public WorkingBeatmap GetWorkingBeatmap ( ILive < BeatmapInfo > ? importedBeatmap )
{
WorkingBeatmap working = workingBeatmapCache . GetWorkingBeatmap ( null ) ;
importedBeatmap ? . PerformRead ( b = > working = workingBeatmapCache . GetWorkingBeatmap ( b ) ) ;
return working ;
}
2021-10-06 03:05:30 +00:00
void IWorkingBeatmapCache . Invalidate ( BeatmapSetInfo beatmapSetInfo ) = > workingBeatmapCache . Invalidate ( beatmapSetInfo ) ;
void IWorkingBeatmapCache . Invalidate ( BeatmapInfo beatmapInfo ) = > workingBeatmapCache . Invalidate ( beatmapInfo ) ;
2021-09-30 07:45:32 +00:00
#endregion
#region Implementation of IModelFileManager < in BeatmapSetInfo , in BeatmapSetFileInfo >
2021-11-24 03:16:08 +00:00
public void ReplaceFile ( BeatmapSetInfo model , RealmNamedFileUsage file , Stream contents )
2021-09-30 07:45:32 +00:00
{
2021-11-29 09:08:02 +00:00
beatmapModelManager . ReplaceFile ( model , file , contents ) ;
2021-09-30 07:45:32 +00:00
}
2021-11-24 03:16:08 +00:00
public void DeleteFile ( BeatmapSetInfo model , RealmNamedFileUsage file )
2021-09-30 07:45:32 +00:00
{
beatmapModelManager . DeleteFile ( model , file ) ;
}
public void AddFile ( BeatmapSetInfo model , Stream contents , string filename )
{
beatmapModelManager . AddFile ( model , contents , filename ) ;
}
#endregion
2021-09-30 16:43:57 +00:00
#region Implementation of IDisposable
public void Dispose ( )
{
2021-10-02 15:55:29 +00:00
onlineBeatmapLookupQueue ? . Dispose ( ) ;
2021-09-30 16:43:57 +00:00
}
#endregion
2021-10-15 07:00:09 +00:00
#region Implementation of IPostImports < out BeatmapSetInfo >
2021-12-15 05:26:38 +00:00
public Action < IEnumerable < ILive < BeatmapSetInfo > > > ? PostImport
2021-10-15 07:00:09 +00:00
{
set = > beatmapModelManager . PostImport = value ;
}
#endregion
2021-09-30 07:45:32 +00:00
}
2018-04-13 09:19:50 +00:00
}