osu/osu.Game/Database/BeatmapDatabase.cs

289 lines
11 KiB
C#
Raw Normal View History

// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
2016-12-06 09:56:20 +00:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
2017-03-04 10:02:36 +00:00
using osu.Framework.Extensions;
2017-02-24 08:08:13 +00:00
using osu.Framework.Logging;
2016-10-10 13:20:06 +00:00
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.IO;
using osu.Game.IPC;
2016-10-18 17:35:01 +00:00
using SQLite.Net;
using SQLiteNetExtensions.Extensions;
2016-10-04 15:31:10 +00:00
namespace osu.Game.Database
{
public class BeatmapDatabase : Database
{
2017-04-17 10:44:03 +00:00
private readonly RulesetDatabase rulesets;
public event Action<BeatmapSetInfo> BeatmapSetAdded;
public event Action<BeatmapSetInfo> BeatmapSetRemoved;
2017-03-07 01:59:19 +00:00
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
2017-03-04 09:51:16 +00:00
private BeatmapIPCChannel ipc;
2017-04-17 10:44:03 +00:00
public BeatmapDatabase(Storage storage, SQLiteConnection connection, RulesetDatabase rulesets, IIpcHost importHost = null) : base(storage, connection)
{
2017-04-17 10:44:03 +00:00
this.rulesets = rulesets;
if (importHost != null)
2017-03-04 09:51:16 +00:00
ipc = new BeatmapIPCChannel(importHost, this);
}
2016-10-18 17:35:01 +00:00
2017-02-24 08:08:13 +00:00
private void deletePending()
{
2017-05-06 07:37:53 +00:00
foreach (var b in GetAllWithChildren<BeatmapSetInfo>(b => b.DeletePending))
2017-02-24 08:08:13 +00:00
{
try
{
Storage.Delete(b.Path);
foreach (var i in b.Beatmaps)
{
if (i.Metadata != null) Connection.Delete(i.Metadata);
if (i.Difficulty != null) Connection.Delete(i.Difficulty);
Connection.Delete(i);
}
if (b.Metadata != null) Connection.Delete(b.Metadata);
Connection.Delete(b);
2017-02-24 08:08:13 +00:00
}
catch (Exception e)
{
2017-03-07 01:59:19 +00:00
Logger.Error(e, $@"Could not delete beatmap {b}");
2017-02-24 08:08:13 +00:00
}
}
//this is required because sqlite migrations don't work, initially inserting nulls into this field.
//see https://github.com/praeclarum/sqlite-net/issues/326
Connection.Query<BeatmapSetInfo>("UPDATE BeatmapSetInfo SET DeletePending = 0 WHERE DeletePending IS NULL");
2017-02-24 08:08:13 +00:00
}
2017-04-17 10:44:03 +00:00
protected override void Prepare(bool reset = false)
{
Connection.CreateTable<BeatmapMetadata>();
Connection.CreateTable<BeatmapDifficulty>();
Connection.CreateTable<BeatmapSetInfo>();
Connection.CreateTable<BeatmapInfo>();
2017-04-17 10:44:03 +00:00
if (reset)
{
Storage.DeleteDatabase(@"beatmaps");
2017-04-17 10:44:03 +00:00
foreach (var setInfo in Query<BeatmapSetInfo>())
{
if (Storage.Exists(setInfo.Path))
Storage.Delete(setInfo.Path);
}
2017-04-17 08:43:48 +00:00
2017-04-17 10:44:03 +00:00
Connection.DeleteAll<BeatmapMetadata>();
Connection.DeleteAll<BeatmapDifficulty>();
Connection.DeleteAll<BeatmapSetInfo>();
Connection.DeleteAll<BeatmapInfo>();
}
2016-10-21 08:01:46 +00:00
2017-04-17 10:44:03 +00:00
deletePending();
2016-10-21 08:01:46 +00:00
}
2017-04-17 08:43:48 +00:00
protected override Type[] ValidTypes => new[] {
typeof(BeatmapSetInfo),
typeof(BeatmapInfo),
typeof(BeatmapMetadata),
typeof(BeatmapDifficulty),
};
2017-03-02 14:37:45 +00:00
/// <summary>
/// Import multiple <see cref="BeatmapSetInfo"/> from <paramref name="paths"/>.
/// </summary>
/// <param name="paths">Multiple locations on disk</param>
public void Import(IEnumerable<string> paths)
{
foreach (string p in paths)
2017-03-17 09:57:24 +00:00
{
try
{
BeatmapSetInfo set = getBeatmapSet(p);
2017-03-03 11:51:07 +00:00
//If we have an ID then we already exist in the database.
2017-03-02 12:36:01 +00:00
if (set.ID == 0)
2017-03-17 09:57:24 +00:00
Import(new[] { set });
2017-02-28 13:46:16 +00: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.
2017-03-02 12:39:02 +00:00
// 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(p);
}
catch (Exception e)
{
Logger.Error(e, $@"Could not delete file at {p}");
}
}
2017-02-28 13:46:16 +00:00
catch (Exception e)
{
2017-03-02 13:10:32 +00:00
e = e.InnerException ?? e;
2017-03-07 01:59:19 +00:00
Logger.Error(e, @"Could not import beatmap set");
2017-02-28 13:46:16 +00:00
}
2017-03-17 09:57:24 +00:00
}
}
2017-03-02 14:37:45 +00:00
/// <summary>
/// Import <see cref="BeatmapSetInfo"/> from <paramref name="path"/>.
/// </summary>
/// <param name="path">Location on disk</param>
public void Import(string path)
{
2017-03-04 10:02:36 +00:00
Import(new[] { path });
}
/// <summary>
/// Duplicates content from <paramref name="path"/> to storage and returns a representing <see cref="BeatmapSetInfo"/>.
/// </summary>
/// <param name="path">Content location</param>
2017-03-02 12:39:02 +00:00
/// <returns><see cref="BeatmapSetInfo"/></returns>
private BeatmapSetInfo getBeatmapSet(string path)
{
string hash = null;
BeatmapMetadata metadata;
using (var reader = ArchiveReader.GetReader(Storage, path))
{
using (var stream = new StreamReader(reader.GetStream(reader.BeatmapFilenames[0])))
metadata = BeatmapDecoder.GetDecoder(stream).Decode(stream).Metadata;
}
if (File.Exists(path)) // Not always the case, i.e. for LegacyFilesystemReader
{
using (var input = Storage.GetStream(path))
{
2017-03-04 10:02:36 +00:00
hash = input.GetMd5Hash();
input.Seek(0, SeekOrigin.Begin);
path = Path.Combine(@"beatmaps", hash.Remove(1), hash.Remove(2), hash);
if (!Storage.Exists(path))
using (var output = Storage.GetStream(path, FileAccess.Write))
input.CopyTo(output);
}
}
var existing = Connection.Table<BeatmapSetInfo>().FirstOrDefault(b => b.Hash == hash);
if (existing != null)
{
if (existing.DeletePending)
{
existing.DeletePending = false;
Update(existing, false);
BeatmapSetAdded?.Invoke(existing);
}
2017-03-02 12:36:01 +00:00
return existing;
}
var beatmapSet = new BeatmapSetInfo
{
OnlineBeatmapSetID = metadata.OnlineBeatmapSetID,
Beatmaps = new List<BeatmapInfo>(),
Path = path,
Hash = hash,
Metadata = metadata
};
using (var archive = ArchiveReader.GetReader(Storage, path))
{
2017-03-04 10:02:36 +00:00
string[] mapNames = archive.BeatmapFilenames;
foreach (var name in mapNames)
2017-03-04 10:02:36 +00:00
using (var raw = archive.GetStream(name))
using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit
using (var sr = new StreamReader(ms))
{
2017-03-04 10:02:36 +00:00
raw.CopyTo(ms);
ms.Position = 0;
var decoder = BeatmapDecoder.GetDecoder(sr);
Beatmap beatmap = decoder.Decode(sr);
beatmap.BeatmapInfo.Path = name;
2017-03-04 10:02:36 +00:00
beatmap.BeatmapInfo.Hash = ms.GetMd5Hash();
// TODO: Diff beatmap metadata with set metadata and leave it here if necessary
beatmap.BeatmapInfo.Metadata = null;
2017-04-17 10:44:03 +00: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);
2017-04-17 10:44:03 +00:00
beatmap.BeatmapInfo.StarDifficulty = rulesets.Query<RulesetInfo>().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID)?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0;
beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo);
}
2017-03-04 10:02:36 +00:00
beatmapSet.StoryboardFile = archive.StoryboardFilename;
}
return beatmapSet;
}
public void Import(IEnumerable<BeatmapSetInfo> beatmapSets)
{
lock (Connection)
{
Connection.BeginTransaction();
foreach (var s in beatmapSets)
2017-03-02 12:36:01 +00:00
{
Connection.InsertOrReplaceWithChildren(s, true);
2017-03-02 12:36:01 +00:00
BeatmapSetAdded?.Invoke(s);
}
Connection.Commit();
}
2016-10-13 14:29:30 +00:00
}
public void Delete(BeatmapSetInfo beatmapSet)
{
2017-02-24 08:08:13 +00:00
beatmapSet.DeletePending = true;
Update(beatmapSet, false);
BeatmapSetRemoved?.Invoke(beatmapSet);
}
public ArchiveReader GetReader(BeatmapSetInfo beatmapSet)
{
2016-11-01 14:24:14 +00:00
if (string.IsNullOrEmpty(beatmapSet.Path))
return null;
return ArchiveReader.GetReader(Storage, beatmapSet.Path);
}
public BeatmapSetInfo GetBeatmapSet(int id)
{
return Query<BeatmapSetInfo>().FirstOrDefault(s => s.OnlineBeatmapSetID == id);
}
public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null, bool withStoryboard = false)
{
if (beatmapInfo.BeatmapSet == null || beatmapInfo.Ruleset == null)
beatmapInfo = GetChildren(beatmapInfo, true);
2017-05-06 07:37:53 +00:00
if (beatmapInfo.BeatmapSet == null)
2016-12-20 15:56:49 +00:00
throw new InvalidOperationException($@"Beatmap set {beatmapInfo.BeatmapSetInfoID} is not in the local database.");
if (beatmapInfo.Metadata == null)
2017-05-06 07:37:53 +00:00
beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata;
WorkingBeatmap working = new DatabaseWorkingBeatmap(this, beatmapInfo, withStoryboard);
previous?.TransferTo(working);
return working;
}
public bool Exists(BeatmapSetInfo beatmapSet) => Storage.Exists(beatmapSet.Path);
}
}