Merge remote-tracking branch 'upstream/master' into default-approachrate

This commit is contained in:
Dean Herbert
2018-02-13 20:23:01 +09:00
69 changed files with 1297 additions and 1104 deletions

View File

@ -51,5 +51,17 @@ namespace osu.Game.Beatmaps
return mid - (mid - min) * (5 - difficulty) / 5;
return mid;
}
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// </summary>
/// <param name="difficulty">The difficulty value to be mapped.</param>
/// <param name="range">The values that define the two linear ranges.</param>
/// <param name="range.od0">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
/// <param name="range.od5">Midpoint of the resulting range which will be achieved by a difficulty value of 5.</param>
/// <param name="range.od10">Maximum of the resulting range which will be achieved by a difficulty value of 10.</param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range)
=> DifficultyRange(difficulty, range.od0, range.od5, range.od10);
}
}

View File

@ -9,31 +9,26 @@ using System.Linq.Expressions;
using System.Threading.Tasks;
using Ionic.Zip;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.IO;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Textures;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Storyboards;
namespace osu.Game.Beatmaps
{
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
public class BeatmapManager
public partial class BeatmapManager
{
/// <summary>
/// Fired when a new <see cref="BeatmapSetInfo"/> becomes available in the database.
@ -65,19 +60,7 @@ namespace osu.Game.Beatmaps
/// </summary>
public WorkingBeatmap DefaultBeatmap { private get; set; }
private readonly Storage storage;
private BeatmapStore createBeatmapStore(Func<OsuDbContext> context)
{
var store = new BeatmapStore(context);
store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s);
store.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s);
store.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
store.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
return store;
}
private readonly Func<OsuDbContext> createContext;
private readonly IDatabaseContextFactory contextFactory;
private readonly FileStore files;
@ -102,31 +85,19 @@ namespace osu.Game.Beatmaps
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
private void refreshImportContext()
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
{
lock (importContextLock)
{
importContext?.Value?.Dispose();
this.contextFactory = contextFactory;
importContext = new Lazy<OsuDbContext>(() =>
{
var c = createContext();
c.Database.AutoTransactionsEnabled = false;
return c;
});
}
}
beatmaps = new BeatmapStore(contextFactory);
public BeatmapManager(Storage storage, Func<OsuDbContext> context, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
{
createContext = context;
beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s);
beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s);
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
refreshImportContext();
files = new FileStore(contextFactory, storage);
beatmaps = createBeatmapStore(context);
files = new FileStore(context, storage);
this.storage = files.Storage;
this.rulesets = rulesets;
this.api = api;
@ -138,10 +109,10 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Import one or more <see cref="BeatmapSetInfo"/> from filesystem <paramref name="paths"/>.
/// This will post a notification tracking import progress.
/// This will post notifications tracking progress.
/// </summary>
/// <param name="paths">One or more beatmap locations on disk.</param>
public void Import(params string[] paths)
public List<BeatmapSetInfo> Import(params string[] paths)
{
var notification = new ProgressNotification
{
@ -153,18 +124,20 @@ namespace osu.Game.Beatmaps
PostNotification?.Invoke(notification);
List<BeatmapSetInfo> imported = new List<BeatmapSetInfo>();
int i = 0;
foreach (string path in paths)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
return imported;
try
{
notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}";
using (ArchiveReader reader = getReaderFrom(path))
Import(reader);
imported.Add(Import(reader));
notification.Progress = (float)++i / paths.Length;
@ -186,62 +159,66 @@ namespace osu.Game.Beatmaps
{
e = e.InnerException ?? e;
Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})");
refreshImportContext();
}
}
notification.State = ProgressNotificationState.Completed;
return imported;
}
private readonly object importContextLock = new object();
private Lazy<OsuDbContext> importContext;
/// <summary>
/// Import a beatmap from an <see cref="ArchiveReader"/>.
/// </summary>
/// <param name="archiveReader">The beatmap to be imported.</param>
public BeatmapSetInfo Import(ArchiveReader archiveReader)
/// <param name="archive">The beatmap to be imported.</param>
public BeatmapSetInfo Import(ArchiveReader archive)
{
// let's only allow one concurrent import at a time for now.
lock (importContextLock)
using (contextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
{
var context = importContext.Value;
// create a new set info (don't yet add to database)
var beatmapSet = createBeatmapSetInfo(archive);
using (var transaction = context.BeginTransaction())
// check if this beatmap has already been imported and exit early if so
var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == beatmapSet.Hash);
if (existingHashMatch != null)
{
// create local stores so we can isolate and thread safely, and share a context/transaction.
var iFiles = new FileStore(() => context, storage);
var iBeatmaps = createBeatmapStore(() => context);
BeatmapSetInfo set = importToStorage(iFiles, iBeatmaps, archiveReader);
if (set.ID == 0)
{
iBeatmaps.Add(set);
context.SaveChanges();
}
context.SaveChanges(transaction);
return set;
Undelete(existingHashMatch);
return existingHashMatch;
}
// check if a set already exists with the same online id
if (beatmapSet.OnlineBeatmapSetID != null)
{
var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
if (existingOnlineId != null)
{
Delete(existingOnlineId);
beatmaps.Cleanup(s => s.ID == existingOnlineId.ID);
}
}
beatmapSet.Files = createFileInfos(archive, files);
beatmapSet.Beatmaps = createBeatmapDifficulties(archive);
// remove metadata from difficulties where it matches the set
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
if (beatmapSet.Metadata.Equals(b.Metadata))
b.Metadata = null;
// import to beatmap store
Import(beatmapSet);
return beatmapSet;
}
}
/// <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;
createBeatmapStore(createContext).Add(beatmapSetInfo);
}
/// <param name="beatmapSet">The beatmap to be imported.</param>
public void Import(BeatmapSetInfo beatmapSet) => beatmaps.Add(beatmapSet);
/// <summary>
/// Downloads a beatmap.
/// This will post notifications tracking progress.
/// </summary>
/// <param name="beatmapSetInfo">The <see cref="BeatmapSetInfo"/> to be downloaded.</param>
/// <param name="noVideo">Whether the beatmap should be downloaded without video. Defaults to false.</param>
@ -323,6 +300,12 @@ namespace osu.Game.Beatmaps
/// <returns>The <see cref="DownloadBeatmapSetRequest"/> object if it exists, or null.</returns>
public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID);
/// <summary>
/// Update a BeatmapSetInfo with all changes. TODO: This only supports very basic updates currently.
/// </summary>
/// <param name="beatmapSet">The beatmap set to update.</param>
public void Update(BeatmapSetInfo beatmap) => beatmaps.Update(beatmap);
/// <summary>
/// Delete a beatmap from the manager.
/// Is a no-op for already deleted beatmaps.
@ -330,33 +313,29 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapSet">The beatmap set to delete.</param>
public void Delete(BeatmapSetInfo beatmapSet)
{
lock (importContextLock)
using (var usage = contextFactory.GetForWrite())
{
var context = importContext.Value;
var context = usage.Context;
using (var transaction = context.BeginTransaction())
context.ChangeTracker.AutoDetectChangesEnabled = false;
// re-fetch the beatmap set on the import context.
beatmapSet = context.BeatmapSetInfo.Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == beatmapSet.ID);
if (beatmaps.Delete(beatmapSet))
{
context.ChangeTracker.AutoDetectChangesEnabled = false;
// re-fetch the beatmap set on the import context.
beatmapSet = context.BeatmapSetInfo.Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == beatmapSet.ID);
// create local stores so we can isolate and thread safely, and share a context/transaction.
var iFiles = new FileStore(() => context, storage);
var iBeatmaps = createBeatmapStore(() => context);
if (iBeatmaps.Delete(beatmapSet))
{
if (!beatmapSet.Protected)
iFiles.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
}
context.ChangeTracker.AutoDetectChangesEnabled = true;
context.SaveChanges(transaction);
if (!beatmapSet.Protected)
files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
}
context.ChangeTracker.AutoDetectChangesEnabled = true;
}
}
/// <summary>
/// Restore all beatmaps that were previously deleted.
/// This will post notifications tracking progress.
/// </summary>
public void UndeleteAll()
{
var deleteMaps = QueryBeatmapSets(bs => bs.DeletePending).ToList();
@ -388,27 +367,25 @@ namespace osu.Game.Beatmaps
notification.State = ProgressNotificationState.Completed;
}
/// <summary>
/// Restore a beatmap that was previously deleted. Is a no-op if the beatmap is not in a deleted state, or has its protected flag set.
/// </summary>
/// <param name="beatmapSet">The beatmap to restore</param>
public void Undelete(BeatmapSetInfo beatmapSet)
{
if (beatmapSet.Protected)
return;
lock (importContextLock)
using (var usage = contextFactory.GetForWrite())
{
var context = importContext.Value;
usage.Context.ChangeTracker.AutoDetectChangesEnabled = false;
using (var transaction = context.BeginTransaction())
{
context.ChangeTracker.AutoDetectChangesEnabled = false;
if (!beatmaps.Undelete(beatmapSet)) return;
var iFiles = new FileStore(() => context, storage);
var iBeatmaps = createBeatmapStore(() => context);
if (!beatmapSet.Protected)
files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
undelete(iBeatmaps, iFiles, beatmapSet);
context.ChangeTracker.AutoDetectChangesEnabled = true;
context.SaveChanges(transaction);
}
usage.Context.ChangeTracker.AutoDetectChangesEnabled = true;
}
}
@ -424,21 +401,6 @@ namespace osu.Game.Beatmaps
/// <param name="beatmap">The beatmap difficulty to restore.</param>
public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap);
/// <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="beatmaps">The store to restore beatmaps from.</param>
/// <param name="files">The store to restore beatmap files from.</param>
/// <param name="beatmapSet">The beatmap to restore.</param>
private void undelete(BeatmapStore beatmaps, FileStore files, BeatmapSetInfo beatmapSet)
{
if (!beatmaps.Undelete(beatmapSet)) return;
if (!beatmapSet.Protected)
files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
}
/// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
/// </summary>
@ -474,6 +436,12 @@ namespace osu.Game.Beatmaps
/// <returns>A fresh instance.</returns>
public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID);
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List<BeatmapSetInfo> GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList();
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
/// </summary>
@ -496,220 +464,8 @@ namespace osu.Game.Beatmaps
public IEnumerable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// Denotes whether an osu-stable installation is present to perform automated imports from.
/// </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>
private ArchiveReader getReaderFrom(string path)
{
if (ZipFile.IsZipFile(path))
// ReSharper disable once InconsistentlySynchronizedField
return new OszArchiveReader(storage.GetStream(path));
return new LegacyFilesystemReader(path);
}
/// <summary>
/// Import a beamap into our local <see cref="FileStore"/> storage.
/// If the beatmap is already imported, the existing instance will be returned.
/// </summary>
/// <param name="files">The store to import beatmap files to.</param>
/// <param name="beatmaps">The store to import beatmaps to.</param>
/// <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(FileStore files, BeatmapStore beatmaps, ArchiveReader reader)
{
// let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
if (string.IsNullOrEmpty(mapName))
throw new InvalidOperationException("No beatmap files found in the map folder.");
// for now, concatenate all .osu files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu")))
using (Stream s = reader.GetStream(file))
s.CopyTo(hashable);
var hash = hashable.ComputeSHA2Hash();
// check if this beatmap has already been imported and exit early if so.
var beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == hash);
if (beatmapSet != null)
{
undelete(beatmaps, files, beatmapSet);
// ensure all files are present and accessible
foreach (var f in beatmapSet.Files)
{
if (!storage.Exists(f.FileInfo.StoragePath))
using (Stream s = reader.GetStream(f.Filename))
files.Add(s, false);
}
// todo: delete any files which shouldn't exist any more.
return beatmapSet;
}
List<BeatmapSetFileInfo> fileInfos = new List<BeatmapSetFileInfo>();
// import files to manager
foreach (string file in reader.Filenames)
using (Stream s = reader.GetStream(file))
fileInfos.Add(new BeatmapSetFileInfo
{
Filename = file,
FileInfo = files.Add(s)
});
BeatmapMetadata metadata;
using (var stream = new StreamReader(reader.GetStream(mapName)))
metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata;
// check if a set already exists with the same online id.
if (metadata.OnlineBeatmapSetID != null)
beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == metadata.OnlineBeatmapSetID);
if (beatmapSet == null)
beatmapSet = new BeatmapSetInfo
{
OnlineBeatmapSetID = metadata.OnlineBeatmapSetID,
Beatmaps = new List<BeatmapInfo>(),
Hash = hash,
Files = fileInfos,
Metadata = metadata
};
var mapNames = reader.Filenames.Where(f => f.EndsWith(".osu"));
foreach (var name in mapNames)
{
using (var raw = reader.GetStream(name))
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;
var decoder = Decoder.GetDecoder(sr);
Beatmap beatmap = decoder.DecodeBeatmap(sr);
beatmap.BeatmapInfo.Path = name;
beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash();
beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash();
var existing = beatmaps.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.BeatmapInfo.Hash || beatmap.BeatmapInfo.OnlineBeatmapID != null && b.OnlineBeatmapID == beatmap.BeatmapInfo.OnlineBeatmapID);
if (existing == null)
{
// Exclude beatmap-metadata if it's equal to beatmapset-metadata
if (metadata.Equals(beatmap.Metadata))
beatmap.BeatmapInfo.Metadata = null;
RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.Ruleset = ruleset;
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0;
beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo);
}
}
}
return beatmapSet;
}
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
{
return beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList();
}
protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{
private readonly IResourceStore<byte[]> store;
public BeatmapManagerWorkingBeatmap(IResourceStore<byte[]> store, BeatmapInfo beatmapInfo)
: base(beatmapInfo)
{
this.store = store;
}
protected override Beatmap GetBeatmap()
{
try
{
using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
{
Decoder decoder = Decoder.GetDecoder(stream);
return decoder.DecodeBeatmap(stream);
}
}
catch
{
return null;
}
}
private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath;
protected override Texture GetBackground()
{
if (Metadata?.BackgroundFile == null)
return null;
try
{
return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile));
}
catch
{
return null;
}
}
protected override Track GetTrack()
{
try
{
var trackData = store.GetStream(getPathForFile(Metadata.AudioFile));
return trackData == null ? null : new TrackBass(trackData);
}
catch
{
return new TrackVirtual();
}
}
protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile)));
protected override Storyboard GetStoryboard()
{
try
{
using (var beatmap = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
{
Decoder decoder = Decoder.GetDecoder(beatmap);
if (BeatmapSetInfo?.StoryboardFile == null)
return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap);
using (var storyboard = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile))))
return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, storyboard);
}
}
catch
{
return new Storyboard();
}
}
}
public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null;
/// <summary>
@ -728,6 +484,10 @@ namespace osu.Game.Beatmaps
await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning);
}
/// <summary>
/// Delete all beatmaps.
/// This will post notifications tracking progress.
/// </summary>
public void DeleteAll()
{
var maps = GetAllUsableBeatmapSets();
@ -758,5 +518,109 @@ namespace osu.Game.Beatmaps
notification.State = ProgressNotificationState.Completed;
}
/// <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>
private ArchiveReader getReaderFrom(string path)
{
if (ZipFile.IsZipFile(path))
// ReSharper disable once InconsistentlySynchronizedField
return new OszArchiveReader(files.Storage.GetStream(path));
return new LegacyFilesystemReader(path);
}
/// <summary>
/// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
/// </summary>
private string computeBeatmapSetHash(ArchiveReader reader)
{
// for now, concatenate all .osu files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu")))
using (Stream s = reader.GetStream(file))
s.CopyTo(hashable);
return hashable.ComputeSHA2Hash();
}
/// <summary>
/// Create a <see cref="BeatmapSetInfo"/> from a provided archive.
/// </summary>
private BeatmapSetInfo createBeatmapSetInfo(ArchiveReader reader)
{
// let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in the map folder.");
BeatmapMetadata metadata;
using (var stream = new StreamReader(reader.GetStream(mapName)))
metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata;
return new BeatmapSetInfo
{
OnlineBeatmapSetID = metadata.OnlineBeatmapSetID,
Beatmaps = new List<BeatmapInfo>(),
Hash = computeBeatmapSetHash(reader),
Metadata = metadata
};
}
/// <summary>
/// Create all required <see cref="FileInfo"/>s for the provided archive, adding them to the global file store.
/// </summary>
private List<BeatmapSetFileInfo> createFileInfos(ArchiveReader reader, FileStore files)
{
List<BeatmapSetFileInfo> fileInfos = new List<BeatmapSetFileInfo>();
// import files to manager
foreach (string file in reader.Filenames)
using (Stream s = reader.GetStream(file))
fileInfos.Add(new BeatmapSetFileInfo
{
Filename = file,
FileInfo = files.Add(s)
});
return fileInfos;
}
/// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary>
private List<BeatmapInfo> createBeatmapDifficulties(ArchiveReader reader)
{
var beatmapInfos = new List<BeatmapInfo>();
foreach (var name in reader.Filenames.Where(f => f.EndsWith(".osu")))
{
using (var raw = reader.GetStream(name))
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;
var decoder = Decoder.GetDecoder(sr);
Beatmap beatmap = decoder.DecodeBeatmap(sr);
beatmap.BeatmapInfo.Path = name;
beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash();
beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash();
RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.Ruleset = ruleset;
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0;
beatmapInfos.Add(beatmap.BeatmapInfo);
}
}
return beatmapInfos;
}
}
}

View File

@ -0,0 +1,98 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.IO;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps.Formats;
using osu.Game.Graphics.Textures;
using osu.Game.Storyboards;
namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{
private readonly IResourceStore<byte[]> store;
public BeatmapManagerWorkingBeatmap(IResourceStore<byte[]> store, BeatmapInfo beatmapInfo)
: base(beatmapInfo)
{
this.store = store;
}
protected override Beatmap GetBeatmap()
{
try
{
using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
{
Decoder decoder = Decoder.GetDecoder(stream);
return decoder.DecodeBeatmap(stream);
}
}
catch
{
return null;
}
}
private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath;
protected override Texture GetBackground()
{
if (Metadata?.BackgroundFile == null)
return null;
try
{
return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile));
}
catch
{
return null;
}
}
protected override Track GetTrack()
{
try
{
var trackData = store.GetStream(getPathForFile(Metadata.AudioFile));
return trackData == null ? null : new TrackBass(trackData);
}
catch
{
return new TrackVirtual();
}
}
protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile)));
protected override Storyboard GetStoryboard()
{
try
{
using (var beatmap = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
{
Decoder decoder = Decoder.GetDecoder(beatmap);
if (BeatmapSetInfo?.StoryboardFile == null)
return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap);
using (var storyboard = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile))))
return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, storyboard);
}
}
catch
{
return new Storyboard();
}
}
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using osu.Game.Database;
@ -19,7 +20,7 @@ namespace osu.Game.Beatmaps
public event Action<BeatmapInfo> BeatmapHidden;
public event Action<BeatmapInfo> BeatmapRestored;
public BeatmapStore(Func<OsuDbContext> factory)
public BeatmapStore(IDatabaseContextFactory factory)
: base(factory)
{
}
@ -30,22 +31,38 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapSet">The beatmap to add.</param>
public void Add(BeatmapSetInfo beatmapSet)
{
var context = GetContext();
foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null))
using (var usage = ContextFactory.GetForWrite())
{
// If we detect a new metadata object it'll be attached to the current context so it can be reused
// to prevent duplicate entries when persisting. To accomplish this we look in the cache (.Local)
// of the corresponding table (.Set<BeatmapMetadata>()) for matching entries to our criteria.
var contextMetadata = context.Set<BeatmapMetadata>().Local.SingleOrDefault(e => e.Equals(beatmap.Metadata));
if (contextMetadata != null)
beatmap.Metadata = contextMetadata;
else
context.BeatmapMetadata.Attach(beatmap.Metadata);
}
var context = usage.Context;
context.BeatmapSetInfo.Attach(beatmapSet);
context.SaveChanges();
foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null))
{
// If we detect a new metadata object it'll be attached to the current context so it can be reused
// to prevent duplicate entries when persisting. To accomplish this we look in the cache (.Local)
// of the corresponding table (.Set<BeatmapMetadata>()) for matching entries to our criteria.
var contextMetadata = context.Set<BeatmapMetadata>().Local.SingleOrDefault(e => e.Equals(beatmap.Metadata));
if (contextMetadata != null)
beatmap.Metadata = contextMetadata;
else
context.BeatmapMetadata.Attach(beatmap.Metadata);
}
context.BeatmapSetInfo.Attach(beatmapSet);
BeatmapSetAdded?.Invoke(beatmapSet);
}
}
/// <summary>
/// Update a <see cref="BeatmapSetInfo"/> in the database. TODO: This only supports very basic updates currently.
/// </summary>
/// <param name="beatmapSet">The beatmap to update.</param>
public void Update(BeatmapSetInfo beatmapSet)
{
BeatmapSetRemoved?.Invoke(beatmapSet);
using (var usage = ContextFactory.GetForWrite())
usage.Context.BeatmapSetInfo.Update(beatmapSet);
BeatmapSetAdded?.Invoke(beatmapSet);
}
@ -57,13 +74,14 @@ namespace osu.Game.Beatmaps
/// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns>
public bool Delete(BeatmapSetInfo beatmapSet)
{
var context = GetContext();
using (ContextFactory.GetForWrite())
{
Refresh(ref beatmapSet, BeatmapSets);
Refresh(ref beatmapSet, BeatmapSets);
if (beatmapSet.DeletePending) return false;
if (beatmapSet.DeletePending) return false;
beatmapSet.DeletePending = true;
context.SaveChanges();
beatmapSet.DeletePending = true;
}
BeatmapSetRemoved?.Invoke(beatmapSet);
return true;
@ -76,13 +94,14 @@ namespace osu.Game.Beatmaps
/// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns>
public bool Undelete(BeatmapSetInfo beatmapSet)
{
var context = GetContext();
using (ContextFactory.GetForWrite())
{
Refresh(ref beatmapSet, BeatmapSets);
Refresh(ref beatmapSet, BeatmapSets);
if (!beatmapSet.DeletePending) return false;
if (!beatmapSet.DeletePending) return false;
beatmapSet.DeletePending = false;
context.SaveChanges();
beatmapSet.DeletePending = false;
}
BeatmapSetAdded?.Invoke(beatmapSet);
return true;
@ -95,15 +114,17 @@ namespace osu.Game.Beatmaps
/// <returns>Whether the beatmap's <see cref="BeatmapInfo.Hidden"/> was changed.</returns>
public bool Hide(BeatmapInfo beatmap)
{
var context = GetContext();
using (ContextFactory.GetForWrite())
{
Refresh(ref beatmap, Beatmaps);
Refresh(ref beatmap, Beatmaps);
if (beatmap.Hidden) return false;
if (beatmap.Hidden) return false;
beatmap.Hidden = true;
context.SaveChanges();
beatmap.Hidden = true;
BeatmapHidden?.Invoke(beatmap);
}
BeatmapHidden?.Invoke(beatmap);
return true;
}
@ -114,47 +135,55 @@ namespace osu.Game.Beatmaps
/// <returns>Whether the beatmap's <see cref="BeatmapInfo.Hidden"/> was changed.</returns>
public bool Restore(BeatmapInfo beatmap)
{
var context = GetContext();
using (ContextFactory.GetForWrite())
{
Refresh(ref beatmap, Beatmaps);
Refresh(ref beatmap, Beatmaps);
if (!beatmap.Hidden) return false;
if (!beatmap.Hidden) return false;
beatmap.Hidden = false;
context.SaveChanges();
beatmap.Hidden = false;
}
BeatmapRestored?.Invoke(beatmap);
return true;
}
public override void Cleanup()
public override void Cleanup() => Cleanup(_ => true);
public void Cleanup(Expression<Func<BeatmapSetInfo, bool>> query)
{
var context = GetContext();
using (var usage = ContextFactory.GetForWrite())
{
var context = usage.Context;
var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Metadata);
var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected)
.Where(query)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Metadata).ToList();
// metadata is M-N so we can't rely on cascades
context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata));
context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null)));
if (!purgeable.Any()) return;
// todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly.
context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty)));
// metadata is M-N so we can't rely on cascades
context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata));
context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null)));
// cascades down to beatmaps.
context.BeatmapSetInfo.RemoveRange(purgeable);
context.SaveChanges();
// todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly.
context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty)));
// cascades down to beatmaps.
context.BeatmapSetInfo.RemoveRange(purgeable);
}
}
public IQueryable<BeatmapSetInfo> BeatmapSets => GetContext().BeatmapSetInfo
public IQueryable<BeatmapSetInfo> BeatmapSets => ContextFactory.Get().BeatmapSetInfo
.Include(s => s.Metadata)
.Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
.Include(s => s.Files).ThenInclude(f => f.FileInfo);
public IQueryable<BeatmapInfo> Beatmaps => GetContext().BeatmapInfo
public IQueryable<BeatmapInfo> Beatmaps => ContextFactory.Get().BeatmapInfo
.Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
.Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo)
.Include(b => b.Metadata)