2021-09-30 14:43:49 +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.
using System ;
using System.Collections.Generic ;
2021-12-14 18:47:11 +08:00
using System.Diagnostics ;
2021-09-30 14:43:49 +08: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 15:07:55 +08:00
using osu.Game.Extensions ;
2021-09-30 14:43:49 +08:00
using osu.Game.Skinning ;
2021-12-01 15:19:38 +08:00
using osu.Game.Stores ;
2021-12-14 18:47:11 +08:00
#nullable enable
2021-09-30 14:43:49 +08:00
namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
2021-12-14 18:47:11 +08:00
public class BeatmapModelManager : BeatmapImporter
2021-09-30 14:43:49 +08:00
{
/// <summary>
/// The game working beatmap cache, used to invalidate entries on changes.
/// </summary>
2021-12-14 18:47:11 +08:00
public IWorkingBeatmapCache ? WorkingBeatmapCache { private get ; set ; }
2021-09-30 14:43:49 +08:00
public override IEnumerable < string > HandledExtensions = > new [ ] { ".osz" } ;
protected override string [ ] HashableFileTypes = > new [ ] { ".osu" } ;
2021-12-14 18:47:11 +08:00
public BeatmapModelManager ( RealmContextFactory contextFactory , Storage storage , BeatmapOnlineLookupQueue ? onlineLookupQueue = null )
: base ( contextFactory , storage , onlineLookupQueue )
2021-09-30 14:43:49 +08: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 15:02:45 +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>
2021-09-30 14:43:49 +08: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>
2021-12-14 18:47:11 +08:00
public virtual void Save ( BeatmapInfo beatmapInfo , IBeatmap beatmapContent , ISkin ? beatmapSkin = null )
2021-09-30 14:43:49 +08:00
{
2021-10-04 15:02:45 +08:00
var setInfo = beatmapInfo . BeatmapSet ;
2021-09-30 14:43:49 +08:00
2021-12-14 18:47:11 +08:00
Debug . Assert ( setInfo ! = null ) ;
2021-10-13 13:34:31 +08: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 20:23:54 +08: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.
beatmapContent . Difficulty . CopyTo ( beatmapInfo . BaseDifficulty ) ;
2021-10-13 13:34:31 +08: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 14:43:49 +08: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 17:17:13 +08: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 ) ) ;
if ( existingFileInfo ! = null )
DeleteFile ( setInfo , existingFileInfo ) ;
2021-10-07 16:49:13 +08:00
2022-01-11 17:17:13 +08:00
beatmapInfo . MD5Hash = stream . ComputeMD5Hash ( ) ;
beatmapInfo . Hash = stream . ComputeSHA2Hash ( ) ;
2021-12-14 18:47:11 +08:00
2022-01-11 17:17:13 +08:00
AddFile ( setInfo , stream , getFilename ( beatmapInfo ) ) ;
Update ( setInfo ) ;
2021-09-30 14:43:49 +08:00
}
2021-10-04 15:02:45 +08:00
WorkingBeatmapCache ? . Invalidate ( beatmapInfo ) ;
2021-09-30 14:43:49 +08:00
}
2022-01-11 17:17:13 +08:00
private static string getFilename ( BeatmapInfo beatmapInfo )
{
var metadata = beatmapInfo . Metadata ;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu" . GetValidArchiveContentFilename ( ) ;
}
2021-09-30 14:43:49 +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>
2021-12-14 18:47:11 +08:00
public BeatmapInfo ? QueryBeatmap ( Expression < Func < BeatmapInfo , bool > > query )
{
using ( var context = ContextFactory . CreateContext ( ) )
2022-01-07 13:17:22 +08:00
return context . All < BeatmapInfo > ( ) . FirstOrDefault ( query ) ? . Detach ( ) ;
2021-12-14 18:47:11 +08:00
}
2022-01-12 13:38:37 +08:00
public void Update ( BeatmapSetInfo item )
{
using ( var realm = ContextFactory . CreateContext ( ) )
{
var existing = realm . Find < BeatmapSetInfo > ( item . ID ) ;
realm . Write ( r = > item . CopyChangesToRealm ( existing ) ) ;
}
}
2021-09-30 14:43:49 +08:00
}
}