1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-16 04:52:35 +08:00

Compare commits

...

9 Commits

45 changed files with 1418 additions and 521 deletions
+20 -8
View File
@@ -111,14 +111,26 @@ namespace osu.Desktop
{
var filePaths = new [] { e.FileName };
if (filePaths.All(f => Path.GetExtension(f) == @".osz"))
Task.Factory.StartNew(() => BeatmapManager.Import(filePaths), TaskCreationOptions.LongRunning);
else if (filePaths.All(f => Path.GetExtension(f) == @".osr"))
Task.Run(() =>
{
var score = ScoreStore.ReadReplayFile(filePaths.First());
Schedule(() => LoadScore(score));
});
var firstExtension = Path.GetExtension(filePaths.First());
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
switch (firstExtension)
{
case ".osz":
Task.Factory.StartNew(() => BeatmapManager.Import(filePaths), TaskCreationOptions.LongRunning);
return;
case ".osk":
Task.Factory.StartNew(() => SkinManager.Import(filePaths), TaskCreationOptions.LongRunning);
return;
case ".osr":
Task.Run(() =>
{
var score = ScoreStore.ReadReplayFile(filePaths.First());
Schedule(() => LoadScore(score));
});
return;
}
}
private static readonly string[] allowed_extensions = { @".osz", @".osr" };
+1 -1
View File
@@ -22,7 +22,7 @@ namespace osu.Desktop
{
if (!host.IsPrimaryInstance)
{
var importer = new BeatmapIPCChannel(host);
var importer = new ArchiveImportIPCChannel(host);
// Restore the cwd so relative paths given at the command line work correctly
Directory.SetCurrentDirectory(cwd);
foreach (var file in args)
@@ -6,30 +6,24 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class ApproachCircle : Container
{
private readonly Sprite approachCircle;
public ApproachCircle()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
approachCircle = new Sprite()
};
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
approachCircle.Texture = textures.Get(@"Play/osu/approachcircle");
Child = new SkinnableComponent("Play/osu/approachcircle", name => new Sprite { Texture = textures.Get(name) });
}
}
}
@@ -165,7 +165,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var temp = prepareTempCopy(osz_path);
Assert.IsTrue(File.Exists(temp));
var importer = new BeatmapIPCChannel(client);
var importer = new ArchiveImportIPCChannel(client);
if (!importer.ImportAsync(temp).Wait(10000))
Assert.Fail(@"IPC took too long to send");
@@ -209,7 +209,11 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(File.Exists(temp));
var imported = osu.Dependencies.Get<BeatmapManager>().Import(temp);
var manager = osu.Dependencies.Get<BeatmapManager>();
manager.Import(temp);
var imported = manager.GetAllUsableBeatmapSets();
ensureLoaded(osu);
@@ -5,9 +5,9 @@ using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.IO;
using osu.Game.Tests.Resources;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO.Archives;
namespace osu.Game.Tests.Beatmaps.IO
{
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{
using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz"))
{
var reader = new OszArchiveReader(osz);
var reader = new ZipArchiveReader(osz);
string[] expected =
{
"Soleily - Renatus (Deif) [Platter].osu",
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{
using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz"))
{
var reader = new OszArchiveReader(osz);
var reader = new ZipArchiveReader(osz);
BeatmapMetadata meta;
using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
@@ -71,7 +71,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{
using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz"))
{
var reader = new OszArchiveReader(osz);
var reader = new ZipArchiveReader(osz);
using (var stream = new StreamReader(
reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
{
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual
{
if (deleteMaps)
{
manager.DeleteAll();
manager.Delete(manager.GetAllUsableBeatmapSets());
game.Beatmap.SetDefault();
}
+33 -308
View File
@@ -7,17 +7,14 @@ using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Ionic.Zip;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Extensions;
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.IO;
using osu.Game.IPC;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Notifications;
@@ -28,23 +25,13 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
public partial class BeatmapManager
public partial class BeatmapManager : ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
{
/// <summary>
/// Fired when a new <see cref="BeatmapSetInfo"/> becomes available in the database.
/// </summary>
public event Action<BeatmapSetInfo> BeatmapSetAdded;
/// <summary>
/// Fired when a single difficulty has been hidden.
/// </summary>
public event Action<BeatmapInfo> BeatmapHidden;
/// <summary>
/// Fired when a <see cref="BeatmapSetInfo"/> is removed from the database.
/// </summary>
public event Action<BeatmapSetInfo> BeatmapSetRemoved;
/// <summary>
/// Fired when a single difficulty has been restored.
/// </summary>
@@ -60,9 +47,7 @@ namespace osu.Game.Beatmaps
/// </summary>
public WorkingBeatmap DefaultBeatmap { private get; set; }
private readonly IDatabaseContextFactory contextFactory;
private readonly FileStore files;
public override string[] HandledExtensions => new[] { ".osz" };
private readonly RulesetStore rulesets;
@@ -72,150 +57,56 @@ namespace osu.Game.Beatmaps
private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private BeatmapIPCChannel ipc;
/// <summary>
/// Set an endpoint for notifications to be posted to.
/// </summary>
public Action<Notification> PostNotification { private get; set; }
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
: base(storage, contextFactory, new BeatmapStore(contextFactory), importHost)
{
this.contextFactory = contextFactory;
beatmaps = new BeatmapStore(contextFactory);
beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s);
beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s);
beatmaps = (BeatmapStore)ModelStore;
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
files = new FileStore(contextFactory, storage);
this.rulesets = rulesets;
this.api = api;
if (importHost != null)
ipc = new BeatmapIPCChannel(importHost, this);
beatmaps.Cleanup();
}
/// <summary>
/// Import one or more <see cref="BeatmapSetInfo"/> from filesystem <paramref name="paths"/>.
/// This will post notifications tracking progress.
/// </summary>
/// <param name="paths">One or more beatmap locations on disk.</param>
public List<BeatmapSetInfo> Import(params string[] paths)
protected override void Populate(BeatmapSetInfo model, ArchiveReader archive)
{
var notification = new ProgressNotification
model.Beatmaps = createBeatmapDifficulties(archive);
// remove metadata from difficulties where it matches the set
foreach (BeatmapInfo b in model.Beatmaps)
if (model.Metadata.Equals(b.Metadata))
b.Metadata = null;
}
protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model)
{
// check if this beatmap has already been imported and exit early if so
var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == model.Hash);
if (existingHashMatch != null)
{
Text = "Beatmap import is initialising...",
CompletionText = "Import successful!",
Progress = 0,
State = ProgressNotificationState.Active,
};
Undelete(existingHashMatch);
return existingHashMatch;
}
PostNotification?.Invoke(notification);
List<BeatmapSetInfo> imported = new List<BeatmapSetInfo>();
int i = 0;
foreach (string path in paths)
// check if a set already exists with the same online id
if (model.OnlineBeatmapSetID != null)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return imported;
try
var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID);
if (existingOnlineId != null)
{
notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}";
using (ArchiveReader reader = getReaderFrom(path))
imported.Add(Import(reader));
notification.Progress = (float)++i / paths.Length;
// 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
{
if (File.Exists(path))
File.Delete(path);
}
catch (Exception e)
{
Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})");
}
}
catch (Exception e)
{
e = e.InnerException ?? e;
Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})");
Delete(existingOnlineId);
beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID);
}
}
notification.State = ProgressNotificationState.Completed;
return imported;
return null;
}
/// <summary>
/// Import a beatmap from an <see cref="ArchiveReader"/>.
/// </summary>
/// <param name="archive">The beatmap to be imported.</param>
public BeatmapSetInfo Import(ArchiveReader archive)
{
using (contextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
{
// create a new set info (don't yet add to database)
var beatmapSet = createBeatmapSetInfo(archive);
// 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)
{
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="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.
@@ -260,7 +151,7 @@ namespace osu.Game.Beatmaps
{
// This gets scheduled back to the update thread, but we want the import to run in the background.
using (var stream = new MemoryStream(data))
using (var archive = new OszArchiveReader(stream))
using (var archive = new ZipArchiveReader(stream, beatmapSetInfo.ToString()))
Import(archive);
downloadNotification.State = ProgressNotificationState.Completed;
@@ -300,95 +191,6 @@ 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.
/// </summary>
/// <param name="beatmapSet">The beatmap set to delete.</param>
public void Delete(BeatmapSetInfo beatmapSet)
{
using (var usage = contextFactory.GetForWrite())
{
var context = usage.Context;
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))
{
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();
if (!deleteMaps.Any()) return;
var notification = new ProgressNotification
{
CompletionText = "Restored all deleted beatmaps!",
Progress = 0,
State = ProgressNotificationState.Active,
};
PostNotification?.Invoke(notification);
int i = 0;
foreach (var bs in deleteMaps)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
notification.Text = $"Restoring ({i} of {deleteMaps.Count})";
notification.Progress = (float)++i / deleteMaps.Count;
Undelete(bs);
}
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;
using (var usage = contextFactory.GetForWrite())
{
usage.Context.ChangeTracker.AutoDetectChangesEnabled = false;
if (!beatmaps.Undelete(beatmapSet)) return;
if (!beatmapSet.Protected)
files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
usage.Context.ChangeTracker.AutoDetectChangesEnabled = true;
}
}
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>
@@ -415,7 +217,7 @@ namespace osu.Game.Beatmaps
if (beatmapInfo.Metadata == null)
beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata;
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(files.Store, beatmapInfo);
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, beatmapInfo);
previous?.TransferTo(working);
@@ -429,18 +231,11 @@ namespace osu.Game.Beatmaps
/// <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.BeatmapSets.AsNoTracking().FirstOrDefault(query);
/// <summary>
/// Refresh an existing instance of a <see cref="BeatmapSetInfo"/> from the store.
/// </summary>
/// <param name="beatmapSet">A stale instance.</param>
/// <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();
public List<BeatmapSetInfo> GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending && !s.Protected).ToList();
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
@@ -484,54 +279,6 @@ 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();
if (maps.Count == 0) return;
var notification = new ProgressNotification
{
Progress = 0,
CompletionText = "Deleted all beatmaps!",
State = ProgressNotificationState.Active,
};
PostNotification?.Invoke(notification);
int i = 0;
foreach (var b in maps)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
notification.Text = $"Deleting ({i} of {maps.Count})";
notification.Progress = (float)++i / maps.Count;
Delete(b);
}
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>
@@ -546,10 +293,7 @@ namespace osu.Game.Beatmaps
return hashable.ComputeSHA2Hash();
}
/// <summary>
/// Create a <see cref="BeatmapSetInfo"/> from a provided archive.
/// </summary>
private BeatmapSetInfo createBeatmapSetInfo(ArchiveReader reader)
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{
// let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
@@ -568,25 +312,6 @@ namespace osu.Game.Beatmaps
};
}
/// <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>
+2 -1
View File
@@ -3,11 +3,12 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using osu.Game.Database;
using osu.Game.IO;
namespace osu.Game.Beatmaps
{
public class BeatmapSetFileInfo
public class BeatmapSetFileInfo : INamedFileInfo
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
+2 -1
View File
@@ -5,10 +5,11 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using osu.Game.Database;
using osu.Game.IO;
namespace osu.Game.Beatmaps
{
public class BeatmapSetInfo : IHasPrimaryKey
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
+26 -119
View File
@@ -2,8 +2,8 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using osu.Game.Database;
@@ -12,11 +12,8 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing
/// </summary>
public class BeatmapStore : DatabaseBackedStore
public class BeatmapStore : MutableDatabaseBackedStore<BeatmapSetInfo>
{
public event Action<BeatmapSetInfo> BeatmapSetAdded;
public event Action<BeatmapSetInfo> BeatmapSetRemoved;
public event Action<BeatmapInfo> BeatmapHidden;
public event Action<BeatmapInfo> BeatmapRestored;
@@ -25,88 +22,6 @@ namespace osu.Game.Beatmaps
{
}
/// <summary>
/// Add a <see cref="BeatmapSetInfo"/> to the database.
/// </summary>
/// <param name="beatmapSet">The beatmap to add.</param>
public void Add(BeatmapSetInfo beatmapSet)
{
using (var usage = ContextFactory.GetForWrite())
{
var context = usage.Context;
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);
}
/// <summary>
/// Delete a <see cref="BeatmapSetInfo"/> from the database.
/// </summary>
/// <param name="beatmapSet">The beatmap to delete.</param>
/// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns>
public bool Delete(BeatmapSetInfo beatmapSet)
{
using (ContextFactory.GetForWrite())
{
Refresh(ref beatmapSet, BeatmapSets);
if (beatmapSet.DeletePending) return false;
beatmapSet.DeletePending = true;
}
BeatmapSetRemoved?.Invoke(beatmapSet);
return true;
}
/// <summary>
/// Restore a previously deleted <see cref="BeatmapSetInfo"/>.
/// </summary>
/// <param name="beatmapSet">The beatmap to restore.</param>
/// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns>
public bool Undelete(BeatmapSetInfo beatmapSet)
{
using (ContextFactory.GetForWrite())
{
Refresh(ref beatmapSet, BeatmapSets);
if (!beatmapSet.DeletePending) return false;
beatmapSet.DeletePending = false;
}
BeatmapSetAdded?.Invoke(beatmapSet);
return true;
}
/// <summary>
/// Hide a <see cref="BeatmapInfo"/> in the database.
/// </summary>
@@ -148,46 +63,38 @@ namespace osu.Game.Beatmaps
return true;
}
public override void Cleanup() => Cleanup(_ => true);
public void Cleanup(Expression<Func<BeatmapSetInfo, bool>> query)
protected override IQueryable<BeatmapSetInfo> AddIncludesForDeletion(IQueryable<BeatmapSetInfo> query)
{
using (var usage = ContextFactory.GetForWrite())
{
var context = usage.Context;
return base.AddIncludesForDeletion(query)
.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();
protected override void Purge(List<BeatmapSetInfo> items, OsuDbContext context)
{
// metadata is M-N so we can't rely on cascades
context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata));
context.BeatmapMetadata.RemoveRange(items.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(items.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)));
// 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);
}
base.Purge(items, context);
}
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);
.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 => 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)
.Include(b => b.Ruleset)
.Include(b => b.BaseDifficulty);
.Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
.Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo)
.Include(b => b.Metadata)
.Include(b => b.Ruleset)
.Include(b => b.BaseDifficulty);
}
}
+334
View File
@@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Ionic.Zip;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.IPC;
using osu.Game.Overlays.Notifications;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Database
{
/// <summary>
/// Encapsulates a model store class to give it import functionality.
/// Adds cross-functionality with <see cref="FileStore"/> to give access to the central file store for the provided model.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanImportArchives
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : INamedFileInfo, new()
{
/// <summary>
/// Set an endpoint for notifications to be posted to.
/// </summary>
public Action<Notification> PostNotification { protected get; set; }
/// <summary>
/// Fired when a new <see cref="TModel"/> becomes available in the database.
/// </summary>
public event Action<TModel> ItemAdded;
/// <summary>
/// Fired when a <see cref="TModel"/> is removed from the database.
/// </summary>
public event Action<TModel> ItemRemoved;
public virtual string[] HandledExtensions => new[] { ".zip" };
protected readonly FileStore Files;
protected readonly IDatabaseContextFactory ContextFactory;
protected readonly MutableDatabaseBackedStore<TModel> ModelStore;
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private ArchiveImportIPCChannel ipc;
protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStore<TModel> modelStore, IIpcHost importHost = null)
{
ContextFactory = contextFactory;
ModelStore = modelStore;
ModelStore.ItemAdded += s => ItemAdded?.Invoke(s);
ModelStore.ItemRemoved += s => ItemRemoved?.Invoke(s);
Files = new FileStore(contextFactory, storage);
if (importHost != null)
ipc = new ArchiveImportIPCChannel(importHost, this);
ModelStore.PurgeDeletable();
}
/// <summary>
/// Import one or more <see cref="TModel"/> items from filesystem <paramref name="paths"/>.
/// This will post notifications tracking progress.
/// </summary>
/// <param name="paths">One or more archive locations on disk.</param>
public void Import(params string[] paths)
{
var notification = new ProgressNotification
{
Text = "Import is initialising...",
CompletionText = "Import successful!",
Progress = 0,
State = ProgressNotificationState.Active,
};
PostNotification?.Invoke(notification);
List<TModel> imported = new List<TModel>();
int i = 0;
foreach (string path in paths)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
try
{
notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}";
using (ArchiveReader reader = getReaderFrom(path))
imported.Add(Import(reader));
notification.Progress = (float)++i / paths.Length;
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with items 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
{
if (File.Exists(path))
File.Delete(path);
}
catch (Exception e)
{
Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})");
}
}
catch (Exception e)
{
e = e.InnerException ?? e;
Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})");
}
}
notification.State = ProgressNotificationState.Completed;
}
/// <summary>
/// Import an item from an <see cref="ArchiveReader"/>.
/// </summary>
/// <param name="archive">The archive to be imported.</param>
public TModel Import(ArchiveReader archive)
{
using (ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
{
// create a new model (don't yet add to database)
var item = CreateModel(archive);
var existing = CheckForExisting(item);
if (existing != null) return existing;
item.Files = createFileInfos(archive, Files);
Populate(item, archive);
// import to store
ModelStore.Add(item);
return item;
}
}
/// <summary>
/// Import an item from a <see cref="TModel"/>.
/// </summary>
/// <param name="item">The model to be imported.</param>
public void Import(TModel item) => ModelStore.Add(item);
/// <summary>
/// Perform an update of the specified item.
/// TODO: Support file changes.
/// </summary>
/// <param name="item">The item to update.</param>
public void Update(TModel item) => ModelStore.Update(item);
/// <summary>
/// Delete an item from the manager.
/// Is a no-op for already deleted items.
/// </summary>
/// <param name="item">The item to delete.</param>
public void Delete(TModel item)
{
using (var usage = ContextFactory.GetForWrite())
{
var context = usage.Context;
context.ChangeTracker.AutoDetectChangesEnabled = false;
// re-fetch the model on the import context.
var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == item.ID);
if (foundModel.DeletePending) return;
if (ModelStore.Delete(foundModel))
Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray());
context.ChangeTracker.AutoDetectChangesEnabled = true;
}
}
/// <summary>
/// Delete multiple items.
/// This will post notifications tracking progress.
/// </summary>
public void Delete(List<TModel> items)
{
if (items.Count == 0) return;
var notification = new ProgressNotification
{
Progress = 0,
CompletionText = "Deleted all beatmaps!",
State = ProgressNotificationState.Active,
};
PostNotification?.Invoke(notification);
int i = 0;
using (ContextFactory.GetForWrite())
{
foreach (var b in items)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
notification.Text = $"Deleting ({i} of {items.Count})";
notification.Progress = (float)++i / items.Count;
Delete(b);
}
}
notification.State = ProgressNotificationState.Completed;
}
/// <summary>
/// Restore multiple items that were previously deleted.
/// This will post notifications tracking progress.
/// </summary>
public void Undelete(List<TModel> items)
{
if (!items.Any()) return;
var notification = new ProgressNotification
{
CompletionText = "Restored all deleted items!",
Progress = 0,
State = ProgressNotificationState.Active,
};
PostNotification?.Invoke(notification);
int i = 0;
using (ContextFactory.GetForWrite())
{
foreach (var item in items)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
notification.Text = $"Restoring ({i} of {items.Count})";
notification.Progress = (float)++i / items.Count;
Undelete(item);
}
}
notification.State = ProgressNotificationState.Completed;
}
/// <summary>
/// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set.
/// </summary>
/// <param name="item">The item to restore</param>
public void Undelete(TModel item)
{
using (var usage = ContextFactory.GetForWrite())
{
usage.Context.ChangeTracker.AutoDetectChangesEnabled = false;
if (!ModelStore.Undelete(item)) return;
Files.Reference(item.Files.Select(f => f.FileInfo).ToArray());
usage.Context.ChangeTracker.AutoDetectChangesEnabled = true;
}
}
/// <summary>
/// Create all required <see cref="FileInfo"/>s for the provided archive, adding them to the global file store.
/// </summary>
private List<TFileModel> createFileInfos(ArchiveReader reader, FileStore files)
{
var fileInfos = new List<TFileModel>();
// import files to manager
foreach (string file in reader.Filenames)
using (Stream s = reader.GetStream(file))
fileInfos.Add(new TFileModel
{
Filename = file,
FileInfo = files.Add(s)
});
return fileInfos;
}
/// <summary>
/// Create a barebones model from the provided archive.
/// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking.
/// </summary>
/// <param name="archive"></param>
/// <returns></returns>
protected abstract TModel CreateModel(ArchiveReader archive);
/// <summary>
/// Populate the provided model completely from the given archive.
/// After this method, the model should be in a state ready to commit to a store.
/// </summary>
/// <param name="model">The model to populate.</param>
/// <param name="archive">The archive to use as a reference for population.</param>
protected virtual void Populate(TModel model, ArchiveReader archive)
{
}
protected virtual TModel CheckForExisting(TModel model) => null;
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
/// <param name="path">A file or folder path resolving the archive content.</param>
/// <returns>A reader giving access to the archive's content.</returns>
private ArchiveReader getReaderFrom(string path)
{
if (ZipFile.IsZipFile(path))
return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path));
return new LegacyFilesystemReader(path);
}
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ namespace osu.Game.Database
/// <summary>
/// Perform any common clean-up tasks. Should be run when idle, or whenever necessary.
/// </summary>
public virtual void Cleanup()
public virtual void PurgeDeletable()
{
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace osu.Game.Database
{
public interface ICanImportArchives
{
void Import(params string[] paths);
string[] HandledExtensions { get; }
}
}
+13
View File
@@ -0,0 +1,13 @@
using osu.Game.IO;
namespace osu.Game.Database
{
/// <summary>
/// Represent a join model which gives a filename and scope to a <see cref="FileInfo"/>.
/// </summary>
public interface INamedFileInfo
{
FileInfo FileInfo { get; set; }
string Filename { get; set; }
}
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Database
{
public interface ISoftDelete
{
bool DeletePending { get; set; }
}
}
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using osu.Framework.Platform;
namespace osu.Game.Database
{
/// <summary>
/// A typed store which supports basic addition, deletion and updating for soft-deletable models.
/// </summary>
/// <typeparam name="T">The databased model.</typeparam>
public abstract class MutableDatabaseBackedStore<T> : DatabaseBackedStore
where T : class, IHasPrimaryKey, ISoftDelete
{
public event Action<T> ItemAdded;
public event Action<T> ItemRemoved;
protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
: base(contextFactory, storage)
{
}
public void Add(T item)
{
using (var usage = ContextFactory.GetForWrite())
{
var context = usage.Context;
context.Attach(item);
}
ItemAdded?.Invoke(item);
}
/// <summary>
/// Update a <see cref="T"/> in the database.
/// </summary>
/// <param name="item">The item to update.</param>
public void Update(T item)
{
ItemRemoved?.Invoke(item);
using (var usage = ContextFactory.GetForWrite())
usage.Context.Update(item);
ItemAdded?.Invoke(item);
}
public bool Delete(T item)
{
using (ContextFactory.GetForWrite())
{
Refresh(ref item);
if (item.DeletePending) return false;
item.DeletePending = true;
}
ItemRemoved?.Invoke(item);
return true;
}
public bool Undelete(T item)
{
using (ContextFactory.GetForWrite())
{
Refresh(ref item);
if (!item.DeletePending) return false;
item.DeletePending = false;
}
ItemAdded?.Invoke(item);
return true;
}
protected virtual IQueryable<T> AddIncludesForDeletion(IQueryable<T> query) => query;
protected virtual void Purge(List<T> items, OsuDbContext context)
{
// cascades down to beatmaps.
context.RemoveRange(items);
}
/// <summary>
/// Purge items in a pending delete state.
/// </summary>
/// <param name="query">An optional query limiting the scope of the purge.</param>
public void PurgeDeletable(Expression<Func<T, bool>> query = null)
{
using (var usage = ContextFactory.GetForWrite())
{
var context = usage.Context;
var lookup = context.Set<T>().Where(s => s.DeletePending);
if (query != null) lookup = lookup.Where(query);
AddIncludesForDeletion(lookup);
var purgeable = lookup.ToList();
if (!purgeable.Any()) return;
Purge(purgeable, context);
}
}
}
}
+2
View File
@@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Skinning;
using DatabasedKeyBinding = osu.Game.Input.Bindings.DatabasedKeyBinding;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
@@ -26,6 +27,7 @@ namespace osu.Game.Database
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
public DbSet<FileInfo> FileInfo { get; set; }
public DbSet<RulesetInfo> RulesetInfo { get; set; }
public DbSet<SkinInfo> SkinInfo { get; set; }
private readonly string connectionString;
@@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.IO;
using osu.Framework.IO.Stores;
namespace osu.Game.Beatmaps.IO
namespace osu.Game.IO.Archives
{
public abstract class ArchiveReader : IDisposable, IResourceStore<byte[]>
{
@@ -17,6 +17,16 @@ namespace osu.Game.Beatmaps.IO
public abstract void Dispose();
/// <summary>
/// The name of this archive (usually the containing filename).
/// </summary>
public readonly string Name;
protected ArchiveReader(string name)
{
Name = name;
}
public abstract IEnumerable<string> Filenames { get; }
public virtual byte[] Get(string name)
@@ -1,12 +1,12 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.IO.File;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.IO.File;
namespace osu.Game.Beatmaps.IO
namespace osu.Game.IO.Archives
{
/// <summary>
/// Reads an extracted legacy beatmap from disk.
@@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps.IO
{
private readonly string path;
public LegacyFilesystemReader(string path)
public LegacyFilesystemReader(string path) : base(Path.GetFileName(path))
{
this.path = path;
}
@@ -6,14 +6,15 @@ using System.IO;
using System.Linq;
using Ionic.Zip;
namespace osu.Game.Beatmaps.IO
namespace osu.Game.IO.Archives
{
public sealed class OszArchiveReader : ArchiveReader
public sealed class ZipArchiveReader : ArchiveReader
{
private readonly Stream archiveStream;
private readonly ZipFile archive;
public OszArchiveReader(Stream archiveStream)
public ZipArchiveReader(Stream archiveStream, string name = null)
: base(name)
{
this.archiveStream = archiveStream;
archive = ZipFile.Read(archiveStream);
+1 -1
View File
@@ -90,7 +90,7 @@ namespace osu.Game.IO
}
}
public override void Cleanup()
public override void PurgeDeletable()
{
using (var usage = ContextFactory.GetForWrite())
{
+9
View File
@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace osu.Game.IO
{
public interface IHasFiles<TFile>
{
List<TFile> Files { get; set; }
}
}
@@ -2,23 +2,25 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
namespace osu.Game.IPC
{
public class BeatmapIPCChannel : IpcChannel<BeatmapImportMessage>
public class ArchiveImportIPCChannel : IpcChannel<ArchiveImportMessage>
{
private readonly BeatmapManager beatmaps;
private readonly ICanImportArchives importer;
public BeatmapIPCChannel(IIpcHost host, BeatmapManager beatmaps = null)
public ArchiveImportIPCChannel(IIpcHost host, ICanImportArchives importer = null)
: base(host)
{
this.beatmaps = beatmaps;
this.importer = importer;
MessageReceived += msg =>
{
Debug.Assert(beatmaps != null);
Debug.Assert(importer != null);
ImportAsync(msg.Path).ContinueWith(t =>
{
if (t.Exception != null) throw t.Exception;
@@ -28,18 +30,19 @@ namespace osu.Game.IPC
public async Task ImportAsync(string path)
{
if (beatmaps == null)
if (importer == null)
{
//we want to contact a remote osu! to handle the import.
await SendMessageAsync(new BeatmapImportMessage { Path = path });
await SendMessageAsync(new ArchiveImportMessage { Path = path });
return;
}
beatmaps.Import(path);
if (importer.HandledExtensions.Contains(Path.GetExtension(path)))
importer.Import(path);
}
}
public class BeatmapImportMessage
public class ArchiveImportMessage
{
public string Path;
}
+377
View File
@@ -0,0 +1,377 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using osu.Game.Database;
using System;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20180214113409_AddSkins")]
partial class AddSkins
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<float>("SliderMultiplier");
b.Property<float>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("AudioLeadIn");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<bool>("Countdown");
b.Property<double>("DistanceSpacing");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MD5Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntKey")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<string>("Name");
b.HasKey("ID");
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,72 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace osu.Game.Migrations
{
public partial class AddSkins : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SkinInfo",
columns: table => new
{
ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Creator = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SkinInfo", x => x.ID);
});
migrationBuilder.CreateTable(
name: "SkinFileInfo",
columns: table => new
{
ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
FileInfoID = table.Column<int>(type: "INTEGER", nullable: false),
Filename = table.Column<string>(type: "TEXT", nullable: false),
SkinInfoID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SkinFileInfo", x => x.ID);
table.ForeignKey(
name: "FK_SkinFileInfo_FileInfo_FileInfoID",
column: x => x.FileInfoID,
principalTable: "FileInfo",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SkinFileInfo_SkinInfo_SkinInfoID",
column: x => x.SkinInfoID,
principalTable: "SkinInfo",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SkinFileInfo_FileInfoID",
table: "SkinFileInfo",
column: "FileInfoID");
migrationBuilder.CreateIndex(
name: "IX_SkinFileInfo_SkinInfoID",
table: "SkinFileInfo",
column: "SkinInfoID");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SkinFileInfo");
migrationBuilder.DropTable(
name: "SkinInfo");
}
}
}
@@ -281,6 +281,41 @@ namespace osu.Game.Migrations
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<string>("Name");
b.HasKey("ID");
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
@@ -322,6 +357,19 @@ namespace osu.Game.Migrations
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
+30
View File
@@ -0,0 +1,30 @@
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
public abstract class APIDownloadRequest : APIRequest
{
protected override WebRequest CreateWebRequest()
{
var request = new WebRequest(Uri);
request.DownloadProgress += request_Progress;
return request;
}
private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); });
protected APIDownloadRequest()
{
base.Success += onSuccess;
}
private void onSuccess()
{
Success?.Invoke(WebRequest.ResponseData);
}
public event APIProgressHandler Progress;
public new event APISuccessHandler<byte[]> Success;
}
}
-26
View File
@@ -27,32 +27,6 @@ namespace osu.Game.Online.API
public new event APISuccessHandler<T> Success;
}
public abstract class APIDownloadRequest : APIRequest
{
protected override WebRequest CreateWebRequest()
{
var request = new WebRequest(Uri);
request.DownloadProgress += request_Progress;
return request;
}
private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); });
protected APIDownloadRequest()
{
base.Success += onSuccess;
}
private void onSuccess()
{
Success?.Invoke(WebRequest.ResponseData);
}
public event APIProgressHandler Progress;
public new event APISuccessHandler<byte[]> Success;
}
/// <summary>
/// AN API request with no specified response type.
/// </summary>
+15 -1
View File
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using osu.Framework.Configuration;
using osu.Framework.Screens;
using osu.Game.Configuration;
@@ -114,7 +115,20 @@ namespace osu.Game
if (args?.Length > 0)
{
var paths = args.Where(a => !a.StartsWith(@"-"));
Task.Run(() => BeatmapManager.Import(paths.ToArray()));
var firstPath = paths.FirstOrDefault();
if (firstPath != null)
{
switch (Path.GetExtension(firstPath))
{
case ".osz":
Task.Run(() => BeatmapManager.Import(paths.ToArray()));
break;
case ".osk":
Task.Run(() => SkinManager.Import(paths.ToArray()));
break;
}
}
}
dependencies.CacheAs(this);
+6 -1
View File
@@ -27,6 +27,7 @@ using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game
{
@@ -36,6 +37,8 @@ namespace osu.Game
protected BeatmapManager BeatmapManager;
protected SkinManager SkinManager;
protected RulesetStore RulesetStore;
protected FileStore FileStore;
@@ -95,6 +98,8 @@ namespace osu.Game
dependencies.Cache(new LargeTextureStore(new RawTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures"))));
dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host));
dependencies.CacheAs(this);
dependencies.Cache(LocalConfig);
@@ -172,7 +177,7 @@ namespace osu.Game
API.Register(this);
FileStore.Cleanup();
FileStore.PurgeDeletable();
}
private void runMigrations()
+2 -2
View File
@@ -223,13 +223,13 @@ namespace osu.Game.Overlays.BeatmapSet
tabsBg.Colour = colours.Gray3;
this.beatmaps = beatmaps;
beatmaps.BeatmapSetAdded += handleBeatmapAdd;
beatmaps.ItemAdded += handleBeatmapAdd;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (beatmaps != null) beatmaps.BeatmapSetAdded -= handleBeatmapAdd;
if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd;
}
private void handleBeatmapAdd(BeatmapSetInfo beatmap)
+1 -1
View File
@@ -185,7 +185,7 @@ namespace osu.Game.Overlays
resultCountsContainer.Colour = colours.Yellow;
beatmaps.BeatmapSetAdded += setAdded;
beatmaps.ItemAdded += setAdded;
}
private void setAdded(BeatmapSetInfo set)
+2 -2
View File
@@ -74,8 +74,8 @@ namespace osu.Game.Overlays.Music
},
};
beatmaps.BeatmapSetAdded += list.AddBeatmapSet;
beatmaps.BeatmapSetRemoved += list.RemoveBeatmapSet;
beatmaps.ItemAdded += list.AddBeatmapSet;
beatmaps.ItemRemoved += list.RemoveBeatmapSet;
list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets();
@@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
{
deleteButton.Enabled.Value = false;
Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true));
Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true));
}));
}
},
@@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () =>
{
undeleteButton.Enabled.Value = false;
Task.Run(() => beatmaps.UndeleteAll()).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.DeletePending))).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
}
},
};
+4 -5
View File
@@ -10,8 +10,8 @@ using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.IO;
using osu.Game.Configuration;
using osu.Game.IO.Archives;
using osu.Game.Screens.Backgrounds;
using OpenTK;
using OpenTK.Graphics;
@@ -62,8 +62,10 @@ namespace osu.Game.Screens.Menu
if (setInfo == null)
{
// we need to import the default menu background beatmap
setInfo = beatmaps.Import(new OszArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz")));
setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz"));
setInfo.Protected = true;
beatmaps.Update(setInfo);
}
}
@@ -73,9 +75,6 @@ namespace osu.Game.Screens.Menu
welcome = audio.Sample.Get(@"welcome");
seeya = audio.Sample.Get(@"seeya");
if (setInfo.Protected)
beatmaps.Delete(setInfo);
}
protected override void OnEntering(Screen last)
+4 -4
View File
@@ -197,8 +197,8 @@ namespace osu.Game.Screens.Select
if (osu != null)
Ruleset.BindTo(osu.Ruleset);
this.beatmaps.BeatmapSetAdded += onBeatmapSetAdded;
this.beatmaps.BeatmapSetRemoved += onBeatmapSetRemoved;
this.beatmaps.ItemAdded += onBeatmapSetAdded;
this.beatmaps.ItemRemoved += onBeatmapSetRemoved;
this.beatmaps.BeatmapHidden += onBeatmapHidden;
this.beatmaps.BeatmapRestored += onBeatmapRestored;
@@ -401,8 +401,8 @@ namespace osu.Game.Screens.Select
if (beatmaps != null)
{
beatmaps.BeatmapSetAdded -= onBeatmapSetAdded;
beatmaps.BeatmapSetRemoved -= onBeatmapSetRemoved;
beatmaps.ItemAdded -= onBeatmapSetAdded;
beatmaps.ItemRemoved -= onBeatmapSetRemoved;
beatmaps.BeatmapHidden -= onBeatmapHidden;
beatmaps.BeatmapRestored -= onBeatmapRestored;
}
+14
View File
@@ -0,0 +1,14 @@
using osu.Framework.Graphics;
namespace osu.Game.Skinning
{
public class DefaultSkin : Skin
{
public DefaultSkin()
: base("Default")
{
}
public override Drawable GetComponent(string componentName) => null;
}
}
+38
View File
@@ -0,0 +1,38 @@
// 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.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
namespace osu.Game.Skinning
{
public class LegacySkin : Skin
{
private readonly TextureStore textures;
public LegacySkin(string name, Storage storage)
: base(name)
{
textures = new TextureStore(new RawTextureLoaderStore(new StorageBackedResourceStore(storage.GetStorageForDirectory($"skins/{name}"))));
}
public override Drawable GetComponent(string componentName)
{
var legacyComponentName = componentName.Split('/').Last();
var texture = textures.Get(legacyComponentName);
if (texture == null) return null;
return new Sprite
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Texture = textures.Get(legacyComponentName),
};
}
}
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
namespace osu.Game.Skinning
{
public abstract class Skin
{
public string Name { get; }
public abstract Drawable GetComponent(string componentName);
protected Skin(string name)
{
Name = name;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
// 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.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using osu.Game.Database;
using osu.Game.IO;
namespace osu.Game.Skinning
{
public class SkinFileInfo : INamedFileInfo
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public int SkinInfoID { get; set; }
public int FileInfoID { get; set; }
public FileInfo FileInfo { get; set; }
[Required]
public string Filename { get; set; }
}
}
+24
View File
@@ -0,0 +1,24 @@
// 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.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using osu.Game.Database;
using osu.Game.IO;
namespace osu.Game.Skinning
{
public class SkinInfo : IHasFiles<SkinFileInfo>, IHasPrimaryKey, ISoftDelete
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public string Name { get; set; }
public string Creator { get; set; }
public List<SkinFileInfo> Files { get; set; }
public bool DeletePending { get; set; }
}
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.IO.Archives;
namespace osu.Game.Skinning
{
public class SkinManager : ArchiveModelManager<SkinInfo, SkinFileInfo>
{
public Bindable<Skin> CurrentSkin = new Bindable<Skin>(new DefaultSkin());
public override string[] HandledExtensions => new[] { ".osk" };
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo
{
Name = archive.Name
};
private SkinStore store;
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost)
: base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost)
{
}
}
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Platform;
using osu.Game.Database;
namespace osu.Game.Skinning
{
public class SkinStore : MutableDatabaseBackedStore<SkinInfo>
{
public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null)
: base(contextFactory, storage)
{
}
}
}
+48
View File
@@ -0,0 +1,48 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Skinning
{
public class SkinnableComponent : SkinnableComponent<Drawable>
{
public SkinnableComponent(string name, Func<string,Drawable> defaultImplementation)
: base(name, defaultImplementation)
{
RelativeSizeAxes = Axes.Both;
}
}
public class SkinnableComponent<T> : CompositeDrawable
where T : Drawable
{
private Bindable<Skin> skin;
protected Func<string,T> CreateDefault;
public string ComponentName { get; set; }
public SkinnableComponent(string name, Func<string,T> defaultImplementation)
{
ComponentName = name;
CreateDefault = defaultImplementation;
}
[BackgroundDependencyLoader]
private void load(SkinManager skinManager)
{
skin = skinManager.CurrentSkin.GetBoundCopy();
skin.ValueChanged += updateComponent;
skin.TriggerChange();
}
private void updateComponent(Skin skin)
{
InternalChild = skin.GetComponent(ComponentName) ?? CreateDefault(Name);
}
}
}
+23 -4
View File
@@ -274,13 +274,27 @@
<Compile Include="Configuration\SettingsStore.cs" />
<Compile Include="Configuration\DatabasedConfigManager.cs" />
<Compile Include="Configuration\SpeedChangeVisualisationMethod.cs" />
<Compile Include="Database\ArchiveModelManager.cs" />
<Compile Include="Database\DatabaseContextFactory.cs" />
<Compile Include="Database\DatabaseWriteUsage.cs" />
<Compile Include="Database\ICanImportArchives.cs" />
<Compile Include="Database\IDatabaseContextFactory.cs" />
<Compile Include="Database\IHasPrimaryKey.cs" />
<Compile Include="Database\INamedFileInfo.cs" />
<Compile Include="Database\ISoftDelete.cs" />
<Compile Include="Database\MutableDatabaseBackedStore.cs" />
<Compile Include="Database\SingletonContextFactory.cs" />
<Compile Include="Graphics\Containers\LinkFlowContainer.cs" />
<Compile Include="Graphics\Textures\LargeTextureStore.cs" />
<Compile Include="IO\Archives\ArchiveReader.cs" />
<Compile Include="IO\Archives\LegacyFilesystemReader.cs" />
<Compile Include="IO\Archives\ZipArchiveReader.cs" />
<Compile Include="IO\IHasFiles.cs" />
<Compile Include="Migrations\20180214113409_AddSkins.cs" />
<Compile Include="Migrations\20180214113409_AddSkins.designer.cs">
<DependentUpon>20180214113409_AddSkins.cs</DependentUpon>
</Compile>
<Compile Include="Online\API\APIDownloadRequest.cs" />
<Compile Include="Online\API\Requests\GetUserRequest.cs" />
<Compile Include="Migrations\20180125143340_Settings.cs" />
<Compile Include="Migrations\20180125143340_Settings.Designer.cs">
@@ -371,8 +385,6 @@
<Compile Include="Beatmaps\DummyWorkingBeatmap.cs" />
<Compile Include="Beatmaps\Formats\Decoder.cs" />
<Compile Include="Beatmaps\Formats\LegacyBeatmapDecoder.cs" />
<Compile Include="Beatmaps\IO\ArchiveReader.cs" />
<Compile Include="Beatmaps\IO\LegacyFilesystemReader.cs" />
<Compile Include="Online\API\Requests\GetUserScoresRequest.cs" />
<Compile Include="Screens\Edit\Screens\Compose\RadioButtons\DrawableRadioButton.cs" />
<Compile Include="Screens\Edit\Screens\Compose\RadioButtons\RadioButton.cs" />
@@ -387,7 +399,6 @@
<Compile Include="Screens\Play\BreaksOverlay\InfoLine.cs" />
<Compile Include="Screens\Play\BreaksOverlay\LetterboxOverlay.cs" />
<Compile Include="Screens\Play\BreaksOverlay\RemainingTimeCounter.cs" />
<Compile Include="Beatmaps\IO\OszArchiveReader.cs" />
<Compile Include="Beatmaps\Legacy\LegacyBeatmap.cs" />
<Compile Include="Beatmaps\RankStatus.cs" />
<Compile Include="Beatmaps\Timing\BreakPeriod.cs" />
@@ -470,7 +481,7 @@
<Compile Include="IO\Serialization\Converters\TypedListConverter.cs" />
<Compile Include="IO\Serialization\Converters\Vector2Converter.cs" />
<Compile Include="IO\Serialization\IJsonSerializable.cs" />
<Compile Include="IPC\BeatmapIPCChannel.cs" />
<Compile Include="IPC\ArchiveImportIPCChannel.cs" />
<Compile Include="IPC\ScoreIPCChannel.cs" />
<Compile Include="Online\API\APIAccess.cs" />
<Compile Include="Online\API\APIRequest.cs" />
@@ -843,6 +854,14 @@
<Compile Include="Screens\Tournament\Teams\DrawingsTeam.cs" />
<Compile Include="Screens\Tournament\Teams\ITeamList.cs" />
<Compile Include="Screens\Tournament\Teams\StorageBackedTeamList.cs" />
<Compile Include="Skinning\DefaultSkin.cs" />
<Compile Include="Skinning\LegacySkin.cs" />
<Compile Include="Skinning\Skin.cs" />
<Compile Include="Skinning\SkinFileInfo.cs" />
<Compile Include="Skinning\SkinInfo.cs" />
<Compile Include="Skinning\SkinManager.cs" />
<Compile Include="Skinning\SkinnableComponent.cs" />
<Compile Include="Skinning\SkinStore.cs" />
<Compile Include="Storyboards\CommandLoop.cs" />
<Compile Include="Storyboards\CommandTimeline.cs" />
<Compile Include="Storyboards\CommandTimelineGroup.cs" />