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 ;
2022-06-16 17:07:04 +08:00
using System.Diagnostics ;
2021-09-30 15:45:32 +08:00
using System.IO ;
using System.Linq ;
using System.Linq.Expressions ;
2022-06-16 17:07:04 +08:00
using System.Text ;
2021-09-30 15:45:32 +08:00
using System.Threading ;
using System.Threading.Tasks ;
using osu.Framework.Audio ;
2021-11-09 16:27:07 +08:00
using osu.Framework.Audio.Track ;
2022-06-16 17:07:04 +08:00
using osu.Framework.Extensions ;
2021-09-30 15:45:32 +08:00
using osu.Framework.IO.Stores ;
using osu.Framework.Platform ;
2022-06-16 17:07:04 +08:00
using osu.Game.Beatmaps.Formats ;
2021-09-30 15:45:32 +08:00
using osu.Game.Database ;
2022-06-16 17:07:04 +08:00
using osu.Game.Extensions ;
2021-09-30 15:45:32 +08:00
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 ;
2022-02-17 06:12:13 +08:00
using osu.Game.Utils ;
2023-09-04 15:18:14 +08:00
using Realms ;
2018-04-13 17:19:50 +08:00
2017-07-27 15:56:41 +08:00
namespace osu.Game.Beatmaps
{
/// <summary>
2021-09-30 14:43:49 +08:00
/// Handles general operations related to global beatmap management.
2017-07-27 15:56:41 +08:00
/// </summary>
2022-07-04 17:13:53 +08:00
public class BeatmapManager : ModelManager < BeatmapSetInfo > , IModelImporter < BeatmapSetInfo > , IWorkingBeatmapCache
2017-07-27 15:56:41 +08:00
{
2021-11-09 16:27:07 +08:00
public ITrackStore BeatmapTrackStore { get ; }
2022-06-16 17:07:04 +08:00
private readonly BeatmapImporter beatmapImporter ;
2021-09-30 17:21:16 +08:00
2021-09-30 15:45:32 +08:00
private readonly WorkingBeatmapCache workingBeatmapCache ;
2022-07-04 17:13:53 +08:00
2023-07-12 03:04:09 +08:00
private readonly BeatmapExporter beatmapExporter ;
private readonly LegacyBeatmapExporter legacyBeatmapExporter ;
2022-12-11 17:30:24 +08:00
2023-04-12 03:42:55 +08:00
public ProcessBeatmapDelegate ? ProcessBeatmap { private get ; set ; }
2021-12-15 13:26:38 +08:00
2023-01-10 00:10:20 +08:00
public override bool PauseImports
{
get = > base . PauseImports ;
set
{
base . PauseImports = value ;
beatmapImporter . PauseImports = value ;
}
}
2022-07-28 15:19:05 +08:00
public BeatmapManager ( Storage storage , RealmAccess realm , IAPIProvider ? api , AudioManager audioManager , IResourceStore < byte [ ] > gameResources , GameHost ? host = null ,
2022-06-20 17:39:53 +08:00
WorkingBeatmap ? defaultBeatmap = null , BeatmapDifficultyCache ? difficultyCache = null , bool performOnlineLookups = false )
2022-06-16 17:53:13 +08:00
: base ( storage , realm )
2017-07-27 15:56:41 +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." ) ;
2022-06-20 17:39:53 +08:00
if ( difficultyCache = = null )
throw new ArgumentNullException ( nameof ( difficultyCache ) , "Difficulty cache must be provided if online lookups are required." ) ;
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-07-04 17:13:53 +08:00
beatmapImporter = CreateBeatmapImporter ( storage , realm ) ;
2023-04-12 03:42:55 +08:00
beatmapImporter . ProcessBeatmap = ( beatmapSet , scope ) = > ProcessBeatmap ? . Invoke ( beatmapSet , scope ) ;
2022-06-16 18:48:18 +08:00
beatmapImporter . PostNotification = obj = > PostNotification ? . Invoke ( obj ) ;
2021-11-09 16:27:07 +08:00
workingBeatmapCache = CreateWorkingBeatmapCache ( audioManager , gameResources , userResources , defaultBeatmap , host ) ;
2022-12-11 17:30:24 +08:00
2023-07-12 03:04:09 +08:00
beatmapExporter = new BeatmapExporter ( storage )
{
PostNotification = obj = > PostNotification ? . Invoke ( obj )
} ;
legacyBeatmapExporter = new LegacyBeatmapExporter ( storage )
2022-12-11 17:30:24 +08:00
{
PostNotification = obj = > PostNotification ? . Invoke ( obj )
} ;
2018-07-19 12:41:09 +08:00
}
2022-06-16 17:07:04 +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-07-04 17:13:53 +08:00
protected virtual BeatmapImporter CreateBeatmapImporter ( Storage storage , RealmAccess realm ) = > new BeatmapImporter ( storage , realm ) ;
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
{
2022-08-01 23:21:13 +08:00
DateAdded = DateTimeOffset . UtcNow ,
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-06-20 14:18:07 +08:00
var imported = beatmapImporter . ImportModel ( 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>
2022-02-15 03:56:05 +08:00
/// Add a new difficulty to the provided <paramref name="targetBeatmapSet"/> based on the provided <paramref name="referenceWorkingBeatmap"/>.
2022-01-24 00:49:17 +08:00
/// The new difficulty will be backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned <see cref="WorkingBeatmap"/>.
/// </summary>
2022-02-15 03:56:05 +08:00
/// <remarks>
/// Contrary to <see cref="CopyExistingDifficulty"/>, this method does not preserve hitobjects and beatmap-level settings from <paramref name="referenceWorkingBeatmap"/>.
/// The created beatmap will have zero hitobjects and will have default settings (including difficulty settings), but will preserve metadata and existing timing points.
/// </remarks>
/// <param name="targetBeatmapSet">The <see cref="BeatmapSetInfo"/> to add the new difficulty to.</param>
/// <param name="referenceWorkingBeatmap">The <see cref="WorkingBeatmap"/> to use as a baseline reference when creating the new difficulty.</param>
/// <param name="rulesetInfo">The ruleset with which the new difficulty should be created.</param>
public virtual WorkingBeatmap CreateNewDifficulty ( BeatmapSetInfo targetBeatmapSet , WorkingBeatmap referenceWorkingBeatmap , RulesetInfo rulesetInfo )
2022-01-24 00:49:17 +08:00
{
2023-01-15 02:39:30 +08:00
var newBeatmapInfo = new BeatmapInfo ( rulesetInfo , new BeatmapDifficulty ( ) , referenceWorkingBeatmap . Metadata . DeepClone ( ) )
2022-02-17 06:12:13 +08:00
{
DifficultyName = NamingUtils . GetNextBestName ( targetBeatmapSet . Beatmaps . Select ( b = > b . DifficultyName ) , "New Difficulty" )
} ;
2022-02-15 03:56:05 +08:00
var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo } ;
2023-01-15 02:39:30 +08:00
foreach ( var timingPoint in referenceWorkingBeatmap . Beatmap . ControlPointInfo . TimingPoints )
2022-02-15 03:56:05 +08:00
newBeatmap . ControlPointInfo . Add ( timingPoint . Time , timingPoint . DeepClone ( ) ) ;
return addDifficultyToSet ( targetBeatmapSet , newBeatmap , referenceWorkingBeatmap . Skin ) ;
}
/// <summary>
/// Add a copy of the provided <paramref name="referenceWorkingBeatmap"/> to the provided <paramref name="targetBeatmapSet"/>.
/// The new difficulty will be backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned <see cref="WorkingBeatmap"/>.
/// </summary>
/// <remarks>
/// Contrary to <see cref="CreateNewDifficulty"/>, this method creates a nearly-exact copy of <paramref name="referenceWorkingBeatmap"/>
/// (with the exception of a few key properties that cannot be copied under any circumstance, like difficulty name, beatmap hash, or online status).
/// </remarks>
/// <param name="targetBeatmapSet">The <see cref="BeatmapSetInfo"/> to add the copy to.</param>
/// <param name="referenceWorkingBeatmap">The <see cref="WorkingBeatmap"/> to be copied.</param>
public virtual WorkingBeatmap CopyExistingDifficulty ( BeatmapSetInfo targetBeatmapSet , WorkingBeatmap referenceWorkingBeatmap )
{
var newBeatmap = referenceWorkingBeatmap . GetPlayableBeatmap ( referenceWorkingBeatmap . BeatmapInfo . Ruleset ) . Clone ( ) ;
2022-02-07 01:40:51 +08:00
BeatmapInfo newBeatmapInfo ;
2022-02-07 00:52:59 +08:00
2022-02-15 03:56:05 +08:00
newBeatmap . BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap . BeatmapInfo . Clone ( ) ;
// assign a new ID to the clone.
newBeatmapInfo . ID = Guid . NewGuid ( ) ;
2022-02-17 06:12:13 +08:00
// add "(copy)" suffix to difficulty name, and additionally ensure that it doesn't conflict with any other potentially pre-existing copies.
newBeatmapInfo . DifficultyName = NamingUtils . GetNextBestName (
targetBeatmapSet . Beatmaps . Select ( b = > b . DifficultyName ) ,
$"{newBeatmapInfo.DifficultyName} (copy)" ) ;
2022-02-15 03:56:05 +08:00
// clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps.
newBeatmapInfo . Hash = string . Empty ;
// clear online properties.
2022-07-25 17:51:19 +08:00
newBeatmapInfo . ResetOnlineInfo ( ) ;
2022-02-15 03:56:05 +08:00
return addDifficultyToSet ( targetBeatmapSet , newBeatmap , referenceWorkingBeatmap . Skin ) ;
}
2022-01-24 00:49:17 +08:00
2022-02-15 03:56:05 +08:00
private WorkingBeatmap addDifficultyToSet ( BeatmapSetInfo targetBeatmapSet , IBeatmap newBeatmap , ISkin beatmapSkin )
{
2022-02-07 01:40:51 +08:00
// populate circular beatmap set info <-> beatmap info references manually.
2022-06-16 18:01:53 +08:00
// several places like `Save()` or `GetWorkingBeatmap()`
2022-02-07 01:40:51 +08:00
// rely on them being freely traversable in both directions for correct operation.
2022-02-15 03:56:05 +08:00
targetBeatmapSet . Beatmaps . Add ( newBeatmap . BeatmapInfo ) ;
newBeatmap . BeatmapInfo . BeatmapSet = targetBeatmapSet ;
2022-02-07 01:40:51 +08:00
2023-01-25 13:04:30 +08:00
save ( newBeatmap . BeatmapInfo , newBeatmap , beatmapSkin , transferCollections : false ) ;
2022-01-24 01:56:19 +08:00
2022-02-07 00:52:59 +08:00
workingBeatmapCache . Invalidate ( targetBeatmapSet ) ;
2022-02-03 18:39:24 +08:00
return GetWorkingBeatmap ( newBeatmap . BeatmapInfo ) ;
2022-01-24 00:49:17 +08:00
}
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-06-16 17:56:53 +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 )
2023-07-06 12:37:42 +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-06-16 17:56:53 +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 )
2023-07-06 12:37:42 +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-06-16 17:56:53 +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-06-16 17:56:53 +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-06-16 17:56:53 +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
/// <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>
2024-10-13 23:35:18 +08:00
public BeatmapInfo ? QueryBeatmap ( Expression < Func < BeatmapInfo , bool > > query ) = > Realm . Run ( r = >
r . All < BeatmapInfo > ( ) . Filter ( $"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false" ) . FirstOrDefault ( query ) ? . Detach ( ) ) ;
2021-12-15 14:24:26 +08:00
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
2022-06-16 17:07:04 +08:00
/// <summary>
2023-01-25 13:11:02 +08:00
/// Saves an existing <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
2022-06-16 17:07:04 +08:00
/// </summary>
2023-01-25 13:11:02 +08:00
/// <remarks>
/// This method will also update any user beatmap collection hash references to the new post-saved hash.
/// </remarks>
2022-06-16 17:07:04 +08:00
/// <param name="beatmapInfo">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>
2023-01-25 13:09:13 +08:00
public virtual void Save ( BeatmapInfo beatmapInfo , IBeatmap beatmapContent , ISkin ? beatmapSkin = null ) = >
save ( beatmapInfo , beatmapContent , beatmapSkin , transferCollections : true ) ;
2022-06-16 17:07:04 +08:00
2022-06-06 19:12:18 +08:00
public void DeleteAllVideos ( )
2022-05-18 14:09:58 +08:00
{
2022-06-16 17:56:53 +08:00
Realm . Write ( r = >
2022-05-18 14:09:58 +08:00
{
var items = r . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending & & ! s . Protected ) ;
2022-06-16 17:07:04 +08:00
DeleteVideos ( items . ToList ( ) ) ;
2022-05-18 14:09:58 +08:00
} ) ;
}
2024-10-12 21:50:45 +08:00
public void ResetAllOffsets ( )
{
const string reset_complete_message = "All offsets have been reset!" ;
Realm . Write ( r = >
{
var items = r . All < BeatmapInfo > ( ) ;
foreach ( var beatmap in items )
{
2024-10-13 23:35:18 +08:00
if ( beatmap . UserSettings . Offset ! = 0 )
beatmap . UserSettings . Offset = 0 ;
2024-10-12 21:50:45 +08:00
}
PostNotification ? . Invoke ( new ProgressCompletionNotification { Text = reset_complete_message } ) ;
} ) ;
}
2022-06-16 17:26:13 +08:00
public void Delete ( Expression < Func < BeatmapSetInfo , bool > > ? filter = null , bool silent = false )
{
2022-06-16 17:56:53 +08:00
Realm . Run ( r = >
2022-06-16 17:26:13 +08:00
{
var items = r . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending & & ! s . Protected ) ;
if ( filter ! = null )
items = items . Where ( filter ) ;
2022-06-16 17:53:13 +08:00
Delete ( items . ToList ( ) , silent ) ;
2022-06-16 17:26:13 +08:00
} ) ;
}
2022-07-27 19:21:16 +08:00
/// <summary>
2022-09-02 15:58:46 +08:00
/// Delete a beatmap difficulty immediately.
2022-07-27 19:21:16 +08:00
/// </summary>
2022-09-02 15:58:46 +08:00
/// <remarks>
/// There's no undoing this operation, as we don't have a soft-deletion flag on <see cref="BeatmapInfo"/>.
/// This may be a future consideration if there's a user requirement for undeleting support.
/// </remarks>
public void DeleteDifficultyImmediately ( BeatmapInfo beatmapInfo )
2022-07-27 19:21:16 +08:00
{
2022-09-02 15:58:46 +08:00
Realm . Write ( r = >
2022-07-27 19:21:16 +08:00
{
2022-09-02 15:58:46 +08:00
if ( ! beatmapInfo . IsManaged )
2023-07-06 12:37:42 +08:00
beatmapInfo = r . Find < BeatmapInfo > ( beatmapInfo . ID ) ! ;
2022-07-27 19:21:16 +08:00
2022-09-02 15:58:46 +08:00
Debug . Assert ( beatmapInfo . BeatmapSet ! = null ) ;
Debug . Assert ( beatmapInfo . File ! = null ) ;
2022-07-27 19:21:16 +08:00
2022-09-02 15:58:46 +08:00
var setInfo = beatmapInfo . BeatmapSet ;
DeleteFile ( setInfo , beatmapInfo . File ) ;
setInfo . Beatmaps . Remove ( beatmapInfo ) ;
2023-06-28 05:43:00 +08:00
r . Remove ( beatmapInfo . Metadata ) ;
r . Remove ( beatmapInfo ) ;
2022-09-06 17:08:51 +08:00
updateHashAndMarkDirty ( setInfo ) ;
2022-09-06 17:10:59 +08:00
workingBeatmapCache . Invalidate ( setInfo ) ;
2022-07-27 19:21:16 +08:00
} ) ;
}
2022-06-16 17:07:04 +08:00
/// <summary>
/// Delete videos from a list of beatmaps.
/// This will post notifications tracking progress.
/// </summary>
public void DeleteVideos ( List < BeatmapSetInfo > items , bool silent = false )
{
2024-02-19 14:12:03 +08:00
const string no_videos_message = "No videos found to delete!" ;
2024-02-19 13:52:23 +08:00
if ( items . Count = = 0 )
{
if ( ! silent )
2024-02-19 14:12:03 +08:00
PostNotification ? . Invoke ( new ProgressCompletionNotification { Text = no_videos_message } ) ;
2024-02-19 13:52:23 +08:00
return ;
}
2022-06-16 17:07:04 +08:00
var notification = new ProgressNotification
{
Progress = 0 ,
2022-06-16 18:05:25 +08:00
Text = $"Preparing to delete all {HumanisedModelName} videos..." ,
2024-02-19 14:12:03 +08:00
CompletionText = no_videos_message ,
2022-06-16 17:07:04 +08:00
State = ProgressNotificationState . Active ,
} ;
if ( ! silent )
PostNotification ? . Invoke ( notification ) ;
int i = 0 ;
int deleted = 0 ;
foreach ( var b in items )
{
if ( notification . State = = ProgressNotificationState . Cancelled )
// user requested abort
return ;
2023-04-21 08:10:24 +08:00
var video = b . Files . FirstOrDefault ( f = > OsuGameBase . VIDEO_EXTENSIONS . Any ( ex = > f . Filename . EndsWith ( ex , StringComparison . OrdinalIgnoreCase ) ) ) ;
2022-06-16 17:07:04 +08:00
if ( video ! = null )
{
DeleteFile ( b , video ) ;
deleted + + ;
2022-06-16 18:05:25 +08:00
notification . CompletionText = $"Deleted {deleted} {HumanisedModelName} video(s)!" ;
2022-06-16 17:07:04 +08:00
}
2022-06-16 18:05:25 +08:00
notification . Text = $"Deleting videos from {HumanisedModelName}s ({deleted} deleted)" ;
2022-06-16 17:07:04 +08:00
notification . Progress = ( float ) + + i / items . Count ;
}
notification . State = ProgressNotificationState . Completed ;
}
2022-01-11 22:04:36 +08:00
public void UndeleteAll ( )
{
2022-06-16 17:56:53 +08:00
Realm . Run ( r = > Undelete ( r . All < BeatmapSetInfo > ( ) . Where ( s = > s . DeletePending ) . ToList ( ) ) ) ;
2022-01-11 22:04:36 +08:00
}
2022-07-26 14:46:29 +08:00
public Task < Live < BeatmapSetInfo > ? > ImportAsUpdate ( ProgressNotification notification , ImportTask importTask , BeatmapSetInfo original ) = >
2022-07-25 18:31:46 +08:00
beatmapImporter . ImportAsUpdate ( notification , importTask , original ) ;
2024-07-01 11:07:13 +08:00
public Task < ExternalEditOperation < BeatmapSetInfo > > BeginExternalEditing ( BeatmapSetInfo model ) = >
beatmapImporter . BeginExternalEditing ( model ) ;
2023-05-06 22:10:18 +08:00
public Task Export ( BeatmapSetInfo beatmap ) = > beatmapExporter . ExportAsync ( beatmap . ToLive ( Realm ) ) ;
2022-12-11 17:30:24 +08:00
2023-07-13 06:20:01 +08:00
public Task ExportLegacy ( BeatmapSetInfo beatmap ) = > legacyBeatmapExporter . ExportAsync ( beatmap . ToLive ( Realm ) ) ;
2023-07-11 08:30:16 +08:00
2022-09-06 17:08:51 +08:00
private void updateHashAndMarkDirty ( BeatmapSetInfo setInfo )
{
setInfo . Hash = beatmapImporter . ComputeHash ( setInfo ) ;
setInfo . Status = BeatmapOnlineStatus . LocallyModified ;
}
2023-01-25 13:09:13 +08:00
private void save ( BeatmapInfo beatmapInfo , IBeatmap beatmapContent , ISkin ? beatmapSkin , bool transferCollections )
2023-01-22 09:27:33 +08:00
{
var setInfo = beatmapInfo . BeatmapSet ;
Debug . Assert ( setInfo ! = null ) ;
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
// CopyTo() will undo such adjustments, while CopyFrom() will not.
beatmapContent . Difficulty . CopyTo ( beatmapInfo . Difficulty ) ;
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent . BeatmapInfo = beatmapInfo ;
2023-05-02 01:06:25 +08:00
// Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.
// Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file,
// which influences the beatmap checksums.
beatmapInfo . LastLocalUpdate = DateTimeOffset . Now ;
beatmapInfo . Status = BeatmapOnlineStatus . LocallyModified ;
beatmapInfo . ResetOnlineInfo ( ) ;
2023-07-13 20:33:21 +08:00
Realm . Write ( r = >
2023-01-22 09:27:33 +08:00
{
2023-07-13 20:33:21 +08:00
using var stream = new MemoryStream ( ) ;
2023-01-22 09:27:33 +08:00
using ( var sw = new StreamWriter ( stream , Encoding . UTF8 , 1024 , true ) )
new LegacyBeatmapEncoder ( beatmapContent , beatmapSkin ) . Encode ( sw ) ;
stream . Seek ( 0 , SeekOrigin . Begin ) ;
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = beatmapInfo . Path ! = null ? setInfo . GetFile ( beatmapInfo . Path ) : null ;
string targetFilename = createBeatmapFilenameFromMetadata ( beatmapInfo ) ;
// ensure that two difficulties from the set don't point at the same beatmap file.
if ( setInfo . Beatmaps . Any ( b = > b . ID ! = beatmapInfo . ID & & string . Equals ( b . Path , targetFilename , StringComparison . OrdinalIgnoreCase ) ) )
throw new InvalidOperationException ( $"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'." ) ;
if ( existingFileInfo ! = null )
DeleteFile ( setInfo , existingFileInfo ) ;
2023-01-24 06:25:13 +08:00
string oldMd5Hash = beatmapInfo . MD5Hash ;
2023-01-22 09:27:33 +08:00
beatmapInfo . MD5Hash = stream . ComputeMD5Hash ( ) ;
beatmapInfo . Hash = stream . ComputeSHA2Hash ( ) ;
AddFile ( setInfo , stream , createBeatmapFilenameFromMetadata ( beatmapInfo ) ) ;
updateHashAndMarkDirty ( setInfo ) ;
2023-07-13 20:33:21 +08:00
var liveBeatmapSet = r . Find < BeatmapSetInfo > ( setInfo . ID ) ! ;
2023-01-22 09:27:33 +08:00
2023-07-13 20:33:21 +08:00
setInfo . CopyChangesToRealm ( liveBeatmapSet ) ;
2023-01-22 09:27:33 +08:00
2023-07-13 20:33:21 +08:00
if ( transferCollections )
beatmapInfo . TransferCollectionReferences ( r , oldMd5Hash ) ;
2023-01-24 06:25:13 +08:00
2023-07-13 20:33:21 +08:00
liveBeatmapSet . Beatmaps . Single ( b = > b . ID = = beatmapInfo . ID )
. UpdateLocalScores ( r ) ;
2023-07-04 13:51:09 +08:00
2023-07-13 20:33:21 +08:00
// do not look up metadata.
// this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
ProcessBeatmap ? . Invoke ( liveBeatmapSet , MetadataLookupScope . None ) ;
} ) ;
2023-01-22 09:27:33 +08:00
Debug . Assert ( beatmapInfo . BeatmapSet ! = null ) ;
static string createBeatmapFilenameFromMetadata ( BeatmapInfo beatmapInfo )
{
var metadata = beatmapInfo . Metadata ;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu" . GetValidFilename ( ) ;
}
}
2021-09-30 15:45:32 +08:00
#region Implementation of ICanAcceptFiles
2022-06-16 17:07:04 +08:00
public Task Import ( params string [ ] paths ) = > beatmapImporter . Import ( paths ) ;
2021-09-30 15:45:32 +08:00
2022-12-13 20:03:25 +08:00
public Task Import ( ImportTask [ ] tasks , ImportParameters parameters = default ) = > beatmapImporter . Import ( tasks , parameters ) ;
2021-09-30 15:45:32 +08:00
2023-01-09 17:54:11 +08:00
public Task < IEnumerable < Live < BeatmapSetInfo > > > Import ( ProgressNotification notification , ImportTask [ ] tasks , ImportParameters parameters = default ) = >
beatmapImporter . Import ( notification , tasks , parameters ) ;
2021-09-30 15:45:32 +08:00
2022-12-12 22:56:11 +08:00
public Task < Live < BeatmapSetInfo > ? > Import ( ImportTask task , ImportParameters parameters = default , CancellationToken cancellationToken = default ) = >
beatmapImporter . Import ( task , parameters , cancellationToken ) ;
2021-09-30 15:45:32 +08:00
2022-06-16 17:07:04 +08:00
public Live < BeatmapSetInfo > ? Import ( BeatmapSetInfo item , ArchiveReader ? archive = null , CancellationToken cancellationToken = default ) = >
2022-12-12 22:56:11 +08:00
beatmapImporter . ImportModel ( item , archive , default , cancellationToken ) ;
2021-09-30 15:45:32 +08:00
2022-06-16 17:07:04 +08:00
public IEnumerable < string > HandledExtensions = > beatmapImporter . HandledExtensions ;
2021-09-30 15:45:32 +08:00
#endregion
#region Implementation of IWorkingBeatmapCache
2022-06-24 20:02:14 +08:00
/// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
/// </summary>
/// <param name="beatmapInfo">The beatmap to lookup.</param>
/// <param name="refetch">Whether to force a refetch from the database to ensure <see cref="BeatmapInfo"/> is up-to-date.</param>
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
2022-06-30 15:46:28 +08:00
public WorkingBeatmap GetWorkingBeatmap ( BeatmapInfo ? beatmapInfo , bool refetch = false )
2022-01-18 23:49:16 +08:00
{
2022-06-30 15:46:28 +08:00
if ( beatmapInfo ! = null )
2022-01-18 23:49:16 +08:00
{
2022-07-25 13:59:11 +08:00
if ( refetch )
2022-06-30 15:46:28 +08:00
workingBeatmapCache . Invalidate ( beatmapInfo ) ;
2022-01-18 23:49:16 +08:00
2022-07-25 13:59:11 +08:00
// Detached beatmapsets don't come with files as an optimisation (see `RealmObjectExtensions.beatmap_set_mapper`).
// If we seem to be missing files, now is a good time to re-fetch.
bool missingFiles = beatmapInfo . BeatmapSet ? . Files . Count = = 0 ;
if ( refetch | | beatmapInfo . IsManaged | | missingFiles )
{
2022-06-30 15:46:28 +08:00
Guid id = beatmapInfo . ID ;
beatmapInfo = Realm . Run ( r = > r . Find < BeatmapInfo > ( id ) ? . Detach ( ) ) ? ? beatmapInfo ;
}
2022-01-18 23:49:16 +08:00
2022-06-30 15:46:28 +08:00
Debug . Assert ( beatmapInfo . IsManaged ! = true ) ;
}
2022-06-24 18:04:29 +08:00
2022-06-24 16:46:46 +08:00
return workingBeatmapCache . GetWorkingBeatmap ( beatmapInfo ) ;
2022-01-18 23:49:16 +08:00
}
2021-09-30 15:45:32 +08:00
2022-06-24 20:02:14 +08:00
WorkingBeatmap IWorkingBeatmapCache . GetWorkingBeatmap ( BeatmapInfo beatmapInfo ) = > GetWorkingBeatmap ( beatmapInfo ) ;
2021-10-06 11:05:30 +08:00
void IWorkingBeatmapCache . Invalidate ( BeatmapSetInfo beatmapSetInfo ) = > workingBeatmapCache . Invalidate ( beatmapSetInfo ) ;
void IWorkingBeatmapCache . Invalidate ( BeatmapInfo beatmapInfo ) = > workingBeatmapCache . Invalidate ( beatmapInfo ) ;
2022-06-20 18:48:46 +08:00
public event Action < WorkingBeatmap > ? OnInvalidated
{
add = > workingBeatmapCache . OnInvalidated + = value ;
remove = > workingBeatmapCache . OnInvalidated - = value ;
}
2024-08-08 13:54:00 +08:00
public override bool IsAvailableLocally ( BeatmapSetInfo model ) = > Realm . Run ( realm = > realm . All < BeatmapSetInfo > ( ) . Any ( s = > s . OnlineID = = model . OnlineID & & ! s . DeletePending ) ) ;
2022-06-16 18:05:25 +08:00
2021-09-30 15:45:32 +08:00
#endregion
2021-10-15 15:00:09 +08:00
#region Implementation of IPostImports < out BeatmapSetInfo >
2022-06-20 17:21:37 +08:00
public Action < IEnumerable < Live < BeatmapSetInfo > > > ? PresentImport
2021-10-15 15:00:09 +08:00
{
2022-06-20 17:21:37 +08:00
set = > beatmapImporter . PresentImport = value ;
2021-10-15 15:00:09 +08:00
}
#endregion
2022-06-16 18:05:25 +08:00
public override string HumanisedModelName = > "beatmap" ;
2021-09-30 15:45:32 +08:00
}
2023-04-12 03:42:55 +08:00
/// <summary>
/// Delegate type for beatmap processing callbacks.
/// </summary>
/// <param name="beatmapSet">The beatmap set to be processed.</param>
/// <param name="lookupScope">The scope to use when looking up metadata.</param>
public delegate void ProcessBeatmapDelegate ( BeatmapSetInfo beatmapSet , MetadataLookupScope lookupScope ) ;
2017-07-27 15:56:41 +08:00
}