2019-01-24 16:43:03 +08: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 17:19:50 +08:00
2021-09-30 15:45:32 +08: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 16:27:07 +08:00
using osu.Framework.Audio.Track ;
2021-09-30 15:45:32 +08:00
using osu.Framework.IO.Stores ;
using osu.Framework.Platform ;
2020-10-16 13:39:02 +08:00
using osu.Framework.Testing ;
2021-09-30 15:45:32 +08:00
using osu.Game.Database ;
using osu.Game.IO.Archives ;
2021-11-22 14:52:55 +08:00
using osu.Game.Models ;
2021-09-30 15:45:32 +08:00
using osu.Game.Online.API ;
2021-11-04 17:02:44 +08:00
using osu.Game.Online.API.Requests.Responses ;
2021-09-30 15:45:32 +08:00
using osu.Game.Overlays.Notifications ;
using osu.Game.Rulesets ;
using osu.Game.Skinning ;
2021-12-01 15:10:50 +08:00
using osu.Game.Stores ;
2018-04-13 17:19:50 +08:00
2021-12-15 13:26:38 +08:00
#nullable enable
2018-04-13 17:19:50 +08:00
namespace osu.Game.Beatmaps
{
/// <summary>
2021-09-30 14:43:49 +08:00
/// Handles general operations related to global beatmap management.
2018-04-13 17:19:50 +08:00
/// </summary>
2020-10-16 13:39:02 +08:00
[ExcludeFromDynamicCompile]
2021-11-24 11:16:08 +08:00
public class BeatmapManager : IModelManager < BeatmapSetInfo > , IModelFileManager < BeatmapSetInfo , RealmNamedFileUsage > , IModelImporter < BeatmapSetInfo > , IWorkingBeatmapCache , IDisposable
2018-04-13 17:19:50 +08:00
{
2021-11-09 16:27:07 +08:00
public ITrackStore BeatmapTrackStore { get ; }
2021-09-30 15:45:32 +08:00
private readonly BeatmapModelManager beatmapModelManager ;
2021-09-30 17:21:16 +08:00
2021-09-30 15:45:32 +08:00
private readonly WorkingBeatmapCache workingBeatmapCache ;
2021-12-15 13:26:38 +08:00
private readonly BeatmapOnlineLookupQueue ? onlineBeatmapLookupQueue ;
2022-01-24 18:59:58 +08:00
private readonly RealmAccess realm ;
2021-09-30 15:45:32 +08:00
2022-01-24 18:59:58 +08:00
public BeatmapManager ( Storage storage , RealmAccess realm , RulesetStore rulesets , IAPIProvider ? api , AudioManager audioManager , IResourceStore < byte [ ] > gameResources , GameHost ? host = null , WorkingBeatmap ? defaultBeatmap = null , bool performOnlineLookups = false )
2018-04-13 17:19:50 +08:00
{
2022-01-24 18:59:58 +08:00
this . realm = realm ;
2022-01-11 20:36:34 +08:00
2021-12-14 18:47:11 +08:00
if ( performOnlineLookups )
2022-01-11 20:36:34 +08:00
{
if ( api = = null )
throw new ArgumentNullException ( nameof ( api ) , "API must be provided if online lookups are required." ) ;
2021-12-14 18:47:11 +08:00
onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue ( api , storage ) ;
2022-01-11 20:36:34 +08:00
}
2021-12-14 18:47:11 +08:00
2022-01-24 18:59:58 +08:00
var userResources = new RealmFileStore ( realm , storage ) . Store ;
2021-11-09 16:27:07 +08:00
BeatmapTrackStore = audioManager . GetTrackStore ( userResources ) ;
2022-01-24 18:59:58 +08:00
beatmapModelManager = CreateBeatmapModelManager ( storage , realm , rulesets , onlineBeatmapLookupQueue ) ;
2021-11-09 16:27:07 +08:00
workingBeatmapCache = CreateWorkingBeatmapCache ( audioManager , gameResources , userResources , defaultBeatmap , host ) ;
2021-09-30 15:45:32 +08:00
2021-10-06 11:05:30 +08:00
beatmapModelManager . WorkingBeatmapCache = workingBeatmapCache ;
2018-07-19 12:41:09 +08:00
}
2021-12-15 13:26:38 +08:00
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache ( AudioManager audioManager , IResourceStore < byte [ ] > resources , IResourceStore < byte [ ] > storage , WorkingBeatmap ? defaultBeatmap , GameHost ? host )
2021-11-09 16:27:07 +08:00
{
return new WorkingBeatmapCache ( BeatmapTrackStore , audioManager , resources , storage , defaultBeatmap , host ) ;
}
2021-09-30 15:45:32 +08:00
2022-01-24 18:59:58 +08:00
protected virtual BeatmapModelManager CreateBeatmapModelManager ( Storage storage , RealmAccess realm , RulesetStore rulesets , BeatmapOnlineLookupQueue ? onlineLookupQueue ) = >
new BeatmapModelManager ( realm , storage , onlineLookupQueue ) ;
2021-09-30 15:45:32 +08:00
/// <summary>
2022-01-24 00:49:17 +08:00
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,
/// with a single difficulty which is backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned usable <see cref="WorkingBeatmap"/>.
2021-09-30 15:45:32 +08:00
/// </summary>
2021-11-04 17:02:44 +08:00
public WorkingBeatmap CreateNew ( RulesetInfo ruleset , APIUser user )
2021-09-30 15:45:32 +08:00
{
var metadata = new BeatmapMetadata
{
2021-11-22 14:52:55 +08:00
Author = new RealmUser
{
OnlineID = user . OnlineID ,
Username = user . Username ,
}
2021-09-30 15:45:32 +08:00
} ;
2022-01-11 17:17:13 +08:00
var beatmapSet = new BeatmapSetInfo
2021-09-30 15:45:32 +08:00
{
2021-11-24 12:25:52 +08:00
Beatmaps =
2021-09-30 15:45:32 +08:00
{
2022-01-28 04:41:30 +08:00
new BeatmapInfo ( ruleset , new BeatmapDifficulty ( ) , metadata )
2021-09-30 15:45:32 +08:00
}
} ;
2022-01-11 17:17:13 +08:00
foreach ( BeatmapInfo b in beatmapSet . Beatmaps )
b . BeatmapSet = beatmapSet ;
2022-01-25 14:23:51 +08:00
var imported = beatmapModelManager . Import ( beatmapSet ) ;
2021-09-30 18:33:12 +08:00
2022-01-11 15:30:55 +08:00
if ( imported = = null )
throw new InvalidOperationException ( "Failed to import new beatmap" ) ;
return imported . PerformRead ( s = > GetWorkingBeatmap ( s . Beatmaps . First ( ) ) ) ;
2021-09-30 15:45:32 +08:00
}
2022-01-24 00:49:17 +08:00
/// <summary>
/// Add a new difficulty to the beatmap set represented by the provided <see cref="BeatmapSetInfo"/>.
/// The new difficulty will be backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned <see cref="WorkingBeatmap"/>.
/// </summary>
2022-01-24 02:58:21 +08:00
public virtual WorkingBeatmap CreateNewBlankDifficulty ( BeatmapSetInfo beatmapSetInfo , RulesetInfo rulesetInfo )
2022-01-24 00:49:17 +08:00
{
// fetch one of the existing difficulties to copy timing points and metadata from,
// so that the user doesn't have to fill all of that out again.
// this silently assumes that all difficulties have the same timing points and metadata,
// but cases where this isn't true seem rather rare / pathological.
var referenceBeatmap = GetWorkingBeatmap ( beatmapSetInfo . Beatmaps . First ( ) ) ;
2022-02-04 01:14:30 +08:00
var newBeatmapInfo = new BeatmapInfo ( rulesetInfo , new BeatmapDifficulty ( ) , referenceBeatmap . Metadata . DeepClone ( ) ) ;
// populate circular beatmap set info <-> beatmap info references manually.
// several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()`
// rely on them being freely traversable in both directions for correct operation.
beatmapSetInfo . Beatmaps . Add ( newBeatmapInfo ) ;
newBeatmapInfo . BeatmapSet = beatmapSetInfo ;
2022-01-24 00:49:17 +08:00
2022-02-04 01:14:30 +08:00
var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo } ;
2022-01-24 00:49:17 +08:00
foreach ( var timingPoint in referenceBeatmap . Beatmap . ControlPointInfo . TimingPoints )
newBeatmap . ControlPointInfo . Add ( timingPoint . Time , timingPoint . DeepClone ( ) ) ;
2022-02-04 01:14:30 +08:00
beatmapModelManager . Save ( newBeatmapInfo , newBeatmap ) ;
2022-01-24 01:56:19 +08:00
2022-02-03 18:39:24 +08:00
workingBeatmapCache . Invalidate ( beatmapSetInfo ) ;
return GetWorkingBeatmap ( newBeatmap . BeatmapInfo ) ;
2022-01-24 00:49:17 +08:00
}
// TODO: add back support for making a copy of another difficulty
// (likely via a separate `CopyDifficulty()` method).
2021-12-15 13:26:38 +08:00
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>
/// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
public void Hide ( BeatmapInfo beatmapInfo )
2021-11-05 17:05:31 +08:00
{
2022-01-25 12:04:05 +08:00
realm . Run ( r = >
2021-12-15 13:26:38 +08:00
{
2022-01-25 12:04:05 +08:00
using ( var transaction = r . BeginWrite ( ) )
2022-01-21 00:34:20 +08:00
{
if ( ! beatmapInfo . IsManaged )
2022-01-25 12:04:05 +08:00
beatmapInfo = r . Find < BeatmapInfo > ( beatmapInfo . ID ) ;
2022-01-11 21:55:00 +08:00
2022-01-21 00:34:20 +08:00
beatmapInfo . Hidden = true ;
transaction . Commit ( ) ;
}
} ) ;
2021-11-05 17:05:31 +08:00
}
2021-09-30 15:45:32 +08:00
/// <summary>
2021-12-15 13:26:38 +08:00
/// Restore a beatmap difficulty.
2021-09-30 15:45:32 +08:00
/// </summary>
2021-12-15 13:26:38 +08:00
/// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
public void Restore ( BeatmapInfo beatmapInfo )
2021-11-05 17:05:31 +08:00
{
2022-01-25 12:04:05 +08:00
realm . Run ( r = >
2021-12-15 13:26:38 +08:00
{
2022-01-25 12:04:05 +08:00
using ( var transaction = r . BeginWrite ( ) )
2022-01-21 00:34:20 +08:00
{
if ( ! beatmapInfo . IsManaged )
2022-01-25 12:04:05 +08:00
beatmapInfo = r . Find < BeatmapInfo > ( beatmapInfo . ID ) ;
2022-01-11 21:55:00 +08:00
2022-01-21 00:34:20 +08:00
beatmapInfo . Hidden = false ;
transaction . Commit ( ) ;
}
} ) ;
2021-11-05 17:05:31 +08:00
}
2021-09-30 15:45:32 +08:00
2022-01-11 22:04:36 +08:00
public void RestoreAll ( )
{
2022-01-25 12:04:05 +08:00
realm . Run ( r = >
2022-01-11 22:04:36 +08:00
{
2022-01-25 12:04:05 +08:00
using ( var transaction = r . BeginWrite ( ) )
2022-01-21 16:08:20 +08:00
{
2022-01-25 12:04:05 +08:00
foreach ( var beatmap in r . All < BeatmapInfo > ( ) . Where ( b = > b . Hidden ) )
2022-01-21 16:08:20 +08:00
beatmap . Hidden = false ;
2022-01-11 22:04:36 +08:00
2022-01-21 16:08:20 +08:00
transaction . Commit ( ) ;
}
} ) ;
2022-01-11 22:04:36 +08:00
}
2021-09-30 15:45:32 +08: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 19:11:45 +08:00
public List < BeatmapSetInfo > GetAllUsableBeatmapSets ( )
2021-12-15 14:24:26 +08:00
{
2022-01-25 12:04:05 +08:00
return realm . Run ( r = >
2022-01-21 16:33:26 +08:00
{
2022-01-25 12:04:05 +08:00
r . Refresh ( ) ;
return r . All < BeatmapSetInfo > ( ) . Where ( b = > ! b . DeletePending ) . Detach ( ) ;
2022-01-21 16:33:26 +08:00
} ) ;
2021-12-15 14:24:26 +08:00
}
2021-09-30 15:45:32 +08: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>
2022-01-26 12:37:33 +08:00
public Live < BeatmapSetInfo > ? QueryBeatmapSet ( Expression < Func < BeatmapSetInfo , bool > > query )
2021-12-15 14:24:26 +08:00
{
2022-01-24 18:59:58 +08:00
return realm . Run ( r = > r . All < BeatmapSetInfo > ( ) . FirstOrDefault ( query ) ? . ToLive ( realm ) ) ;
2021-12-15 14:24:26 +08:00
}
2021-09-30 15:45:32 +08:00
2021-12-16 19:11:45 +08:00
#region Delegation to BeatmapModelManager ( methods which previously existed locally ) .
2021-09-30 15:45:32 +08: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 13:17:22 +08:00
public BeatmapInfo ? QueryBeatmap ( Expression < Func < BeatmapInfo , bool > > query ) = > beatmapModelManager . QueryBeatmap ( query ) ? . Detach ( ) ;
2021-09-30 15:45:32 +08:00
2021-12-15 14:24:26 +08: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 15:45:32 +08:00
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
2021-11-15 17:46:11 +08:00
public IWorkingBeatmap DefaultBeatmap = > workingBeatmapCache . DefaultBeatmap ;
2021-09-30 15:45:32 +08:00
/// <summary>
/// Fired when a notification should be presented to the user.
/// </summary>
2021-09-30 17:21:16 +08:00
public Action < Notification > PostNotification
{
2021-11-25 16:23:46 +08:00
set = > beatmapModelManager . PostNotification = value ;
2021-09-30 17:21:16 +08:00
}
2021-09-30 15:45:32 +08:00
#endregion
#region Implementation of IModelManager < BeatmapSetInfo >
2021-09-30 17:21:16 +08:00
public bool IsAvailableLocally ( BeatmapSetInfo model )
{
return beatmapModelManager . IsAvailableLocally ( model ) ;
}
2021-09-30 15:45:32 +08: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 21:57:47 +08:00
public void Delete ( Expression < Func < BeatmapSetInfo , bool > > ? filter = null , bool silent = false )
{
2022-01-25 12:04:05 +08:00
realm . Run ( r = >
2022-01-11 21:57:47 +08:00
{
2022-01-25 12:04:05 +08:00
var items = r . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending & & ! s . Protected ) ;
2022-01-11 21:57:47 +08:00
if ( filter ! = null )
items = items . Where ( filter ) ;
beatmapModelManager . Delete ( items . ToList ( ) , silent ) ;
2022-01-21 16:08:20 +08:00
} ) ;
2022-01-11 21:57:47 +08:00
}
2022-01-11 22:04:36 +08:00
public void UndeleteAll ( )
{
2022-01-25 12:04:05 +08:00
realm . Run ( r = > beatmapModelManager . Undelete ( r . All < BeatmapSetInfo > ( ) . Where ( s = > s . DeletePending ) . ToList ( ) ) ) ;
2022-01-11 22:04:36 +08:00
}
2021-09-30 15:45:32 +08: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 ) ;
}
2022-01-26 12:37:33 +08:00
public Task < IEnumerable < Live < BeatmapSetInfo > > > Import ( ProgressNotification notification , params ImportTask [ ] tasks )
2021-09-30 15:45:32 +08:00
{
return beatmapModelManager . Import ( notification , tasks ) ;
}
2022-01-26 12:37:33 +08:00
public Task < Live < BeatmapSetInfo > ? > Import ( ImportTask task , bool lowPriority = false , CancellationToken cancellationToken = default )
2021-09-30 15:45:32 +08:00
{
return beatmapModelManager . Import ( task , lowPriority , cancellationToken ) ;
}
2022-01-26 12:37:33 +08:00
public Task < Live < BeatmapSetInfo > ? > Import ( ArchiveReader archive , bool lowPriority = false , CancellationToken cancellationToken = default )
2021-09-30 15:45:32 +08:00
{
return beatmapModelManager . Import ( archive , lowPriority , cancellationToken ) ;
}
2022-01-26 12:37:33 +08:00
public Live < BeatmapSetInfo > ? Import ( BeatmapSetInfo item , ArchiveReader ? archive = null , bool lowPriority = false , CancellationToken cancellationToken = default )
2021-09-30 15:45:32 +08:00
{
return beatmapModelManager . Import ( item , archive , lowPriority , cancellationToken ) ;
}
public IEnumerable < string > HandledExtensions = > beatmapModelManager . HandledExtensions ;
#endregion
#region Implementation of IWorkingBeatmapCache
2022-01-18 23:49:16 +08: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 )
{
2022-01-25 12:04:05 +08:00
realm . Run ( r = >
2022-01-18 23:49:16 +08:00
{
2022-01-25 12:04:05 +08:00
var refetch = r . Find < BeatmapInfo > ( importedBeatmap . ID ) ? . Detach ( ) ;
2022-01-18 23:49:16 +08:00
if ( refetch ! = null )
importedBeatmap = refetch ;
2022-01-21 00:34:20 +08:00
} ) ;
2022-01-18 23:49:16 +08:00
}
return workingBeatmapCache . GetWorkingBeatmap ( importedBeatmap ) ;
}
2021-09-30 15:45:32 +08:00
2022-01-26 12:37:33 +08:00
public WorkingBeatmap GetWorkingBeatmap ( Live < BeatmapInfo > ? importedBeatmap )
2021-12-15 14:24:26 +08:00
{
WorkingBeatmap working = workingBeatmapCache . GetWorkingBeatmap ( null ) ;
importedBeatmap ? . PerformRead ( b = > working = workingBeatmapCache . GetWorkingBeatmap ( b ) ) ;
return working ;
}
2021-10-06 11:05:30 +08:00
void IWorkingBeatmapCache . Invalidate ( BeatmapSetInfo beatmapSetInfo ) = > workingBeatmapCache . Invalidate ( beatmapSetInfo ) ;
void IWorkingBeatmapCache . Invalidate ( BeatmapInfo beatmapInfo ) = > workingBeatmapCache . Invalidate ( beatmapInfo ) ;
2021-09-30 15:45:32 +08:00
#endregion
#region Implementation of IModelFileManager < in BeatmapSetInfo , in BeatmapSetFileInfo >
2021-11-24 11:16:08 +08:00
public void ReplaceFile ( BeatmapSetInfo model , RealmNamedFileUsage file , Stream contents )
2021-09-30 15:45:32 +08:00
{
2021-11-29 17:08:02 +08:00
beatmapModelManager . ReplaceFile ( model , file , contents ) ;
2021-09-30 15:45:32 +08:00
}
2021-11-24 11:16:08 +08:00
public void DeleteFile ( BeatmapSetInfo model , RealmNamedFileUsage file )
2021-09-30 15:45:32 +08:00
{
beatmapModelManager . DeleteFile ( model , file ) ;
}
public void AddFile ( BeatmapSetInfo model , Stream contents , string filename )
{
beatmapModelManager . AddFile ( model , contents , filename ) ;
}
#endregion
2021-10-01 00:43:57 +08:00
#region Implementation of IDisposable
public void Dispose ( )
{
2021-10-02 23:55:29 +08:00
onlineBeatmapLookupQueue ? . Dispose ( ) ;
2021-10-01 00:43:57 +08:00
}
#endregion
2021-10-15 15:00:09 +08:00
#region Implementation of IPostImports < out BeatmapSetInfo >
2022-01-26 12:37:33 +08:00
public Action < IEnumerable < Live < BeatmapSetInfo > > > ? PostImport
2021-10-15 15:00:09 +08:00
{
set = > beatmapModelManager . PostImport = value ;
}
#endregion
2021-09-30 15:45:32 +08:00
}
2018-04-13 17:19:50 +08:00
}