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
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Linq.Expressions ;
2020-01-10 18:57:34 +08:00
using System.Text ;
2019-05-28 17:59:21 +08:00
using System.Threading ;
2018-04-13 17:19:50 +08:00
using System.Threading.Tasks ;
2020-08-12 23:29:23 +08:00
using JetBrains.Annotations ;
2018-04-13 17:19:50 +08:00
using Microsoft.EntityFrameworkCore ;
using osu.Framework.Audio ;
2018-05-07 11:25:21 +08:00
using osu.Framework.Audio.Track ;
2020-05-19 15:44:22 +08:00
using osu.Framework.Bindables ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Extensions ;
2018-05-07 11:25:21 +08:00
using osu.Framework.Graphics.Textures ;
2019-06-26 13:08:19 +08:00
using osu.Framework.Lists ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Logging ;
using osu.Framework.Platform ;
using osu.Game.Beatmaps.Formats ;
using osu.Game.Database ;
2019-09-10 06:43:30 +08:00
using osu.Game.IO ;
2018-04-13 17:19:50 +08:00
using osu.Game.IO.Archives ;
using osu.Game.Online.API ;
using osu.Game.Online.API.Requests ;
using osu.Game.Rulesets ;
2019-11-25 18:01:24 +08:00
using osu.Game.Rulesets.Objects ;
2020-08-24 18:38:05 +08:00
using osu.Game.Users ;
2020-08-10 11:21:10 +08:00
using osu.Game.Skinning ;
2020-01-10 18:57:34 +08:00
using Decoder = osu . Game . Beatmaps . Formats . Decoder ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Beatmaps
{
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
2020-05-22 22:26:37 +08:00
public partial class BeatmapManager : DownloadableArchiveModelManager < BeatmapSetInfo , BeatmapSetFileInfo > , IDisposable
2018-04-13 17:19:50 +08:00
{
/// <summary>
/// Fired when a single difficulty has been hidden.
/// </summary>
2020-05-19 15:44:22 +08:00
public IBindable < WeakReference < BeatmapInfo > > BeatmapHidden = > beatmapHidden ;
private readonly Bindable < WeakReference < BeatmapInfo > > beatmapHidden = new Bindable < WeakReference < BeatmapInfo > > ( ) ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Fired when a single difficulty has been restored.
/// </summary>
2020-05-19 15:44:22 +08:00
public IBindable < WeakReference < BeatmapInfo > > BeatmapRestored = > beatmapRestored ;
private readonly Bindable < WeakReference < BeatmapInfo > > beatmapRestored = new Bindable < WeakReference < BeatmapInfo > > ( ) ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
2018-12-25 17:34:45 +08:00
public readonly WorkingBeatmap DefaultBeatmap ;
2018-04-13 17:19:50 +08:00
2020-10-02 15:17:10 +08:00
public override IEnumerable < string > HandledExtensions = > new [ ] { ".osz" } ;
2018-04-13 17:19:50 +08:00
2018-11-28 18:16:05 +08:00
protected override string [ ] HashableFileTypes = > new [ ] { ".osu" } ;
2018-08-31 17:28:53 +08:00
protected override string ImportFromStablePath = > "Songs" ;
2018-04-13 17:19:50 +08:00
private readonly RulesetStore rulesets ;
private readonly BeatmapStore beatmaps ;
private readonly AudioManager audioManager ;
2020-08-11 12:16:06 +08:00
private readonly TextureStore textureStore ;
private readonly ITrackStore trackStore ;
2019-05-28 17:59:21 +08:00
2020-09-09 19:11:29 +08:00
[CanBeNull]
private readonly BeatmapOnlineLookupQueue onlineLookupQueue ;
2020-08-12 23:29:23 +08:00
public BeatmapManager ( Storage storage , IDatabaseContextFactory contextFactory , RulesetStore rulesets , IAPIProvider api , [ NotNull ] AudioManager audioManager , GameHost host = null ,
2020-09-09 19:11:29 +08:00
WorkingBeatmap defaultBeatmap = null , bool performOnlineLookups = false )
2019-06-11 22:06:08 +08:00
: base ( storage , contextFactory , api , new BeatmapStore ( contextFactory ) , host )
2018-04-13 17:19:50 +08:00
{
this . rulesets = rulesets ;
this . audioManager = audioManager ;
2018-12-25 17:34:45 +08:00
DefaultBeatmap = defaultBeatmap ;
beatmaps = ( BeatmapStore ) ModelStore ;
2020-05-19 15:44:22 +08:00
beatmaps . BeatmapHidden + = b = > beatmapHidden . Value = new WeakReference < BeatmapInfo > ( b ) ;
beatmaps . BeatmapRestored + = b = > beatmapRestored . Value = new WeakReference < BeatmapInfo > ( b ) ;
2020-06-08 13:48:26 +08:00
beatmaps . ItemRemoved + = removeWorkingCache ;
beatmaps . ItemUpdated + = removeWorkingCache ;
2019-05-28 17:59:21 +08:00
2020-09-09 19:11:29 +08:00
if ( performOnlineLookups )
onlineLookupQueue = new BeatmapOnlineLookupQueue ( api , storage ) ;
2020-08-11 12:16:06 +08:00
textureStore = new LargeTextureStore ( host ? . CreateTextureLoaderStore ( Files . Store ) ) ;
trackStore = audioManager . GetTrackStore ( Files . Store ) ;
2018-04-13 17:19:50 +08:00
}
2019-06-19 00:41:19 +08:00
protected override ArchiveDownloadRequest < BeatmapSetInfo > CreateDownloadRequest ( BeatmapSetInfo set , bool minimiseDownloadSize ) = >
new DownloadBeatmapSetRequest ( set , minimiseDownloadSize ) ;
2019-06-11 22:06:08 +08:00
2019-07-05 12:49:54 +08:00
protected override bool ShouldDeleteArchive ( string path ) = > Path . GetExtension ( path ) ? . ToLowerInvariant ( ) = = ".osz" ;
2019-06-27 20:41:11 +08:00
2020-09-04 12:17:43 +08:00
public WorkingBeatmap CreateNew ( RulesetInfo ruleset , User user )
2020-08-24 18:38:05 +08:00
{
2020-09-01 17:56:49 +08:00
var metadata = new BeatmapMetadata
{
Artist = "artist" ,
Title = "title" ,
2020-09-04 12:17:43 +08:00
Author = user ,
2020-09-01 17:56:49 +08:00
} ;
2020-08-24 18:38:05 +08:00
var set = new BeatmapSetInfo
{
2020-09-01 17:56:49 +08:00
Metadata = metadata ,
2020-08-24 18:38:05 +08:00
Beatmaps = new List < BeatmapInfo >
{
new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty ( ) ,
2020-09-01 17:56:49 +08:00
Ruleset = ruleset ,
Metadata = metadata ,
Version = "difficulty"
2020-08-24 18:38:05 +08:00
}
}
} ;
var working = Import ( set ) . Result ;
return GetWorkingBeatmap ( working . Beatmaps . First ( ) ) ;
}
2020-03-30 10:52:11 +08:00
protected override async Task Populate ( BeatmapSetInfo beatmapSet , ArchiveReader archive , CancellationToken cancellationToken = default )
2018-04-13 17:19:50 +08:00
{
2018-07-18 11:58:28 +08:00
if ( archive ! = null )
2019-09-19 19:02:45 +08:00
beatmapSet . Beatmaps = createBeatmapDifficulties ( beatmapSet . Files ) ;
2018-04-13 17:19:50 +08:00
2018-07-19 12:41:09 +08:00
foreach ( BeatmapInfo b in beatmapSet . Beatmaps )
2018-06-08 14:59:45 +08:00
{
// remove metadata from difficulties where it matches the set
2018-07-19 12:41:09 +08:00
if ( beatmapSet . Metadata . Equals ( b . Metadata ) )
2018-04-13 17:19:50 +08:00
b . Metadata = null ;
2018-07-19 12:41:09 +08:00
b . BeatmapSet = beatmapSet ;
2018-04-13 17:19:50 +08:00
}
2019-03-11 17:13:33 +08:00
validateOnlineIds ( beatmapSet ) ;
2018-08-27 23:59:30 +08:00
2020-03-30 14:07:56 +08:00
bool hadOnlineBeatmapIDs = beatmapSet . Beatmaps . Any ( b = > b . OnlineBeatmapID > 0 ) ;
2020-09-09 19:11:29 +08:00
if ( onlineLookupQueue ! = null )
await onlineLookupQueue . UpdateAsync ( beatmapSet , cancellationToken ) ;
2020-03-30 10:52:11 +08:00
2020-03-30 14:07:56 +08:00
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if ( hadOnlineBeatmapIDs & & ! beatmapSet . Beatmaps . Any ( b = > b . OnlineBeatmapID > 0 ) )
{
if ( beatmapSet . OnlineBeatmapSetID ! = null )
{
beatmapSet . OnlineBeatmapSetID = null ;
LogForModel ( beatmapSet , "Disassociating beatmap set ID due to loss of all beatmap IDs" ) ;
}
}
2019-03-11 16:03:01 +08:00
}
2018-08-27 23:59:30 +08:00
2019-03-11 16:03:01 +08:00
protected override void PreImport ( BeatmapSetInfo beatmapSet )
{
2019-03-12 13:40:13 +08:00
if ( beatmapSet . Beatmaps . Any ( b = > b . BaseDifficulty = = null ) )
throw new InvalidOperationException ( $"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}." ) ;
2018-06-08 14:59:45 +08:00
// check if a set already exists with the same online id, delete if it does.
2018-07-19 12:41:09 +08:00
if ( beatmapSet . OnlineBeatmapSetID ! = null )
2018-04-13 17:19:50 +08:00
{
2018-07-19 12:41:09 +08:00
var existingOnlineId = beatmaps . ConsumableItems . FirstOrDefault ( b = > b . OnlineBeatmapSetID = = beatmapSet . OnlineBeatmapSetID ) ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
if ( existingOnlineId ! = null )
{
Delete ( existingOnlineId ) ;
beatmaps . PurgeDeletable ( s = > s . ID = = existingOnlineId . ID ) ;
2019-06-10 18:34:32 +08:00
LogForModel ( beatmapSet , $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged." ) ;
2018-04-13 17:19:50 +08:00
}
}
2018-07-19 12:41:09 +08:00
}
2019-03-11 17:13:33 +08:00
private void validateOnlineIds ( BeatmapSetInfo beatmapSet )
2018-07-19 12:41:09 +08:00
{
2019-03-11 17:13:33 +08:00
var beatmapIds = beatmapSet . Beatmaps . Where ( b = > b . OnlineBeatmapID . HasValue ) . Select ( b = > b . OnlineBeatmapID ) . ToList ( ) ;
2018-07-19 12:41:09 +08:00
2020-05-04 13:43:47 +08:00
LogForModel ( beatmapSet , $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps..." ) ;
2019-11-14 17:52:07 +08:00
2019-03-11 17:13:33 +08:00
// ensure all IDs are unique
if ( beatmapIds . GroupBy ( b = > b ) . Any ( g = > g . Count ( ) > 1 ) )
{
2019-11-14 17:52:07 +08:00
LogForModel ( beatmapSet , "Found non-unique IDs, resetting..." ) ;
2019-03-11 17:13:33 +08:00
resetIds ( ) ;
return ;
}
// find any existing beatmaps in the database that have matching online ids
var existingBeatmaps = QueryBeatmaps ( b = > beatmapIds . Contains ( b . OnlineBeatmapID ) ) . ToList ( ) ;
if ( existingBeatmaps . Count > 0 )
{
// reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
// we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
var existing = CheckForExisting ( beatmapSet ) ;
2019-11-14 17:52:07 +08:00
2019-03-11 17:13:33 +08:00
if ( existing = = null | | existingBeatmaps . Any ( b = > ! existing . Beatmaps . Contains ( b ) ) )
2019-11-14 17:52:07 +08:00
{
LogForModel ( beatmapSet , "Found existing import with IDs already, resetting..." ) ;
2019-03-11 17:13:33 +08:00
resetIds ( ) ;
2019-11-14 17:52:07 +08:00
}
2019-03-11 17:13:33 +08:00
}
void resetIds ( ) = > beatmapSet . Beatmaps . ForEach ( b = > b . OnlineBeatmapID = null ) ;
2018-06-08 14:59:45 +08:00
}
2019-12-17 14:34:16 +08:00
protected override bool CheckLocalAvailability ( BeatmapSetInfo model , IQueryable < BeatmapSetInfo > items )
= > base . CheckLocalAvailability ( model , items )
| | ( model . OnlineBeatmapSetID ! = null & & items . Any ( b = > b . OnlineBeatmapSetID = = model . OnlineBeatmapSetID ) ) ;
2019-06-27 17:44:57 +08:00
2018-04-13 17:19:50 +08:00
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>
/// <param name="beatmap">The beatmap difficulty to hide.</param>
public void Hide ( BeatmapInfo beatmap ) = > beatmaps . Hide ( beatmap ) ;
/// <summary>
/// Restore a beatmap difficulty.
/// </summary>
/// <param name="beatmap">The beatmap difficulty to restore.</param>
public void Restore ( BeatmapInfo beatmap ) = > beatmaps . Restore ( beatmap ) ;
2020-01-10 18:57:34 +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>
2020-08-31 23:24:00 +08:00
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
2020-09-01 23:58:06 +08:00
public void Save ( BeatmapInfo info , IBeatmap beatmapContent , ISkin beatmapSkin = null )
2020-01-10 18:57:34 +08:00
{
2020-09-09 11:48:11 +08:00
var setInfo = info . BeatmapSet ;
2020-01-10 18:57:34 +08:00
using ( var stream = new MemoryStream ( ) )
{
using ( var sw = new StreamWriter ( stream , Encoding . UTF8 , 1024 , true ) )
2020-08-23 21:08:02 +08:00
new LegacyBeatmapEncoder ( beatmapContent , beatmapSkin ) . Encode ( sw ) ;
2020-01-10 18:57:34 +08:00
stream . Seek ( 0 , SeekOrigin . Begin ) ;
2020-06-08 13:40:17 +08:00
using ( ContextFactory . GetForWrite ( ) )
{
var beatmapInfo = setInfo . Beatmaps . Single ( b = > b . ID = = info . ID ) ;
2020-09-01 17:55:49 +08:00
var metadata = beatmapInfo . Metadata ? ? setInfo . Metadata ;
2020-09-03 18:31:40 +08:00
// grab the original file (or create a new one if not found).
var fileInfo = setInfo . Files . SingleOrDefault ( f = > string . Equals ( f . Filename , beatmapInfo . Path , StringComparison . OrdinalIgnoreCase ) ) ? ? new BeatmapSetFileInfo ( ) ;
2020-09-01 17:55:49 +08:00
// metadata may have changed; update the path with the standard format.
2020-09-03 20:33:25 +08:00
beatmapInfo . Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu" ;
2020-06-08 13:40:17 +08:00
beatmapInfo . MD5Hash = stream . ComputeMD5Hash ( ) ;
2020-09-03 18:31:40 +08:00
// update existing or populate new file's filename.
fileInfo . Filename = beatmapInfo . Path ;
2020-09-01 17:55:49 +08:00
2020-06-08 13:40:17 +08:00
stream . Seek ( 0 , SeekOrigin . Begin ) ;
2020-09-25 12:10:04 +08:00
ReplaceFile ( setInfo , fileInfo , stream ) ;
2020-06-08 13:40:17 +08:00
}
2020-01-10 18:57:34 +08:00
}
2020-01-14 18:03:50 +08:00
2020-06-08 13:48:26 +08:00
removeWorkingCache ( info ) ;
2020-01-10 18:57:34 +08:00
}
2020-08-05 21:09:47 +08:00
private readonly WeakList < BeatmapManagerWorkingBeatmap > workingCache = new WeakList < BeatmapManagerWorkingBeatmap > ( ) ;
2019-06-26 13:08:19 +08:00
2018-04-13 17:19:50 +08:00
/// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
/// </summary>
/// <param name="beatmapInfo">The beatmap to lookup.</param>
2018-12-05 00:45:32 +08:00
/// <param name="previous">The currently loaded <see cref="WorkingBeatmap"/>. Allows for optimisation where elements are shared with the new beatmap. May be returned if beatmapInfo requested matches</param>
2018-04-13 17:19:50 +08:00
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
public WorkingBeatmap GetWorkingBeatmap ( BeatmapInfo beatmapInfo , WorkingBeatmap previous = null )
{
2018-12-05 01:12:15 +08:00
if ( beatmapInfo ? . ID > 0 & & previous ! = null & & previous . BeatmapInfo ? . ID = = beatmapInfo . ID )
2018-12-05 00:45:32 +08:00
return previous ;
2018-04-13 17:19:50 +08:00
if ( beatmapInfo ? . BeatmapSet = = null | | beatmapInfo = = DefaultBeatmap ? . BeatmapInfo )
return DefaultBeatmap ;
2020-04-28 20:43:35 +08:00
if ( beatmapInfo . BeatmapSet . Files = = null )
{
var info = beatmapInfo ;
beatmapInfo = QueryBeatmap ( b = > b . ID = = info . ID ) ;
}
2020-06-22 19:33:08 +08:00
if ( beatmapInfo = = null )
return DefaultBeatmap ;
2019-07-04 13:33:00 +08:00
lock ( workingCache )
{
2019-07-08 16:10:12 +08:00
var working = workingCache . FirstOrDefault ( w = > w . BeatmapInfo ? . ID = = beatmapInfo . ID ) ;
2020-08-05 21:09:47 +08:00
if ( working ! = null )
return working ;
2019-06-26 13:08:19 +08:00
2020-08-05 21:09:47 +08:00
beatmapInfo . Metadata ? ? = beatmapInfo . BeatmapSet . Metadata ;
2018-04-13 17:19:50 +08:00
2020-08-11 12:16:06 +08:00
workingCache . Add ( working = new BeatmapManagerWorkingBeatmap ( Files . Store , textureStore , trackStore , beatmapInfo , audioManager ) ) ;
2018-04-13 17:19:50 +08:00
2019-07-04 13:33:00 +08:00
return working ;
}
2018-04-13 17:19:50 +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>
public BeatmapSetInfo QueryBeatmapSet ( Expression < Func < BeatmapSetInfo , bool > > query ) = > beatmaps . ConsumableItems . AsNoTracking ( ) . FirstOrDefault ( query ) ;
2020-06-03 17:03:10 +08:00
protected override bool CanReuseExisting ( BeatmapSetInfo existing , BeatmapSetInfo import )
2019-03-11 16:03:01 +08:00
{
2020-06-03 17:03:10 +08:00
if ( ! base . CanReuseExisting ( existing , import ) )
2019-03-11 16:03:01 +08:00
return false ;
2019-03-11 17:13:33 +08:00
var existingIds = existing . Beatmaps . Select ( b = > b . OnlineBeatmapID ) . OrderBy ( i = > i ) ;
var importIds = import . Beatmaps . Select ( b = > b . OnlineBeatmapID ) . OrderBy ( i = > i ) ;
2019-03-11 16:03:01 +08:00
// force re-import if we are not in a sane state.
2019-03-11 17:13:33 +08:00
return existing . OnlineBeatmapSetID = = import . OnlineBeatmapSetID & & existingIds . SequenceEqual ( importIds ) ;
2019-03-11 16:03:01 +08:00
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
2020-07-10 15:33:20 +08:00
public List < BeatmapSetInfo > GetAllUsableBeatmapSets ( IncludedDetails includes = IncludedDetails . All , bool includeProtected = false ) = >
GetAllUsableBeatmapSetsEnumerable ( includes , includeProtected ) . ToList ( ) ;
2018-05-30 15:15:00 +08:00
/// <summary>
2020-04-28 20:43:35 +08:00
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. Note that files are not populated.
2018-05-30 15:15:00 +08:00
/// </summary>
2020-04-28 20:43:35 +08:00
/// <param name="includes">The level of detail to include in the returned objects.</param>
2020-07-10 15:33:20 +08:00
/// <param name="includeProtected">Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases.</param>
2018-05-30 15:15:00 +08:00
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
2020-07-10 15:33:20 +08:00
public IEnumerable < BeatmapSetInfo > GetAllUsableBeatmapSetsEnumerable ( IncludedDetails includes , bool includeProtected = false )
2020-04-28 20:43:35 +08:00
{
IQueryable < BeatmapSetInfo > queryable ;
switch ( includes )
{
case IncludedDetails . Minimal :
queryable = beatmaps . BeatmapSetsOverview ;
break ;
case IncludedDetails . AllButFiles :
queryable = beatmaps . BeatmapSetsWithoutFiles ;
break ;
default :
queryable = beatmaps . ConsumableItems ;
break ;
}
2020-04-29 15:51:22 +08:00
// AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY
// clause which causes queries to take 5-10x longer.
// TODO: remove if upgrading to EF core 3.x.
2020-07-10 15:33:20 +08:00
return queryable . AsEnumerable ( ) . Where ( s = > ! s . DeletePending & & ( includeProtected | | ! s . Protected ) ) ;
2020-04-28 20:43:35 +08:00
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>Results from the provided query.</returns>
public IEnumerable < BeatmapSetInfo > QueryBeatmapSets ( Expression < Func < BeatmapSetInfo , bool > > query ) = > beatmaps . ConsumableItems . AsNoTracking ( ) . Where ( query ) ;
/// <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>
public BeatmapInfo QueryBeatmap ( Expression < Func < BeatmapInfo , bool > > query ) = > beatmaps . Beatmaps . AsNoTracking ( ) . FirstOrDefault ( query ) ;
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>Results from the provided query.</returns>
2018-07-19 12:41:09 +08:00
public IQueryable < BeatmapInfo > QueryBeatmaps ( Expression < Func < BeatmapInfo , bool > > query ) = > beatmaps . Beatmaps . AsNoTracking ( ) . Where ( query ) ;
2018-04-13 17:19:50 +08:00
2019-06-12 19:41:02 +08:00
protected override string HumanisedModelName = > "beatmap" ;
2018-06-08 11:46:34 +08:00
protected override BeatmapSetInfo CreateModel ( ArchiveReader reader )
2018-04-13 17:19:50 +08:00
{
// let's make sure there are actually .osu files to import.
string mapName = reader . Filenames . FirstOrDefault ( f = > f . EndsWith ( ".osu" ) ) ;
2019-04-01 11:16:05 +08:00
2018-08-22 14:42:43 +08:00
if ( string . IsNullOrEmpty ( mapName ) )
2018-08-23 09:56:52 +08:00
{
2018-08-25 13:50:46 +08:00
Logger . Log ( $"No beatmap files found in the beatmap archive ({reader.Name})." , LoggingTarget . Database ) ;
2018-08-24 16:57:39 +08:00
return null ;
2018-08-23 09:56:52 +08:00
}
2018-04-13 17:19:50 +08:00
2018-06-08 11:46:34 +08:00
Beatmap beatmap ;
2019-09-10 06:43:30 +08:00
using ( var stream = new LineBufferedReader ( reader . GetStream ( mapName ) ) )
2018-06-08 11:46:34 +08:00
beatmap = Decoder . GetDecoder < Beatmap > ( stream ) . Decode ( stream ) ;
2018-04-13 17:19:50 +08:00
return new BeatmapSetInfo
{
2018-06-08 14:59:45 +08:00
OnlineBeatmapSetID = beatmap . BeatmapInfo . BeatmapSet ? . OnlineBeatmapSetID ,
2018-04-13 17:19:50 +08:00
Beatmaps = new List < BeatmapInfo > ( ) ,
2018-10-03 12:28:00 +08:00
Metadata = beatmap . Metadata ,
2019-07-08 15:43:35 +08:00
DateAdded = DateTimeOffset . UtcNow
2018-04-13 17:19:50 +08:00
} ;
}
/// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary>
2019-09-19 19:02:45 +08:00
private List < BeatmapInfo > createBeatmapDifficulties ( List < BeatmapSetFileInfo > files )
2018-04-13 17:19:50 +08:00
{
var beatmapInfos = new List < BeatmapInfo > ( ) ;
2019-09-19 19:02:45 +08:00
foreach ( var file in files . Where ( f = > f . Filename . EndsWith ( ".osu" ) ) )
2018-04-13 17:19:50 +08:00
{
2019-09-19 19:02:45 +08:00
using ( var raw = Files . Store . GetStream ( file . FileInfo . StoragePath ) )
2020-05-05 09:31:11 +08:00
using ( var ms = new MemoryStream ( ) ) // we need a memory stream so we can seek
2019-09-10 06:43:30 +08:00
using ( var sr = new LineBufferedReader ( ms ) )
2018-04-13 17:19:50 +08:00
{
raw . CopyTo ( ms ) ;
ms . Position = 0 ;
var decoder = Decoder . GetDecoder < Beatmap > ( sr ) ;
2018-04-19 19:44:38 +08:00
IBeatmap beatmap = decoder . Decode ( sr ) ;
2018-04-13 17:19:50 +08:00
2019-11-14 18:02:11 +08:00
string hash = ms . ComputeSHA2Hash ( ) ;
if ( beatmapInfos . Any ( b = > b . Hash = = hash ) )
continue ;
2019-09-19 19:02:45 +08:00
beatmap . BeatmapInfo . Path = file . Filename ;
2019-11-14 18:02:11 +08:00
beatmap . BeatmapInfo . Hash = hash ;
2018-04-13 17:19:50 +08:00
beatmap . BeatmapInfo . MD5Hash = ms . ComputeMD5Hash ( ) ;
2018-07-19 12:41:09 +08:00
var ruleset = rulesets . GetRuleset ( beatmap . BeatmapInfo . RulesetID ) ;
2018-04-13 17:19:50 +08:00
beatmap . BeatmapInfo . Ruleset = ruleset ;
2019-09-19 01:37:35 +08:00
2018-07-19 12:41:09 +08:00
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap . BeatmapInfo . StarDifficulty = ruleset ? . CreateInstance ( ) . CreateDifficultyCalculator ( new DummyConversionBeatmap ( beatmap ) ) . Calculate ( ) . StarRating ? ? 0 ;
2019-07-08 16:56:48 +08:00
beatmap . BeatmapInfo . Length = calculateLength ( beatmap ) ;
2019-07-08 15:43:35 +08:00
beatmap . BeatmapInfo . BPM = beatmap . ControlPointInfo . BPMMode ;
2019-07-07 23:26:56 +08:00
2018-04-13 17:19:50 +08:00
beatmapInfos . Add ( beatmap . BeatmapInfo ) ;
}
}
return beatmapInfos ;
}
2018-05-07 11:25:21 +08:00
2019-07-08 16:56:48 +08:00
private double calculateLength ( IBeatmap b )
{
2019-07-09 22:22:21 +08:00
if ( ! b . HitObjects . Any ( ) )
return 0 ;
2019-07-08 16:56:48 +08:00
2019-07-09 22:22:21 +08:00
var lastObject = b . HitObjects . Last ( ) ;
2019-11-25 18:01:24 +08:00
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
double endTime = lastObject . GetEndTime ( ) ;
2019-07-09 22:22:21 +08:00
double startTime = b . HitObjects . First ( ) . StartTime ;
return endTime - startTime ;
2019-07-08 16:56:48 +08:00
}
2020-06-08 13:48:26 +08:00
private void removeWorkingCache ( BeatmapSetInfo info )
{
if ( info . Beatmaps = = null ) return ;
foreach ( var b in info . Beatmaps )
removeWorkingCache ( b ) ;
}
private void removeWorkingCache ( BeatmapInfo info )
{
lock ( workingCache )
{
var working = workingCache . FirstOrDefault ( w = > w . BeatmapInfo ? . ID = = info . ID ) ;
if ( working ! = null )
workingCache . Remove ( working ) ;
}
}
2020-05-22 22:26:37 +08:00
public void Dispose ( )
{
onlineLookupQueue ? . Dispose ( ) ;
}
2018-05-07 11:25:21 +08:00
/// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary>
private class DummyConversionBeatmap : WorkingBeatmap
{
private readonly IBeatmap beatmap ;
public DummyConversionBeatmap ( IBeatmap beatmap )
2019-05-31 13:40:53 +08:00
: base ( beatmap . BeatmapInfo , null )
2018-05-07 11:25:21 +08:00
{
this . beatmap = beatmap ;
}
protected override IBeatmap GetBeatmap ( ) = > beatmap ;
protected override Texture GetBackground ( ) = > null ;
2020-08-07 21:31:41 +08:00
protected override Track GetBeatmapTrack ( ) = > null ;
2018-05-07 11:25:21 +08:00
}
2018-04-13 17:19:50 +08:00
}
2020-04-28 20:43:35 +08:00
/// <summary>
/// The level of detail to include in database results.
/// </summary>
public enum IncludedDetails
{
/// <summary>
/// Only include beatmap difficulties and set level metadata.
/// </summary>
Minimal ,
/// <summary>
/// Include all difficulties, rulesets, difficulty metadata but no files.
/// </summary>
AllButFiles ,
/// <summary>
/// Include everything.
/// </summary>
All
}
2018-04-13 17:19:50 +08:00
}