2017-07-26 15:28:32 +08:00
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
2017-07-27 14:34:13 +08:00
using System.Linq.Expressions ;
2017-07-26 19:22:02 +08:00
using Ionic.Zip ;
2017-07-26 15:28:32 +08:00
using osu.Framework.Extensions ;
using osu.Framework.Logging ;
using osu.Framework.Platform ;
using osu.Game.Beatmaps.Formats ;
using osu.Game.Beatmaps.IO ;
2017-07-26 19:22:02 +08:00
using osu.Game.IO ;
2017-07-26 15:28:32 +08:00
using osu.Game.IPC ;
using osu.Game.Rulesets ;
using SQLite.Net ;
2017-07-26 19:22:02 +08:00
using FileInfo = osu . Game . IO . FileInfo ;
2017-07-26 15:28:32 +08:00
namespace osu.Game.Beatmaps
{
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
public class BeatmapStore
{
2017-07-26 22:13:02 +08:00
/// <summary>
/// Fired when a new <see cref="BeatmapSetInfo"/> becomes available in the database.
/// </summary>
2017-07-26 15:28:32 +08:00
public event Action < BeatmapSetInfo > BeatmapSetAdded ;
2017-07-26 22:13:02 +08:00
/// <summary>
/// Fired when a <see cref="BeatmapSetInfo"/> is removed from the database.
/// </summary>
public event Action < BeatmapSetInfo > BeatmapSetRemoved ;
2017-07-26 15:28:32 +08:00
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
public WorkingBeatmap DefaultBeatmap { private get ; set ; }
2017-07-26 22:13:02 +08:00
private readonly Storage storage ;
private readonly FileDatabase files ;
private readonly RulesetDatabase rulesets ;
2017-07-27 14:34:13 +08:00
private readonly BeatmapDatabase beatmaps ;
2017-07-26 22:13:02 +08:00
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private BeatmapIPCChannel ipc ;
2017-07-26 19:22:02 +08:00
public BeatmapStore ( Storage storage , FileDatabase files , SQLiteConnection connection , RulesetDatabase rulesets , IIpcHost importHost = null )
2017-07-26 15:28:32 +08:00
{
2017-07-27 14:34:13 +08:00
beatmaps = new BeatmapDatabase ( connection ) ;
beatmaps . BeatmapSetAdded + = s = > BeatmapSetAdded ? . Invoke ( s ) ;
beatmaps . BeatmapSetRemoved + = s = > BeatmapSetRemoved ? . Invoke ( s ) ;
2017-07-26 15:28:32 +08:00
this . storage = storage ;
2017-07-26 19:22:02 +08:00
this . files = files ;
2017-07-26 15:28:32 +08:00
this . rulesets = rulesets ;
2017-07-26 19:22:02 +08:00
2017-07-26 15:28:32 +08:00
if ( importHost ! = null )
ipc = new BeatmapIPCChannel ( importHost , this ) ;
}
/// <summary>
/// Import multiple <see cref="BeatmapSetInfo"/> from filesystem <paramref name="paths"/>.
/// </summary>
/// <param name="paths">Multiple locations on disk.</param>
public void Import ( params string [ ] paths )
{
foreach ( string path in paths )
{
try
{
2017-07-26 19:22:02 +08:00
using ( ArchiveReader reader = getReaderFrom ( path ) )
Import ( reader ) ;
2017-07-26 15:28:32 +08:00
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with beatmaps from default storage.
// Also, not always a single file, i.e. for LegacyFilesystemReader
// TODO: Add a check to prevent files from storage to be deleted.
try
{
File . Delete ( path ) ;
}
catch ( Exception e )
{
Logger . Error ( e , $@"Could not delete file at {path}" ) ;
}
}
catch ( Exception e )
{
e = e . InnerException ? ? e ;
Logger . Error ( e , @"Could not import beatmap set" ) ;
}
}
}
/// <summary>
/// Import a beatmap from an <see cref="ArchiveReader"/>.
/// </summary>
/// <param name="archiveReader">The beatmap to be imported.</param>
2017-07-26 19:22:02 +08:00
public BeatmapSetInfo Import ( ArchiveReader archiveReader )
2017-07-26 15:28:32 +08:00
{
BeatmapSetInfo set = importToStorage ( archiveReader ) ;
2017-07-27 14:34:13 +08:00
Import ( set ) ;
return set ;
}
2017-07-26 15:28:32 +08:00
2017-07-27 14:34:13 +08:00
/// <summary>
/// Import a beatmap from a <see cref="BeatmapSetInfo"/>.
/// </summary>
/// <param name="beatmapSetInfo">The beatmap to be imported.</param>
public void Import ( BeatmapSetInfo beatmapSetInfo )
{
// If we have an ID then we already exist in the database.
if ( beatmapSetInfo . ID ! = 0 ) return ;
2017-07-26 19:22:02 +08:00
2017-07-27 14:34:13 +08:00
beatmaps . Add ( beatmapSetInfo ) ;
2017-07-26 15:28:32 +08:00
}
/// <summary>
/// Delete a beatmap from the store.
2017-07-26 21:48:16 +08:00
/// Is a no-op for already deleted beatmaps.
2017-07-26 15:28:32 +08:00
/// </summary>
/// <param name="beatmapSet">The beatmap to delete.</param>
2017-07-26 19:22:02 +08:00
public void Delete ( BeatmapSetInfo beatmapSet )
{
2017-07-27 14:34:13 +08:00
if ( ! beatmaps . Delete ( beatmapSet ) ) return ;
2017-07-26 19:22:02 +08:00
if ( ! beatmapSet . Protected )
files . Dereference ( beatmapSet . Files ) ;
}
2017-07-26 21:48:16 +08:00
/// <summary>
/// Returns a <see cref="BeatmapSetInfo"/> to a usable state if it has previously been deleted but not yet purged.
/// Is a no-op for already usable beatmaps.
/// </summary>
/// <param name="beatmapSet">The beatmap to restore.</param>
2017-07-26 19:22:02 +08:00
public void Undelete ( BeatmapSetInfo beatmapSet )
{
2017-07-27 14:34:13 +08:00
if ( ! beatmaps . Undelete ( beatmapSet ) ) return ;
2017-07-26 19:22:02 +08:00
files . Reference ( beatmapSet . Files ) ;
}
2017-07-26 15:28:32 +08:00
2017-07-26 21:48:16 +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="previous">The currently loaded <see cref="WorkingBeatmap"/>. Allows for optimisation where elements are shared with the new beatmap.</param>
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
2017-07-26 15:28:32 +08:00
public WorkingBeatmap GetWorkingBeatmap ( BeatmapInfo beatmapInfo , WorkingBeatmap previous = null )
{
if ( beatmapInfo = = null | | beatmapInfo = = DefaultBeatmap ? . BeatmapInfo )
return DefaultBeatmap ;
2017-07-27 14:34:13 +08:00
beatmaps . Populate ( beatmapInfo ) ;
2017-07-26 15:28:32 +08:00
if ( beatmapInfo . BeatmapSet = = null )
throw new InvalidOperationException ( $@"Beatmap set {beatmapInfo.BeatmapSetInfoID} is not in the local database." ) ;
if ( beatmapInfo . Metadata = = null )
beatmapInfo . Metadata = beatmapInfo . BeatmapSet . Metadata ;
2017-07-26 19:22:02 +08:00
WorkingBeatmap working = new BeatmapStoreWorkingBeatmap ( files . Store , beatmapInfo ) ;
2017-07-26 15:28:32 +08:00
previous ? . TransferTo ( working ) ;
return working ;
}
/// <summary>
/// Reset the store to an empty state.
/// </summary>
public void Reset ( )
{
2017-07-27 14:34:13 +08:00
beatmaps . Reset ( ) ;
2017-07-26 15:28:32 +08:00
}
2017-07-26 22:13:02 +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 ( Func < BeatmapSetInfo , bool > query )
{
2017-07-27 14:34:13 +08:00
BeatmapSetInfo set = beatmaps . Query < BeatmapSetInfo > ( ) . FirstOrDefault ( query ) ;
if ( set ! = null )
beatmaps . Populate ( set ) ;
return set ;
}
/// <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 List < BeatmapSetInfo > QueryBeatmapSets ( Expression < Func < BeatmapSetInfo , bool > > query ) = > beatmaps . QueryAndPopulate ( 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 ( Func < BeatmapInfo , bool > query )
{
BeatmapInfo set = beatmaps . Query < BeatmapInfo > ( ) . FirstOrDefault ( query ) ;
2017-07-26 22:13:02 +08:00
if ( set ! = null )
2017-07-27 14:34:13 +08:00
beatmaps . Populate ( set ) ;
2017-07-26 22:13:02 +08:00
return set ;
}
2017-07-27 14:34:13 +08:00
/// <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>
public List < BeatmapInfo > QueryBeatmaps ( Expression < Func < BeatmapInfo , bool > > query ) = > beatmaps . QueryAndPopulate ( query ) ;
2017-07-26 21:48:16 +08:00
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
/// <param name="path">A file or folder path resolving the beatmap content.</param>
/// <returns>A reader giving access to the beatmap's content.</returns>
2017-07-26 19:22:02 +08:00
private ArchiveReader getReaderFrom ( string path )
{
if ( ZipFile . IsZipFile ( path ) )
return new OszArchiveReader ( storage . GetStream ( path ) ) ;
else
return new LegacyFilesystemReader ( path ) ;
}
2017-07-26 21:48:16 +08:00
/// <summary>
/// Import a beamap into our local <see cref="FileDatabase"/> storage.
/// If the beatmap is already imported, the existing instance will be returned.
/// </summary>
/// <param name="reader">The beatmap archive to be read.</param>
/// <returns>The imported beatmap, or an existing instance if it is already present.</returns>
private BeatmapSetInfo importToStorage ( ArchiveReader reader )
2017-07-26 15:28:32 +08:00
{
BeatmapMetadata metadata ;
2017-07-26 21:48:16 +08:00
using ( var stream = new StreamReader ( reader . GetStream ( reader . Filenames . First ( f = > f . EndsWith ( ".osu" ) ) ) ) )
2017-07-26 15:28:32 +08:00
metadata = BeatmapDecoder . GetDecoder ( stream ) . Decode ( stream ) . Metadata ;
2017-07-26 19:22:02 +08:00
MemoryStream hashable = new MemoryStream ( ) ;
List < FileInfo > fileInfos = new List < FileInfo > ( ) ;
2017-07-26 15:28:32 +08:00
2017-07-26 21:48:16 +08:00
foreach ( string file in reader . Filenames )
2017-07-26 15:28:32 +08:00
{
2017-07-26 21:48:16 +08:00
using ( Stream s = reader . GetStream ( file ) )
2017-07-26 19:22:02 +08:00
{
fileInfos . Add ( files . Add ( s , file ) ) ;
s . CopyTo ( hashable ) ;
}
2017-07-26 15:28:32 +08:00
}
2017-07-26 19:22:02 +08:00
var overallHash = hashable . GetMd5Hash ( ) ;
2017-07-27 14:34:13 +08:00
var existing = beatmaps . Query < BeatmapSetInfo > ( ) . FirstOrDefault ( b = > b . Hash = = overallHash ) ;
2017-07-26 15:28:32 +08:00
if ( existing ! = null )
{
2017-07-27 14:34:13 +08:00
beatmaps . Populate ( existing ) ;
2017-07-26 19:22:02 +08:00
Undelete ( existing ) ;
2017-07-26 15:28:32 +08:00
return existing ;
}
var beatmapSet = new BeatmapSetInfo
{
OnlineBeatmapSetID = metadata . OnlineBeatmapSetID ,
Beatmaps = new List < BeatmapInfo > ( ) ,
2017-07-26 19:22:02 +08:00
Hash = overallHash ,
Files = fileInfos ,
2017-07-26 15:28:32 +08:00
Metadata = metadata
} ;
2017-07-26 21:48:16 +08:00
var mapNames = reader . Filenames . Where ( f = > f . EndsWith ( ".osu" ) ) ;
2017-07-26 19:22:02 +08:00
foreach ( var name in mapNames )
2017-07-26 15:28:32 +08:00
{
2017-07-26 21:48:16 +08:00
using ( var raw = reader . GetStream ( name ) )
2017-07-26 19:22:02 +08:00
using ( var ms = new MemoryStream ( ) ) //we need a memory stream so we can seek and shit
using ( var sr = new StreamReader ( ms ) )
{
raw . CopyTo ( ms ) ;
ms . Position = 0 ;
2017-07-26 15:28:32 +08:00
2017-07-26 19:22:02 +08:00
var decoder = BeatmapDecoder . GetDecoder ( sr ) ;
Beatmap beatmap = decoder . Decode ( sr ) ;
2017-07-26 15:28:32 +08:00
2017-07-26 19:22:02 +08:00
beatmap . BeatmapInfo . Path = name ;
beatmap . BeatmapInfo . Hash = ms . GetMd5Hash ( ) ;
2017-07-26 15:28:32 +08:00
2017-07-26 19:22:02 +08:00
// TODO: Diff beatmap metadata with set metadata and leave it here if necessary
beatmap . BeatmapInfo . Metadata = null ;
2017-07-26 15:28:32 +08:00
2017-07-26 19:22:02 +08:00
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap . BeatmapInfo . Ruleset = rulesets . Query < RulesetInfo > ( ) . FirstOrDefault ( r = > r . ID = = beatmap . BeatmapInfo . RulesetID ) ;
beatmap . BeatmapInfo . StarDifficulty = rulesets . Query < RulesetInfo > ( ) . FirstOrDefault ( r = > r . ID = = beatmap . BeatmapInfo . RulesetID ) ? . CreateInstance ( ) ? . CreateDifficultyCalculator ( beatmap )
. Calculate ( ) ? ? 0 ;
2017-07-26 15:28:32 +08:00
2017-07-26 19:22:02 +08:00
beatmapSet . Beatmaps . Add ( beatmap . BeatmapInfo ) ;
}
2017-07-26 15:28:32 +08:00
}
return beatmapSet ;
}
2017-07-27 14:34:13 +08:00
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <param name="populate">Whether returned objects should be pre-populated with all data.</param>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List < BeatmapSetInfo > GetAllUsableBeatmapSets ( bool populate = true )
{
if ( populate )
return beatmaps . QueryAndPopulate < BeatmapSetInfo > ( b = > ! b . DeletePending ) . ToList ( ) ;
else
return beatmaps . Query < BeatmapSetInfo > ( b = > ! b . DeletePending ) . ToList ( ) ;
}
2017-07-26 15:28:32 +08:00
}
}