2021-09-30 06:43:49 +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.
using System ;
using System.Collections.Generic ;
2021-12-14 10:47:11 +00:00
using System.Diagnostics ;
2021-09-30 06:43:49 +00:00
using System.IO ;
using System.Linq ;
using System.Linq.Expressions ;
using System.Text ;
using osu.Framework.Extensions ;
using osu.Framework.Platform ;
using osu.Framework.Testing ;
using osu.Game.Beatmaps.Formats ;
using osu.Game.Database ;
2021-11-19 07:07:55 +00:00
using osu.Game.Extensions ;
2021-09-30 06:43:49 +00:00
using osu.Game.Skinning ;
2021-12-01 07:19:38 +00:00
using osu.Game.Stores ;
2022-05-18 20:37:46 +00:00
using osu.Game.Overlays.Notifications ;
2021-12-14 10:47:11 +00:00
#nullable enable
2021-09-30 06:43:49 +00:00
namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
2021-12-14 10:47:11 +00:00
public class BeatmapModelManager : BeatmapImporter
2021-09-30 06:43:49 +00:00
{
/// <summary>
/// The game working beatmap cache, used to invalidate entries on changes.
/// </summary>
2021-12-14 10:47:11 +00:00
public IWorkingBeatmapCache ? WorkingBeatmapCache { private get ; set ; }
2021-09-30 06:43:49 +00:00
public override IEnumerable < string > HandledExtensions = > new [ ] { ".osz" } ;
protected override string [ ] HashableFileTypes = > new [ ] { ".osu" } ;
2022-01-24 10:59:58 +00:00
public BeatmapModelManager ( RealmAccess realm , Storage storage , BeatmapOnlineLookupQueue ? onlineLookupQueue = null )
: base ( realm , storage , onlineLookupQueue )
2021-09-30 06:43:49 +00:00
{
}
protected override bool ShouldDeleteArchive ( string path ) = > Path . GetExtension ( path ) ? . ToLowerInvariant ( ) = = ".osz" ;
/// <summary>
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
/// </summary>
2021-10-04 07:02:45 +00: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>
2021-09-30 06:43:49 +00:00
/// <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>
2022-02-03 10:38:53 +00:00
public void Save ( BeatmapInfo beatmapInfo , IBeatmap beatmapContent , ISkin ? beatmapSkin = null )
2021-09-30 06:43:49 +00:00
{
2021-10-04 07:02:45 +00:00
var setInfo = beatmapInfo . BeatmapSet ;
2021-12-14 10:47:11 +00:00
Debug . Assert ( setInfo ! = null ) ;
2021-10-13 05:34:31 +00:00
// 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.
2021-11-08 12:23:54 +00:00
// 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.
2022-01-18 13:57:39 +00:00
beatmapContent . Difficulty . CopyTo ( beatmapInfo . Difficulty ) ;
2021-10-13 05:34:31 +00:00
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent . BeatmapInfo = beatmapInfo ;
2021-09-30 06:43:49 +00:00
using ( var stream = new MemoryStream ( ) )
{
using ( var sw = new StreamWriter ( stream , Encoding . UTF8 , 1024 , true ) )
new LegacyBeatmapEncoder ( beatmapContent , beatmapSkin ) . Encode ( sw ) ;
stream . Seek ( 0 , SeekOrigin . Begin ) ;
2022-01-11 09:17:13 +00:00
// 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 = setInfo . Files . SingleOrDefault ( f = > string . Equals ( f . Filename , beatmapInfo . Path , StringComparison . OrdinalIgnoreCase ) ) ;
2022-01-23 18:42:07 +00:00
string targetFilename = getFilename ( beatmapInfo ) ;
// ensure that two difficulties from the set don't point at the same beatmap file.
2022-01-23 18:54:57 +00:00
if ( setInfo . Beatmaps . Any ( b = > b . ID ! = beatmapInfo . ID & & string . Equals ( b . Path , targetFilename , StringComparison . OrdinalIgnoreCase ) ) )
2022-01-23 18:42:07 +00:00
throw new InvalidOperationException ( $"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'." ) ;
2022-01-11 09:17:13 +00:00
if ( existingFileInfo ! = null )
DeleteFile ( setInfo , existingFileInfo ) ;
2021-10-07 08:49:13 +00:00
2022-01-11 09:17:13 +00:00
beatmapInfo . MD5Hash = stream . ComputeMD5Hash ( ) ;
beatmapInfo . Hash = stream . ComputeSHA2Hash ( ) ;
2021-12-14 10:47:11 +00:00
2022-01-11 09:17:13 +00:00
AddFile ( setInfo , stream , getFilename ( beatmapInfo ) ) ;
Update ( setInfo ) ;
2021-09-30 06:43:49 +00:00
}
2021-10-04 07:02:45 +00:00
WorkingBeatmapCache ? . Invalidate ( beatmapInfo ) ;
2021-09-30 06:43:49 +00:00
}
2022-01-11 09:17:13 +00:00
private static string getFilename ( BeatmapInfo beatmapInfo )
{
var metadata = beatmapInfo . Metadata ;
2022-01-23 18:41:41 +00:00
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu" . GetValidArchiveContentFilename ( ) ;
2022-01-11 09:17:13 +00:00
}
2021-09-30 06:43:49 +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>
2021-12-14 10:47:11 +00:00
public BeatmapInfo ? QueryBeatmap ( Expression < Func < BeatmapInfo , bool > > query )
{
2022-01-25 03:58:15 +00:00
return Realm . Run ( realm = > realm . All < BeatmapInfo > ( ) . FirstOrDefault ( query ) ? . Detach ( ) ) ;
2021-12-14 10:47:11 +00:00
}
2022-01-12 05:38:37 +00:00
public void Update ( BeatmapSetInfo item )
{
2022-01-23 16:49:17 +00:00
Realm . Write ( r = >
2022-01-12 05:38:37 +00:00
{
2022-01-23 16:49:17 +00:00
var existing = r . Find < BeatmapSetInfo > ( item . ID ) ;
2022-01-21 08:08:20 +00:00
item . CopyChangesToRealm ( existing ) ;
2022-01-20 16:34:20 +00:00
} ) ;
2022-01-12 05:38:37 +00:00
}
2022-05-18 20:37:46 +00: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 )
{
if ( items . Count = = 0 ) return ;
var notification = new ProgressNotification
{
Progress = 0 ,
Text = $"Preparing to delete all {HumanisedModelName} videos..." ,
CompletionText = $"Deleted all {HumanisedModelName} videos!" ,
State = ProgressNotificationState . Active ,
} ;
if ( ! silent )
PostNotification ? . Invoke ( notification ) ;
int i = 0 ;
foreach ( var b in items )
{
if ( notification . State = = ProgressNotificationState . Cancelled )
// user requested abort
return ;
notification . Text = $"Deleting videos from {HumanisedModelName}s ({++i} of {items.Count})" ;
var video = b . Files . FirstOrDefault ( f = > f . Filename . EndsWith ( ".mp4" ) | | f . Filename . EndsWith ( ".avi" ) | | f . Filename . EndsWith ( ".mov" ) | | f . Filename . EndsWith ( ".flv" ) ) ;
if ( video ! = null )
DeleteFile ( b , video ) ;
notification . Progress = ( float ) i / items . Count ;
}
notification . State = ProgressNotificationState . Completed ;
}
2021-09-30 06:43:49 +00:00
}
}