2019-01-24 08:43:03 +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.
2018-04-13 09:19:50 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Linq.Expressions ;
2019-05-28 09:59:21 +00:00
using System.Threading ;
2018-04-13 09:19:50 +00:00
using System.Threading.Tasks ;
using Microsoft.EntityFrameworkCore ;
using osu.Framework.Audio ;
2018-05-07 03:25:21 +00:00
using osu.Framework.Audio.Track ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Extensions ;
2018-05-07 03:25:21 +00:00
using osu.Framework.Graphics.Textures ;
2019-06-26 05:08:19 +00:00
using osu.Framework.Lists ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Logging ;
using osu.Framework.Platform ;
2019-05-28 09:59:21 +00:00
using osu.Framework.Threading ;
2018-04-13 09:19:50 +00:00
using osu.Game.Beatmaps.Formats ;
using osu.Game.Database ;
using osu.Game.IO.Archives ;
using osu.Game.Online.API ;
using osu.Game.Online.API.Requests ;
using osu.Game.Rulesets ;
namespace osu.Game.Beatmaps
{
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
2019-06-12 04:28:44 +00:00
public partial class BeatmapManager : DownloadableArchiveModelManager < BeatmapSetInfo , BeatmapSetFileInfo >
2018-04-13 09:19:50 +00:00
{
/// <summary>
/// Fired when a single difficulty has been hidden.
/// </summary>
public event Action < BeatmapInfo > BeatmapHidden ;
/// <summary>
/// Fired when a single difficulty has been restored.
/// </summary>
public event Action < BeatmapInfo > BeatmapRestored ;
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
2018-12-25 09:34:45 +00:00
public readonly WorkingBeatmap DefaultBeatmap ;
2018-04-13 09:19:50 +00:00
public override string [ ] HandledExtensions = > new [ ] { ".osz" } ;
2018-11-28 10:16:05 +00:00
protected override string [ ] HashableFileTypes = > new [ ] { ".osu" } ;
2018-08-31 09:28:53 +00:00
protected override string ImportFromStablePath = > "Songs" ;
2018-04-13 09:19:50 +00:00
private readonly RulesetStore rulesets ;
private readonly BeatmapStore beatmaps ;
private readonly AudioManager audioManager ;
2019-01-25 02:50:52 +00:00
private readonly GameHost host ;
2019-01-25 02:41:44 +00:00
2019-05-28 09:59:21 +00:00
private readonly BeatmapUpdateQueue updateQueue ;
2019-03-13 03:56:47 +00:00
public BeatmapManager ( Storage storage , IDatabaseContextFactory contextFactory , RulesetStore rulesets , IAPIProvider api , AudioManager audioManager , GameHost host = null ,
2018-12-25 09:34:45 +00:00
WorkingBeatmap defaultBeatmap = null )
2019-06-11 14:06:08 +00:00
: base ( storage , contextFactory , api , new BeatmapStore ( contextFactory ) , host )
2018-04-13 09:19:50 +00:00
{
this . rulesets = rulesets ;
this . audioManager = audioManager ;
2019-01-25 11:24:32 +00:00
this . host = host ;
2018-12-25 09:34:45 +00:00
DefaultBeatmap = defaultBeatmap ;
beatmaps = ( BeatmapStore ) ModelStore ;
beatmaps . BeatmapHidden + = b = > BeatmapHidden ? . Invoke ( b ) ;
beatmaps . BeatmapRestored + = b = > BeatmapRestored ? . Invoke ( b ) ;
2018-04-13 09:19:50 +00:00
2019-05-28 09:59:21 +00:00
updateQueue = new BeatmapUpdateQueue ( api ) ;
2018-04-13 09:19:50 +00:00
}
2019-06-12 13:05:34 +00:00
2019-06-18 16:41:19 +00:00
protected override ArchiveDownloadRequest < BeatmapSetInfo > CreateDownloadRequest ( BeatmapSetInfo set , bool minimiseDownloadSize ) = >
new DownloadBeatmapSetRequest ( set , minimiseDownloadSize ) ;
2019-06-11 14:06:08 +00:00
2019-06-12 08:08:50 +00:00
protected override Task Populate ( BeatmapSetInfo beatmapSet , ArchiveReader archive , CancellationToken cancellationToken = default )
2018-04-13 09:19:50 +00:00
{
2018-07-18 03:58:28 +00:00
if ( archive ! = null )
2018-07-19 04:41:09 +00:00
beatmapSet . Beatmaps = createBeatmapDifficulties ( archive ) ;
2018-04-13 09:19:50 +00:00
2018-07-19 04:41:09 +00:00
foreach ( BeatmapInfo b in beatmapSet . Beatmaps )
2018-06-08 06:59:45 +00:00
{
// remove metadata from difficulties where it matches the set
2018-07-19 04:41:09 +00:00
if ( beatmapSet . Metadata . Equals ( b . Metadata ) )
2018-04-13 09:19:50 +00:00
b . Metadata = null ;
2018-07-19 04:41:09 +00:00
b . BeatmapSet = beatmapSet ;
2018-04-13 09:19:50 +00:00
}
2019-03-11 09:13:33 +00:00
validateOnlineIds ( beatmapSet ) ;
2018-08-27 15:59:30 +00:00
2019-06-12 08:08:50 +00:00
return updateQueue . UpdateAsync ( beatmapSet , cancellationToken ) ;
2019-03-11 08:03:01 +00:00
}
2018-08-27 15:59:30 +00:00
2019-03-11 08:03:01 +00:00
protected override void PreImport ( BeatmapSetInfo beatmapSet )
{
2019-03-12 05:40:13 +00:00
if ( beatmapSet . Beatmaps . Any ( b = > b . BaseDifficulty = = null ) )
throw new InvalidOperationException ( $"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}." ) ;
2018-06-08 06:59:45 +00:00
// check if a set already exists with the same online id, delete if it does.
2018-07-19 04:41:09 +00:00
if ( beatmapSet . OnlineBeatmapSetID ! = null )
2018-04-13 09:19:50 +00:00
{
2018-07-19 04:41:09 +00:00
var existingOnlineId = beatmaps . ConsumableItems . FirstOrDefault ( b = > b . OnlineBeatmapSetID = = beatmapSet . OnlineBeatmapSetID ) ;
2019-04-01 03:16:05 +00:00
2018-04-13 09:19:50 +00:00
if ( existingOnlineId ! = null )
{
Delete ( existingOnlineId ) ;
beatmaps . PurgeDeletable ( s = > s . ID = = existingOnlineId . ID ) ;
2019-06-10 10:34:32 +00:00
LogForModel ( beatmapSet , $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged." ) ;
2018-04-13 09:19:50 +00:00
}
}
2018-07-19 04:41:09 +00:00
}
2019-03-11 09:13:33 +00:00
private void validateOnlineIds ( BeatmapSetInfo beatmapSet )
2018-07-19 04:41:09 +00:00
{
2019-03-11 09:13:33 +00:00
var beatmapIds = beatmapSet . Beatmaps . Where ( b = > b . OnlineBeatmapID . HasValue ) . Select ( b = > b . OnlineBeatmapID ) . ToList ( ) ;
2018-07-19 04:41:09 +00:00
2019-03-11 09:13:33 +00:00
// ensure all IDs are unique
if ( beatmapIds . GroupBy ( b = > b ) . Any ( g = > g . Count ( ) > 1 ) )
{
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 ) ;
if ( existing = = null | | existingBeatmaps . Any ( b = > ! existing . Beatmaps . Contains ( b ) ) )
resetIds ( ) ;
}
void resetIds ( ) = > beatmapSet . Beatmaps . ForEach ( b = > b . OnlineBeatmapID = null ) ;
2018-06-08 06:59:45 +00:00
}
2019-06-27 09:44:57 +00:00
protected override bool CheckLocalAvailability ( BeatmapSetInfo model , IQueryable < BeatmapSetInfo > items ) = > items . Any ( b = > b . OnlineBeatmapSetID = = model . OnlineBeatmapSetID ) ;
2018-04-13 09:19:50 +00: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 ) ;
2019-06-26 05:08:19 +00:00
private readonly WeakList < WorkingBeatmap > workingCache = new WeakList < WorkingBeatmap > ( ) ;
2018-04-13 09:19:50 +00: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-04 16:45:32 +00: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 09:19:50 +00: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-04 17:12:15 +00:00
if ( beatmapInfo ? . ID > 0 & & previous ! = null & & previous . BeatmapInfo ? . ID = = beatmapInfo . ID )
2018-12-04 16:45:32 +00:00
return previous ;
2018-04-13 09:19:50 +00:00
if ( beatmapInfo ? . BeatmapSet = = null | | beatmapInfo = = DefaultBeatmap ? . BeatmapInfo )
return DefaultBeatmap ;
2019-06-26 05:08:19 +00:00
var cached = workingCache . FirstOrDefault ( w = > w . BeatmapInfo ? . ID = = beatmapInfo . ID ) ;
if ( cached ! = null )
return cached ;
2018-04-13 09:19:50 +00:00
if ( beatmapInfo . Metadata = = null )
beatmapInfo . Metadata = beatmapInfo . BeatmapSet . Metadata ;
2019-01-25 03:08:31 +00:00
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap ( Files . Store , new LargeTextureStore ( host ? . CreateTextureLoaderStore ( Files . Store ) ) , beatmapInfo , audioManager ) ;
2018-04-13 09:19:50 +00:00
previous ? . TransferTo ( working ) ;
2019-06-26 05:08:19 +00:00
workingCache . Add ( working ) ;
2018-04-13 09:19:50 +00:00
return working ;
}
/// <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 ) ;
2019-03-11 08:03:01 +00:00
protected override bool CanUndelete ( BeatmapSetInfo existing , BeatmapSetInfo import )
{
if ( ! base . CanUndelete ( existing , import ) )
return false ;
2019-03-11 09:13:33 +00: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 08:03:01 +00:00
// force re-import if we are not in a sane state.
2019-03-11 09:13:33 +00:00
return existing . OnlineBeatmapSetID = = import . OnlineBeatmapSetID & & existingIds . SequenceEqual ( importIds ) ;
2019-03-11 08:03:01 +00:00
}
2018-04-13 09:19:50 +00:00
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
2018-05-30 07:15:00 +00:00
public List < BeatmapSetInfo > GetAllUsableBeatmapSets ( ) = > GetAllUsableBeatmapSetsEnumerable ( ) . ToList ( ) ;
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public IQueryable < BeatmapSetInfo > GetAllUsableBeatmapSetsEnumerable ( ) = > beatmaps . ConsumableItems . Where ( s = > ! s . DeletePending & & ! s . Protected ) ;
2018-04-13 09:19:50 +00: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 04:41:09 +00:00
public IQueryable < BeatmapInfo > QueryBeatmaps ( Expression < Func < BeatmapInfo , bool > > query ) = > beatmaps . Beatmaps . AsNoTracking ( ) . Where ( query ) ;
2018-04-13 09:19:50 +00:00
2019-06-12 11:41:02 +00:00
protected override string HumanisedModelName = > "beatmap" ;
2019-06-11 17:12:57 +00:00
2018-06-08 03:46:34 +00:00
protected override BeatmapSetInfo CreateModel ( ArchiveReader reader )
2018-04-13 09:19:50 +00:00
{
// let's make sure there are actually .osu files to import.
string mapName = reader . Filenames . FirstOrDefault ( f = > f . EndsWith ( ".osu" ) ) ;
2019-04-01 03:16:05 +00:00
2018-08-22 06:42:43 +00:00
if ( string . IsNullOrEmpty ( mapName ) )
2018-08-23 01:56:52 +00:00
{
2018-08-25 05:50:46 +00:00
Logger . Log ( $"No beatmap files found in the beatmap archive ({reader.Name})." , LoggingTarget . Database ) ;
2018-08-24 08:57:39 +00:00
return null ;
2018-08-23 01:56:52 +00:00
}
2018-04-13 09:19:50 +00:00
2018-06-08 03:46:34 +00:00
Beatmap beatmap ;
2018-04-13 09:19:50 +00:00
using ( var stream = new StreamReader ( reader . GetStream ( mapName ) ) )
2018-06-08 03:46:34 +00:00
beatmap = Decoder . GetDecoder < Beatmap > ( stream ) . Decode ( stream ) ;
2018-04-13 09:19:50 +00:00
return new BeatmapSetInfo
{
2018-06-08 06:59:45 +00:00
OnlineBeatmapSetID = beatmap . BeatmapInfo . BeatmapSet ? . OnlineBeatmapSetID ,
2018-04-13 09:19:50 +00:00
Beatmaps = new List < BeatmapInfo > ( ) ,
2018-10-03 04:28:00 +00:00
Metadata = beatmap . Metadata ,
2019-06-05 09:17:43 +00:00
DateAdded = DateTimeOffset . UtcNow
2018-04-13 09:19:50 +00:00
} ;
}
/// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary>
2018-06-08 06:26:27 +00:00
private List < BeatmapInfo > createBeatmapDifficulties ( ArchiveReader reader )
2018-04-13 09:19:50 +00:00
{
var beatmapInfos = new List < BeatmapInfo > ( ) ;
foreach ( var name in reader . Filenames . Where ( f = > f . EndsWith ( ".osu" ) ) )
{
using ( var raw = reader . GetStream ( name ) )
2019-06-20 04:33:58 +00:00
using ( var ms = new MemoryStream ( ) ) //we need a memory stream so we can seek
2018-04-13 09:19:50 +00:00
using ( var sr = new StreamReader ( ms ) )
{
raw . CopyTo ( ms ) ;
ms . Position = 0 ;
var decoder = Decoder . GetDecoder < Beatmap > ( sr ) ;
2018-04-19 11:44:38 +00:00
IBeatmap beatmap = decoder . Decode ( sr ) ;
2018-04-13 09:19:50 +00:00
beatmap . BeatmapInfo . Path = name ;
beatmap . BeatmapInfo . Hash = ms . ComputeSHA2Hash ( ) ;
beatmap . BeatmapInfo . MD5Hash = ms . ComputeMD5Hash ( ) ;
2018-07-19 04:41:09 +00:00
var ruleset = rulesets . GetRuleset ( beatmap . BeatmapInfo . RulesetID ) ;
2018-04-13 09:19:50 +00:00
beatmap . BeatmapInfo . Ruleset = ruleset ;
2018-07-19 04:41:09 +00: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 ;
2018-04-13 09:19:50 +00:00
beatmapInfos . Add ( beatmap . BeatmapInfo ) ;
}
}
return beatmapInfos ;
}
2018-05-07 03:25:21 +00:00
2018-06-08 06:59:45 +00:00
/// <summary>
2018-05-07 03:25:21 +00:00
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
2018-06-08 06:59:45 +00:00
/// </summary>
2018-05-07 03:25:21 +00:00
private class DummyConversionBeatmap : WorkingBeatmap
2018-06-08 06:59:45 +00:00
{
2018-05-07 03:25:21 +00:00
private readonly IBeatmap beatmap ;
2018-06-08 06:59:45 +00:00
2018-05-07 03:25:21 +00:00
public DummyConversionBeatmap ( IBeatmap beatmap )
2019-05-31 05:40:53 +00:00
: base ( beatmap . BeatmapInfo , null )
2018-06-08 06:59:45 +00:00
{
2018-05-07 03:25:21 +00:00
this . beatmap = beatmap ;
}
2018-06-08 06:59:45 +00:00
2018-05-07 03:25:21 +00:00
protected override IBeatmap GetBeatmap ( ) = > beatmap ;
protected override Texture GetBackground ( ) = > null ;
protected override Track GetTrack ( ) = > null ;
}
2018-06-08 06:59:45 +00:00
2019-05-28 09:59:21 +00:00
private class BeatmapUpdateQueue
{
private readonly IAPIProvider api ;
2018-06-08 06:59:45 +00:00
2019-06-10 04:52:09 +00:00
private const int update_queue_request_concurrency = 4 ;
2018-06-08 06:59:45 +00:00
2019-06-11 15:35:13 +00:00
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler ( update_queue_request_concurrency , nameof ( BeatmapUpdateQueue ) ) ;
2018-09-13 09:57:33 +00:00
2019-05-28 09:59:21 +00:00
public BeatmapUpdateQueue ( IAPIProvider api )
{
this . api = api ;
2018-06-08 06:59:45 +00:00
}
2019-05-28 09:59:21 +00:00
2019-06-12 08:08:50 +00:00
public Task UpdateAsync ( BeatmapSetInfo beatmapSet , CancellationToken cancellationToken )
2018-06-08 06:59:45 +00:00
{
2019-05-28 09:59:21 +00:00
if ( api ? . State ! = APIState . Online )
2019-06-12 08:08:50 +00:00
return Task . CompletedTask ;
2019-05-28 09:59:21 +00:00
2019-06-10 10:34:32 +00:00
LogForModel ( beatmapSet , "Performing online lookups..." ) ;
2019-06-12 08:08:50 +00:00
return Task . WhenAll ( beatmapSet . Beatmaps . Select ( b = > UpdateAsync ( beatmapSet , b , cancellationToken ) ) . ToArray ( ) ) ;
2018-06-08 06:59:45 +00:00
}
2019-06-10 10:34:32 +00:00
// todo: expose this when we need to do individual difficulty lookups.
2019-06-12 08:00:27 +00:00
protected Task UpdateAsync ( BeatmapSetInfo beatmapSet , BeatmapInfo beatmap , CancellationToken cancellationToken )
= > Task . Factory . StartNew ( ( ) = > update ( beatmapSet , beatmap ) , cancellationToken , TaskCreationOptions . HideScheduler , updateScheduler ) ;
2018-05-07 03:25:21 +00:00
2019-06-12 08:00:27 +00:00
private void update ( BeatmapSetInfo set , BeatmapInfo beatmap )
2018-05-07 03:25:21 +00:00
{
2019-06-10 10:34:32 +00:00
if ( api ? . State ! = APIState . Online )
return ;
2018-05-07 03:25:21 +00:00
2019-05-28 09:59:21 +00:00
var req = new GetBeatmapRequest ( beatmap ) ;
req . Success + = res = >
{
2019-06-10 10:34:32 +00:00
LogForModel ( set , $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}." ) ;
2019-05-28 09:59:21 +00:00
beatmap . Status = res . Status ;
beatmap . BeatmapSet . Status = res . BeatmapSet . Status ;
beatmap . BeatmapSet . OnlineBeatmapSetID = res . OnlineBeatmapSetID ;
beatmap . OnlineBeatmapID = res . OnlineBeatmapID ;
} ;
2019-06-10 10:34:32 +00:00
req . Failure + = e = > { LogForModel ( set , $"Online retrieval failed for {beatmap}" , e ) ; } ;
2019-05-28 09:59:21 +00:00
2019-06-10 04:19:58 +00:00
// intentionally blocking to limit web request concurrency
2019-05-28 09:59:21 +00:00
req . Perform ( api ) ;
}
2018-05-07 03:25:21 +00:00
}
2018-04-13 09:19:50 +00:00
}
}