mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'master' of git://github.com/ppy/osu into volume-control
This commit is contained in:
@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using osu.Framework.Audio.Sample;
|
||||
|
||||
namespace osu.Game.Audio
|
||||
{
|
||||
@ -14,22 +13,10 @@ namespace osu.Game.Audio
|
||||
public const string HIT_NORMAL = @"hitnormal";
|
||||
public const string HIT_CLAP = @"hitclap";
|
||||
|
||||
public SampleChannel GetChannel(SampleManager manager, string resourceNamespace = null)
|
||||
{
|
||||
SampleChannel channel = null;
|
||||
|
||||
if (resourceNamespace != null)
|
||||
channel = manager.Get($"Gameplay/{resourceNamespace}/{Bank}-{Name}");
|
||||
|
||||
// try without namespace as a fallback.
|
||||
if (channel == null)
|
||||
channel = manager.Get($"Gameplay/{Bank}-{Name}");
|
||||
|
||||
if (channel != null)
|
||||
channel.Volume.Value = Volume / 100.0;
|
||||
|
||||
return channel;
|
||||
}
|
||||
/// <summary>
|
||||
/// An optional ruleset namespace.
|
||||
/// </summary>
|
||||
public string Namespace;
|
||||
|
||||
/// <summary>
|
||||
/// The bank to load the sample from.
|
||||
|
@ -12,8 +12,16 @@ namespace osu.Game.Beatmaps
|
||||
/// Converts a Beatmap for another mode.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of HitObject stored in the Beatmap.</typeparam>
|
||||
public abstract class BeatmapConverter<T> where T : HitObject
|
||||
public abstract class BeatmapConverter<T> : IBeatmapConverter
|
||||
where T : HitObject
|
||||
{
|
||||
private event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
||||
event Action<HitObject, IEnumerable<HitObject>> IBeatmapConverter.ObjectConverted
|
||||
{
|
||||
add => ObjectConverted += value;
|
||||
remove => ObjectConverted -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a Beatmap can be converted using this Beatmap Converter.
|
||||
/// </summary>
|
||||
@ -32,6 +40,8 @@ namespace osu.Game.Beatmaps
|
||||
return ConvertBeatmap(new Beatmap(original));
|
||||
}
|
||||
|
||||
void IBeatmapConverter.Convert(Beatmap original) => Convert(original);
|
||||
|
||||
/// <summary>
|
||||
/// Performs the conversion of a Beatmap using this Beatmap Converter.
|
||||
/// </summary>
|
||||
@ -63,8 +73,11 @@ namespace osu.Game.Beatmaps
|
||||
yield break;
|
||||
}
|
||||
|
||||
var converted = ConvertHitObject(original, beatmap).ToList();
|
||||
ObjectConverted?.Invoke(original, converted);
|
||||
|
||||
// Convert the hit object
|
||||
foreach (var obj in ConvertHitObject(original, beatmap))
|
||||
foreach (var obj in converted)
|
||||
{
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
@ -20,7 +20,15 @@ namespace osu.Game.Beatmaps
|
||||
public float DrainRate { get; set; } = DEFAULT_DIFFICULTY;
|
||||
public float CircleSize { get; set; } = DEFAULT_DIFFICULTY;
|
||||
public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY;
|
||||
public float ApproachRate { get; set; } = DEFAULT_DIFFICULTY;
|
||||
|
||||
private float? approachRate;
|
||||
|
||||
public float ApproachRate
|
||||
{
|
||||
get => approachRate ?? OverallDifficulty;
|
||||
set => approachRate = value;
|
||||
}
|
||||
|
||||
public float SliderMultiplier { get; set; } = 1;
|
||||
public float SliderTickRate { get; set; } = 1;
|
||||
|
||||
@ -40,5 +48,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);
|
||||
}
|
||||
}
|
||||
|
@ -7,49 +7,31 @@ using System.IO;
|
||||
using System.Linq;
|
||||
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.IO.Archives;
|
||||
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 : 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>
|
||||
@ -65,21 +47,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 FileStore files;
|
||||
public override string[] HandledExtensions => new[] { ".osz" };
|
||||
|
||||
private readonly RulesetStore rulesets;
|
||||
|
||||
@ -89,159 +57,59 @@ 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; }
|
||||
|
||||
private void refreshImportContext()
|
||||
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
|
||||
: base(storage, contextFactory, new BeatmapStore(contextFactory), importHost)
|
||||
{
|
||||
lock (importContextLock)
|
||||
{
|
||||
importContext?.Value?.Dispose();
|
||||
beatmaps = (BeatmapStore)ModelStore;
|
||||
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
|
||||
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
|
||||
|
||||
importContext = new Lazy<OsuDbContext>(() =>
|
||||
{
|
||||
var c = createContext();
|
||||
c.Database.AutoTransactionsEnabled = false;
|
||||
return c;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public BeatmapManager(Storage storage, Func<OsuDbContext> context, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
|
||||
{
|
||||
createContext = context;
|
||||
|
||||
refreshImportContext();
|
||||
|
||||
beatmaps = createBeatmapStore(context);
|
||||
files = new FileStore(context, storage);
|
||||
|
||||
this.storage = files.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 a notification tracking import progress.
|
||||
/// </summary>
|
||||
/// <param name="paths">One or more beatmap locations on disk.</param>
|
||||
public void 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.ConsumableItems.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);
|
||||
|
||||
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;
|
||||
|
||||
try
|
||||
var existingOnlineId = beatmaps.ConsumableItems.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))
|
||||
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)})");
|
||||
refreshImportContext();
|
||||
Delete(existingOnlineId);
|
||||
beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID);
|
||||
}
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// let's only allow one concurrent import at a time for now.
|
||||
lock (importContextLock)
|
||||
{
|
||||
var context = importContext.Value;
|
||||
|
||||
using (var transaction = context.BeginTransaction())
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@ -283,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;
|
||||
@ -323,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>
|
||||
/// 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)
|
||||
{
|
||||
lock (importContextLock)
|
||||
{
|
||||
var context = importContext.Value;
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void Undelete(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
if (beatmapSet.Protected)
|
||||
return;
|
||||
|
||||
lock (importContextLock)
|
||||
{
|
||||
var context = importContext.Value;
|
||||
|
||||
using (var transaction = context.BeginTransaction())
|
||||
{
|
||||
context.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
|
||||
var iFiles = new FileStore(() => context, storage);
|
||||
var iBeatmaps = createBeatmapStore(() => context);
|
||||
|
||||
undelete(iBeatmaps, iFiles, beatmapSet);
|
||||
|
||||
context.ChangeTracker.AutoDetectChangesEnabled = true;
|
||||
context.SaveChanges(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a beatmap difficulty.
|
||||
/// </summary>
|
||||
@ -424,21 +203,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>
|
||||
@ -453,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);
|
||||
|
||||
@ -465,21 +229,20 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query);
|
||||
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
|
||||
|
||||
/// <summary>
|
||||
/// Refresh an existing instance of a <see cref="BeatmapSetInfo"/> from the store.
|
||||
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">A stale instance.</param>
|
||||
/// <returns>A fresh instance.</returns>
|
||||
public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID);
|
||||
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||
public List<BeatmapSetInfo> GetAllUsableBeatmapSets() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>Results from the provided query.</returns>
|
||||
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.BeatmapSets.AsNoTracking().Where(query);
|
||||
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().Where(query);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
@ -496,220 +259,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,35 +279,73 @@ namespace osu.Game.Beatmaps
|
||||
await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public void DeleteAll()
|
||||
/// <summary>
|
||||
/// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
|
||||
/// </summary>
|
||||
private string computeBeatmapSetHash(ArchiveReader reader)
|
||||
{
|
||||
var maps = GetAllUsableBeatmapSets();
|
||||
// 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);
|
||||
|
||||
if (maps.Count == 0) return;
|
||||
return hashable.ComputeSHA2Hash();
|
||||
}
|
||||
|
||||
var notification = new ProgressNotification
|
||||
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"));
|
||||
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
|
||||
{
|
||||
Progress = 0,
|
||||
CompletionText = "Deleted all beatmaps!",
|
||||
State = ProgressNotificationState.Active,
|
||||
OnlineBeatmapSetID = metadata.OnlineBeatmapSetID,
|
||||
Beatmaps = new List<BeatmapInfo>(),
|
||||
Hash = computeBeatmapSetHash(reader),
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
/// <summary>
|
||||
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
|
||||
/// </summary>
|
||||
private List<BeatmapInfo> createBeatmapDifficulties(ArchiveReader reader)
|
||||
{
|
||||
var beatmapInfos = new List<BeatmapInfo>();
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach (var b in maps)
|
||||
foreach (var name in reader.Filenames.Where(f => f.EndsWith(".osu")))
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
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;
|
||||
|
||||
notification.Text = $"Deleting ({i} of {maps.Count})";
|
||||
notification.Progress = (float)++i / maps.Count;
|
||||
Delete(b);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
return beatmapInfos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
107
osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
Normal file
107
osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
Normal file
@ -0,0 +1,107 @@
|
||||
// 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()
|
||||
{
|
||||
Storyboard storyboard;
|
||||
try
|
||||
{
|
||||
using (var beatmap = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
|
||||
{
|
||||
Decoder decoder = Decoder.GetDecoder(beatmap);
|
||||
|
||||
// todo: support loading from both set-wide storyboard *and* beatmap specific.
|
||||
|
||||
if (BeatmapSetInfo?.StoryboardFile == null)
|
||||
storyboard = decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap);
|
||||
else
|
||||
{
|
||||
using (var reader = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile))))
|
||||
storyboard = decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
storyboard = new Storyboard();
|
||||
}
|
||||
|
||||
storyboard.BeatmapInfo = BeatmapInfo;
|
||||
|
||||
return storyboard;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public class BeatmapSetInfo : IHasPrimaryKey
|
||||
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete
|
||||
{
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int ID { get; set; }
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Game.Database;
|
||||
@ -11,83 +12,16 @@ 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;
|
||||
|
||||
public BeatmapStore(Func<OsuDbContext> factory)
|
||||
public BeatmapStore(IDatabaseContextFactory factory)
|
||||
: base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a <see cref="BeatmapSetInfo"/> to the database.
|
||||
/// </summary>
|
||||
/// <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))
|
||||
{
|
||||
// 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);
|
||||
context.SaveChanges();
|
||||
|
||||
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)
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
Refresh(ref beatmapSet, BeatmapSets);
|
||||
|
||||
if (beatmapSet.DeletePending) return false;
|
||||
beatmapSet.DeletePending = true;
|
||||
context.SaveChanges();
|
||||
|
||||
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)
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
Refresh(ref beatmapSet, BeatmapSets);
|
||||
|
||||
if (!beatmapSet.DeletePending) return false;
|
||||
beatmapSet.DeletePending = false;
|
||||
context.SaveChanges();
|
||||
|
||||
BeatmapSetAdded?.Invoke(beatmapSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide a <see cref="BeatmapInfo"/> in the database.
|
||||
/// </summary>
|
||||
@ -95,13 +29,13 @@ 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;
|
||||
beatmap.Hidden = true;
|
||||
context.SaveChanges();
|
||||
if (beatmap.Hidden) return false;
|
||||
beatmap.Hidden = true;
|
||||
}
|
||||
|
||||
BeatmapHidden?.Invoke(beatmap);
|
||||
return true;
|
||||
@ -114,51 +48,50 @@ 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;
|
||||
beatmap.Hidden = false;
|
||||
context.SaveChanges();
|
||||
if (!beatmap.Hidden) return false;
|
||||
beatmap.Hidden = false;
|
||||
}
|
||||
|
||||
BeatmapRestored?.Invoke(beatmap);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Cleanup()
|
||||
protected override IQueryable<BeatmapSetInfo> AddIncludesForDeletion(IQueryable<BeatmapSetInfo> query) =>
|
||||
base.AddIncludesForDeletion(query)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
|
||||
.Include(s => s.Metadata);
|
||||
|
||||
protected override IQueryable<BeatmapSetInfo> AddIncludesForConsumption(IQueryable<BeatmapSetInfo> query) =>
|
||||
base.AddIncludesForConsumption(query)
|
||||
.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);
|
||||
|
||||
protected override void Purge(List<BeatmapSetInfo> items, OsuDbContext context)
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
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);
|
||||
|
||||
// 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)));
|
||||
context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata));
|
||||
context.BeatmapMetadata.RemoveRange(items.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)));
|
||||
context.BeatmapDifficulty.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty)));
|
||||
|
||||
// cascades down to beatmaps.
|
||||
context.BeatmapSetInfo.RemoveRange(purgeable);
|
||||
context.SaveChanges();
|
||||
base.Purge(items, context);
|
||||
}
|
||||
|
||||
public IQueryable<BeatmapSetInfo> BeatmapSets => GetContext().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
|
||||
.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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
public class DifficultyControlPoint : ControlPoint
|
||||
@ -8,6 +10,12 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// <summary>
|
||||
/// The speed multiplier at this control point.
|
||||
/// </summary>
|
||||
public double SpeedMultiplier = 1;
|
||||
public double SpeedMultiplier
|
||||
{
|
||||
get => speedMultiplier;
|
||||
set => speedMultiplier = MathHelper.Clamp(value, 0.1, 10);
|
||||
}
|
||||
|
||||
private double speedMultiplier = 1;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using OpenTK;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
@ -15,6 +16,12 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// <summary>
|
||||
/// The beat length at this control point.
|
||||
/// </summary>
|
||||
public double BeatLength = 1000;
|
||||
public double BeatLength
|
||||
{
|
||||
get => beatLength;
|
||||
set => beatLength = MathHelper.Clamp(value, 6, 60000);
|
||||
}
|
||||
|
||||
private double beatLength = 1000;
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,10 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
ParseContent(stream);
|
||||
|
||||
// objects may be out of order *only* if a user has manually edited an .osu file.
|
||||
// unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828).
|
||||
this.beatmap.HitObjects.Sort((x, y) => x.StartTime.CompareTo(y.StartTime));
|
||||
|
||||
foreach (var hitObject in this.beatmap.HitObjects)
|
||||
hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty);
|
||||
}
|
||||
|
25
osu.Game/Beatmaps/IBeatmapConverter.cs
Normal file
25
osu.Game/Beatmaps/IBeatmapConverter.cs
Normal 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;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public interface IBeatmapConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="HitObject"/> has been converted.
|
||||
/// The first argument contains the <see cref="HitObject"/> that was converted.
|
||||
/// The second argument contains the <see cref="HitObject"/>s that were output from the conversion process.
|
||||
/// </summary>
|
||||
event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Beatmap using this Beatmap Converter.
|
||||
/// </summary>
|
||||
/// <param name="original">The un-converted Beatmap.</param>
|
||||
void Convert(Beatmap beatmap);
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
|
||||
protected abstract Texture GetBackground();
|
||||
protected abstract Track GetTrack();
|
||||
protected virtual Waveform GetWaveform() => new Waveform();
|
||||
protected virtual Storyboard GetStoryboard() => new Storyboard();
|
||||
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
|
||||
|
||||
public bool BeatmapLoaded => beatmap.IsResultAvailable;
|
||||
public Beatmap Beatmap => beatmap.Value.Result;
|
||||
|
@ -14,6 +14,8 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
// UI/selection defaults
|
||||
Set(OsuSetting.Ruleset, 0, 0, int.MaxValue);
|
||||
Set(OsuSetting.Skin, 0, 0, int.MaxValue);
|
||||
|
||||
Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details);
|
||||
|
||||
Set(OsuSetting.ShowConvertedBeatmaps, true);
|
||||
@ -122,6 +124,7 @@ namespace osu.Game.Configuration
|
||||
ChatDisplayHeight,
|
||||
Version,
|
||||
ShowConvertedBeatmaps,
|
||||
SpeedChangeVisualisation
|
||||
SpeedChangeVisualisation,
|
||||
Skin
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,8 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
public event Action SettingChanged;
|
||||
|
||||
public SettingsStore(Func<OsuDbContext> createContext)
|
||||
: base(createContext)
|
||||
public SettingsStore(DatabaseContextFactory contextFactory)
|
||||
: base(contextFactory)
|
||||
{
|
||||
}
|
||||
|
||||
@ -24,19 +24,16 @@ namespace osu.Game.Configuration
|
||||
/// <param name="variant">An optional variant.</param>
|
||||
/// <returns></returns>
|
||||
public List<DatabasedSetting> Query(int? rulesetId = null, int? variant = null) =>
|
||||
GetContext().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||
ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||
|
||||
public void Update(DatabasedSetting setting)
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
var newValue = setting.Value;
|
||||
|
||||
Refresh(ref setting);
|
||||
|
||||
setting.Value = newValue;
|
||||
|
||||
context.SaveChanges();
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
var newValue = setting.Value;
|
||||
Refresh(ref setting);
|
||||
setting.Value = newValue;
|
||||
}
|
||||
|
||||
SettingChanged?.Invoke();
|
||||
}
|
||||
|
337
osu.Game/Database/ArchiveModelManager.cs
Normal file
337
osu.Game/Database/ArchiveModelManager.cs
Normal file
@ -0,0 +1,337 @@
|
||||
// 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.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> : ICanAcceptFiles
|
||||
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.Cleanup();
|
||||
}
|
||||
|
||||
/// <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">The archive to create the model for.</param>
|
||||
/// <returns>A model populated with minimal information.</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,10 +1,7 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
@ -17,9 +14,7 @@ namespace osu.Game.Database
|
||||
/// <summary>
|
||||
/// Create a new <see cref="OsuDbContext"/> instance (separate from the shared context via <see cref="GetContext"/> for performing isolated operations.
|
||||
/// </summary>
|
||||
protected readonly Func<OsuDbContext> CreateContext;
|
||||
|
||||
private readonly ThreadLocal<OsuDbContext> queryContext;
|
||||
protected readonly IDatabaseContextFactory ContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Refresh an instance potentially from a different thread with a local context-tracked instance.
|
||||
@ -27,35 +22,26 @@ namespace osu.Game.Database
|
||||
/// <param name="obj">The object to use as a reference when negotiating a local instance.</param>
|
||||
/// <param name="lookupSource">An optional lookup source which will be used to query and populate a freshly retrieved replacement. If not provided, the refreshed object will still be returned but will not have any includes.</param>
|
||||
/// <typeparam name="T">A valid EF-stored type.</typeparam>
|
||||
protected virtual void Refresh<T>(ref T obj, IEnumerable<T> lookupSource = null) where T : class, IHasPrimaryKey
|
||||
protected virtual void Refresh<T>(ref T obj, IQueryable<T> lookupSource = null) where T : class, IHasPrimaryKey
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
if (context.Entry(obj).State != EntityState.Detached) return;
|
||||
|
||||
var id = obj.ID;
|
||||
var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find<T>(id);
|
||||
if (foundObject != null)
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
obj = foundObject;
|
||||
context.Entry(obj).Reload();
|
||||
var context = usage.Context;
|
||||
|
||||
if (context.Entry(obj).State != EntityState.Detached) return;
|
||||
|
||||
var id = obj.ID;
|
||||
var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find<T>(id);
|
||||
if (foundObject != null)
|
||||
obj = foundObject;
|
||||
else
|
||||
context.Add(obj);
|
||||
}
|
||||
else
|
||||
context.Add(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a shared context for performing lookups (or write operations on the update thread, for now).
|
||||
/// </summary>
|
||||
protected OsuDbContext GetContext() => queryContext.Value;
|
||||
|
||||
protected DatabaseBackedStore(Func<OsuDbContext> createContext, Storage storage = null)
|
||||
protected DatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
|
||||
{
|
||||
CreateContext = createContext;
|
||||
|
||||
// todo: while this seems to work quite well, we need to consider that contexts could enter a state where they are never cleaned up.
|
||||
queryContext = new ThreadLocal<OsuDbContext>(CreateContext);
|
||||
|
||||
ContextFactory = contextFactory;
|
||||
Storage = storage;
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,94 @@
|
||||
// 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.Threading;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class DatabaseContextFactory
|
||||
public class DatabaseContextFactory : IDatabaseContextFactory
|
||||
{
|
||||
private readonly GameHost host;
|
||||
|
||||
private const string database_name = @"client";
|
||||
|
||||
private ThreadLocal<OsuDbContext> threadContexts;
|
||||
|
||||
private readonly object writeLock = new object();
|
||||
|
||||
private bool currentWriteDidWrite;
|
||||
private volatile int currentWriteUsages;
|
||||
|
||||
public DatabaseContextFactory(GameHost host)
|
||||
{
|
||||
this.host = host;
|
||||
recycleThreadContexts();
|
||||
}
|
||||
|
||||
public OsuDbContext GetContext() => new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name));
|
||||
/// <summary>
|
||||
/// Get a context for the current thread for read-only usage.
|
||||
/// If a <see cref="DatabaseWriteUsage"/> is in progress, the existing write-safe context will be returned.
|
||||
/// </summary>
|
||||
public OsuDbContext Get() => threadContexts.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context).
|
||||
/// This method may block if a write is already active on a different thread.
|
||||
/// </summary>
|
||||
/// <returns>A usage containing a usable context.</returns>
|
||||
public DatabaseWriteUsage GetForWrite()
|
||||
{
|
||||
Monitor.Enter(writeLock);
|
||||
|
||||
Interlocked.Increment(ref currentWriteUsages);
|
||||
|
||||
return new DatabaseWriteUsage(threadContexts.Value, usageCompleted);
|
||||
}
|
||||
|
||||
private void usageCompleted(DatabaseWriteUsage usage)
|
||||
{
|
||||
int usages = Interlocked.Decrement(ref currentWriteUsages);
|
||||
|
||||
try
|
||||
{
|
||||
currentWriteDidWrite |= usage.PerformedWrite;
|
||||
|
||||
if (usages > 0) return;
|
||||
|
||||
if (currentWriteDidWrite)
|
||||
{
|
||||
// explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged).
|
||||
usage.Context.Dispose();
|
||||
|
||||
currentWriteDidWrite = false;
|
||||
|
||||
// once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches.
|
||||
recycleThreadContexts();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Exit(writeLock);
|
||||
}
|
||||
}
|
||||
|
||||
private void recycleThreadContexts() => threadContexts = new ThreadLocal<OsuDbContext>(CreateContext);
|
||||
|
||||
protected virtual OsuDbContext CreateContext()
|
||||
{
|
||||
var ctx = new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name));
|
||||
ctx.Database.AutoTransactionsEnabled = false;
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public void ResetDatabase()
|
||||
{
|
||||
// todo: we probably want to make sure there are no active contexts before performing this operation.
|
||||
host.Storage.DeleteDatabase(database_name);
|
||||
lock (writeLock)
|
||||
{
|
||||
recycleThreadContexts();
|
||||
host.Storage.DeleteDatabase(database_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
46
osu.Game/Database/DatabaseWriteUsage.cs
Normal file
46
osu.Game/Database/DatabaseWriteUsage.cs
Normal file
@ -0,0 +1,46 @@
|
||||
// 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 Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class DatabaseWriteUsage : IDisposable
|
||||
{
|
||||
public readonly OsuDbContext Context;
|
||||
private readonly IDbContextTransaction transaction;
|
||||
private readonly Action<DatabaseWriteUsage> usageCompleted;
|
||||
|
||||
public DatabaseWriteUsage(OsuDbContext context, Action<DatabaseWriteUsage> onCompleted)
|
||||
{
|
||||
Context = context;
|
||||
transaction = Context.BeginTransaction();
|
||||
usageCompleted = onCompleted;
|
||||
}
|
||||
|
||||
public bool PerformedWrite { get; private set; }
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (isDisposed) return;
|
||||
isDisposed = true;
|
||||
|
||||
PerformedWrite |= Context.SaveChanges(transaction) > 0;
|
||||
usageCompleted?.Invoke(this);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~DatabaseWriteUsage()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
22
osu.Game/Database/ICanAcceptFiles.cs
Normal file
22
osu.Game/Database/ICanAcceptFiles.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// A class which can accept files for importing.
|
||||
/// </summary>
|
||||
public interface ICanAcceptFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Import the specified paths.
|
||||
/// </summary>
|
||||
/// <param name="paths">The files which should be imported.</param>
|
||||
void Import(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// An array of accepted file extensions (in the standard format of ".abc").
|
||||
/// </summary>
|
||||
string[] HandledExtensions { get; }
|
||||
}
|
||||
}
|
20
osu.Game/Database/IDatabaseContextFactory.cs
Normal file
20
osu.Game/Database/IDatabaseContextFactory.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// 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 IDatabaseContextFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a context for read-only usage.
|
||||
/// </summary>
|
||||
OsuDbContext Get();
|
||||
|
||||
/// <summary>
|
||||
/// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context).
|
||||
/// This method may block if a write is already active on a different thread.
|
||||
/// </summary>
|
||||
/// <returns>A usage containing a usable context.</returns>
|
||||
DatabaseWriteUsage GetForWrite();
|
||||
}
|
||||
}
|
16
osu.Game/Database/IHasFiles.cs
Normal file
16
osu.Game/Database/IHasFiles.cs
Normal 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 System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// A model that contains a list of files it is responsible for.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFile">The model representing a file.</typeparam>
|
||||
public interface IHasFiles<TFile>
|
||||
{
|
||||
List<TFile> Files { get; set; }
|
||||
}
|
||||
}
|
16
osu.Game/Database/INamedFileInfo.cs
Normal file
16
osu.Game/Database/INamedFileInfo.cs
Normal 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.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; }
|
||||
}
|
||||
}
|
16
osu.Game/Database/ISoftDelete.cs
Normal file
16
osu.Game/Database/ISoftDelete.cs
Normal 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
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// A model that can be deleted from user's view without being instantly lost.
|
||||
/// </summary>
|
||||
public interface ISoftDelete
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this model is marked for future deletion.
|
||||
/// </summary>
|
||||
bool DeletePending { get; set; }
|
||||
}
|
||||
}
|
149
osu.Game/Database/MutableDatabaseBackedStore.cs
Normal file
149
osu.Game/Database/MutableDatabaseBackedStore.cs
Normal file
@ -0,0 +1,149 @@
|
||||
// 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.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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Access items pre-populated with includes for consumption.
|
||||
/// </summary>
|
||||
public IQueryable<T> ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set<T>());
|
||||
|
||||
/// <summary>
|
||||
/// Add a <see cref="T"/> to the database.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to add.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a <see cref="T"/> from the database.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to delete.</param>
|
||||
public bool Delete(T item)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
Refresh(ref item);
|
||||
|
||||
if (item.DeletePending) return false;
|
||||
item.DeletePending = true;
|
||||
}
|
||||
|
||||
ItemRemoved?.Invoke(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore a <see cref="T"/> from a deleted state.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to undelete.</param>
|
||||
public bool Undelete(T item)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
Refresh(ref item, ConsumableItems);
|
||||
|
||||
if (!item.DeletePending) return false;
|
||||
item.DeletePending = false;
|
||||
}
|
||||
|
||||
ItemAdded?.Invoke(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allow implementations to add database-side includes or constraints when querying for consumption of items.
|
||||
/// </summary>
|
||||
/// <param name="query">The input query.</param>
|
||||
/// <returns>A potentially modified output query.</returns>
|
||||
protected virtual IQueryable<T> AddIncludesForConsumption(IQueryable<T> query) => query;
|
||||
|
||||
/// <summary>
|
||||
/// Allow implementations to add database-side includes or constraints when deleting items.
|
||||
/// Included properties could then be subsequently deleted by overriding <see cref="Purge"/>.
|
||||
/// </summary>
|
||||
/// <param name="query">The input query.</param>
|
||||
/// <returns>A potentially modified output query.</returns>
|
||||
protected virtual IQueryable<T> AddIncludesForDeletion(IQueryable<T> query) => query;
|
||||
|
||||
/// <summary>
|
||||
/// Called when removing an item completely from the database.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to be purged.</param>
|
||||
/// <param name="context">The write context which can be used to perform subsequent deletions.</param>
|
||||
protected virtual void Purge(List<T> items, OsuDbContext context) => context.RemoveRange(items);
|
||||
|
||||
public override void Cleanup()
|
||||
{
|
||||
base.Cleanup();
|
||||
PurgeDeletable();
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
lookup = AddIncludesForDeletion(lookup);
|
||||
|
||||
var purgeable = lookup.ToList();
|
||||
|
||||
if (!purgeable.Any()) return;
|
||||
|
||||
Purge(purgeable, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ using osu.Game.IO;
|
||||
using osu.Game.Rulesets;
|
||||
using DatabasedKeyBinding = osu.Game.Input.Bindings.DatabasedKeyBinding;
|
||||
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
@ -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;
|
||||
|
||||
@ -111,7 +113,7 @@ namespace osu.Game.Database
|
||||
public int SaveChanges(IDbContextTransaction transaction = null)
|
||||
{
|
||||
var ret = base.SaveChanges();
|
||||
transaction?.Commit();
|
||||
if (ret > 0) transaction?.Commit();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@ -186,8 +188,6 @@ namespace osu.Game.Database
|
||||
|
||||
public void Migrate()
|
||||
{
|
||||
migrateFromSqliteNet();
|
||||
|
||||
try
|
||||
{
|
||||
Database.Migrate();
|
||||
@ -197,86 +197,6 @@ namespace osu.Game.Database
|
||||
throw new MigrationFailedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateFromSqliteNet()
|
||||
{
|
||||
try
|
||||
{
|
||||
// will fail if the database isn't in a sane EF-migrated state.
|
||||
Database.ExecuteSqlCommand("SELECT MetadataID FROM BeatmapSetInfo LIMIT 1");
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
Database.ExecuteSqlCommand("DROP TABLE IF EXISTS __EFMigrationsHistory");
|
||||
|
||||
// will fail (intentionally) if we don't have sqlite-net data present.
|
||||
Database.ExecuteSqlCommand("SELECT OnlineBeatmapSetId FROM BeatmapMetadata LIMIT 1");
|
||||
|
||||
try
|
||||
{
|
||||
Logger.Log("Performing migration from sqlite-net to EF...", LoggingTarget.Database, Framework.Logging.LogLevel.Important);
|
||||
|
||||
// we are good to perform messy migration of data!.
|
||||
Database.ExecuteSqlCommand("ALTER TABLE BeatmapDifficulty RENAME TO BeatmapDifficulty_Old");
|
||||
Database.ExecuteSqlCommand("ALTER TABLE BeatmapMetadata RENAME TO BeatmapMetadata_Old");
|
||||
Database.ExecuteSqlCommand("ALTER TABLE FileInfo RENAME TO FileInfo_Old");
|
||||
Database.ExecuteSqlCommand("ALTER TABLE KeyBinding RENAME TO KeyBinding_Old");
|
||||
Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetInfo RENAME TO BeatmapSetInfo_Old");
|
||||
Database.ExecuteSqlCommand("ALTER TABLE BeatmapInfo RENAME TO BeatmapInfo_Old");
|
||||
Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetFileInfo RENAME TO BeatmapSetFileInfo_Old");
|
||||
Database.ExecuteSqlCommand("ALTER TABLE RulesetInfo RENAME TO RulesetInfo_Old");
|
||||
|
||||
Database.ExecuteSqlCommand("DROP TABLE StoreVersion");
|
||||
|
||||
// perform EF migrations to create sane table structure.
|
||||
Database.Migrate();
|
||||
|
||||
// copy data table by table to new structure, dropping old tables as we go.
|
||||
Database.ExecuteSqlCommand("INSERT INTO FileInfo SELECT * FROM FileInfo_Old");
|
||||
Database.ExecuteSqlCommand("DROP TABLE FileInfo_Old");
|
||||
|
||||
Database.ExecuteSqlCommand("INSERT INTO KeyBinding SELECT ID, [Action], Keys, RulesetID, Variant FROM KeyBinding_Old");
|
||||
Database.ExecuteSqlCommand("DROP TABLE KeyBinding_Old");
|
||||
|
||||
Database.ExecuteSqlCommand(
|
||||
"INSERT INTO BeatmapMetadata SELECT ID, Artist, ArtistUnicode, AudioFile, Author, BackgroundFile, PreviewTime, Source, Tags, Title, TitleUnicode FROM BeatmapMetadata_Old");
|
||||
Database.ExecuteSqlCommand("DROP TABLE BeatmapMetadata_Old");
|
||||
|
||||
Database.ExecuteSqlCommand(
|
||||
"INSERT INTO BeatmapDifficulty SELECT `ID`, `ApproachRate`, `CircleSize`, `DrainRate`, `OverallDifficulty`, `SliderMultiplier`, `SliderTickRate` FROM BeatmapDifficulty_Old");
|
||||
Database.ExecuteSqlCommand("DROP TABLE BeatmapDifficulty_Old");
|
||||
|
||||
Database.ExecuteSqlCommand("INSERT INTO BeatmapSetInfo SELECT ID, DeletePending, Hash, BeatmapMetadataID, OnlineBeatmapSetID, Protected FROM BeatmapSetInfo_Old");
|
||||
Database.ExecuteSqlCommand("DROP TABLE BeatmapSetInfo_Old");
|
||||
|
||||
Database.ExecuteSqlCommand("INSERT INTO BeatmapSetFileInfo SELECT ID, BeatmapSetInfoID, FileInfoID, Filename FROM BeatmapSetFileInfo_Old");
|
||||
Database.ExecuteSqlCommand("DROP TABLE BeatmapSetFileInfo_Old");
|
||||
|
||||
Database.ExecuteSqlCommand("INSERT INTO RulesetInfo SELECT ID, Available, InstantiationInfo, Name FROM RulesetInfo_Old");
|
||||
Database.ExecuteSqlCommand("DROP TABLE RulesetInfo_Old");
|
||||
|
||||
Database.ExecuteSqlCommand(
|
||||
"INSERT INTO BeatmapInfo SELECT ID, AudioLeadIn, BaseDifficultyID, BeatDivisor, BeatmapSetInfoID, Countdown, DistanceSpacing, GridSize, Hash, IFNULL(Hidden, 0), LetterboxInBreaks, MD5Hash, NULLIF(BeatmapMetadataID, 0), NULLIF(OnlineBeatmapID, 0), Path, RulesetID, SpecialStyle, StackLeniency, StarDifficulty, StoredBookmarks, TimelineZoom, Version, WidescreenStoryboard FROM BeatmapInfo_Old");
|
||||
Database.ExecuteSqlCommand("DROP TABLE BeatmapInfo_Old");
|
||||
|
||||
Logger.Log("Migration complete!", LoggingTarget.Database, Framework.Logging.LogLevel.Important);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new MigrationFailedException(e);
|
||||
}
|
||||
}
|
||||
catch (MigrationFailedException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MigrationFailedException : Exception
|
||||
|
19
osu.Game/Database/SingletonContextFactory.cs
Normal file
19
osu.Game/Database/SingletonContextFactory.cs
Normal 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
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class SingletonContextFactory : IDatabaseContextFactory
|
||||
{
|
||||
private readonly OsuDbContext context;
|
||||
|
||||
public SingletonContextFactory(OsuDbContext context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public OsuDbContext Get() => context;
|
||||
|
||||
public DatabaseWriteUsage GetForWrite() => new DatabaseWriteUsage(context, null);
|
||||
}
|
||||
}
|
@ -8,12 +8,15 @@ using OpenTK;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.MathUtils;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
public class ParallaxContainer : Container, IRequireHighFrequencyMousePosition
|
||||
{
|
||||
public float ParallaxAmount = 0.02f;
|
||||
public const float DEFAULT_PARALLAX_AMOUNT = 0.02f;
|
||||
|
||||
public float ParallaxAmount = DEFAULT_PARALLAX_AMOUNT;
|
||||
|
||||
private Bindable<bool> parallaxEnabled;
|
||||
|
||||
@ -61,8 +64,9 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
if (parallaxEnabled)
|
||||
{
|
||||
Vector2 offset = input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2;
|
||||
content.MoveTo(offset * ParallaxAmount, firstUpdate ? 0 : 1000, Easing.OutQuint);
|
||||
Vector2 offset = (input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2) * ParallaxAmount;
|
||||
|
||||
content.Position = Interpolation.ValueAt(Clock.ElapsedFrameTime, content.Position, offset, 0, 1000, Easing.OutQuint);
|
||||
content.Scale = new Vector2(1 + ParallaxAmount);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
public Action Exit;
|
||||
|
||||
public override bool HandleLeftRightArrows => false;
|
||||
|
||||
private bool focus;
|
||||
public bool HoldFocus
|
||||
{
|
||||
|
@ -76,6 +76,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
Nub.Current.BindTo(Current);
|
||||
|
||||
Current.DisabledChanged += disabled =>
|
||||
{
|
||||
Alpha = disabled ? 0.3f : 1;
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.ValueChanged += newValue =>
|
||||
{
|
||||
if (newValue)
|
||||
@ -83,11 +93,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
else
|
||||
sampleUnchecked?.Play();
|
||||
};
|
||||
|
||||
Current.DisabledChanged += disabled =>
|
||||
{
|
||||
Alpha = disabled ? 0.3f : 1;
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnHover(InputState state)
|
||||
|
@ -20,6 +20,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
public class OsuSliderBar<T> : SliderBar<T>, IHasTooltip, IHasAccentColour
|
||||
where T : struct, IEquatable<T>, IComparable, IConvertible
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of decimal digits to be displayed in the tooltip.
|
||||
/// </summary>
|
||||
private const int max_decimal_digits = 5;
|
||||
|
||||
private SampleChannel sample;
|
||||
private double lastSampleTime;
|
||||
private T lastSampleValue;
|
||||
@ -35,6 +40,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
var bindableDouble = CurrentNumber as BindableNumber<double>;
|
||||
var bindableFloat = CurrentNumber as BindableNumber<float>;
|
||||
var floatValue = bindableDouble?.Value ?? bindableFloat?.Value;
|
||||
var floatPrecision = bindableDouble?.Precision ?? bindableFloat?.Precision;
|
||||
|
||||
if (floatValue != null)
|
||||
{
|
||||
@ -44,7 +50,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
if (floatMaxValue == 1 && (floatMinValue == 0 || floatMinValue == -1))
|
||||
return floatValue.Value.ToString("P0");
|
||||
|
||||
return floatValue.Value.ToString("N1");
|
||||
var decimalPrecision = normalise((decimal)floatPrecision, max_decimal_digits);
|
||||
|
||||
// Find the number of significant digits (we could have less than 5 after normalize())
|
||||
var significantDigits = findPrecision(decimalPrecision);
|
||||
|
||||
return floatValue.Value.ToString($"N{significantDigits}");
|
||||
}
|
||||
|
||||
var bindableInt = CurrentNumber as BindableNumber<int>;
|
||||
@ -177,5 +188,31 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
Nub.MoveToX(RangePadding + UsableWidth * value, 250, Easing.OutQuint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all non-significant digits, keeping at most a requested number of decimal digits.
|
||||
/// </summary>
|
||||
/// <param name="d">The decimal to normalize.</param>
|
||||
/// <param name="sd">The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value.</param>
|
||||
/// <returns>The normalised decimal.</returns>
|
||||
private decimal normalise(decimal d, int sd)
|
||||
=> decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the number of digits after the decimal.
|
||||
/// </summary>
|
||||
/// <param name="d">The value to find the number of decimal digits for.</param>
|
||||
/// <returns>The number decimal digits.</returns>
|
||||
private int findPrecision(decimal d)
|
||||
{
|
||||
int precision = 0;
|
||||
while (d != Math.Round(d))
|
||||
{
|
||||
d *= 10;
|
||||
precision++;
|
||||
}
|
||||
|
||||
return precision;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
@ -21,86 +21,94 @@ namespace osu.Game.IO
|
||||
|
||||
public new Storage Storage => base.Storage;
|
||||
|
||||
public FileStore(Func<OsuDbContext> createContext, Storage storage) : base(createContext, storage.GetStorageForDirectory(@"files"))
|
||||
public FileStore(IDatabaseContextFactory contextFactory, Storage storage) : base(contextFactory, storage.GetStorageForDirectory(@"files"))
|
||||
{
|
||||
Store = new StorageBackedResourceStore(Storage);
|
||||
}
|
||||
|
||||
public FileInfo Add(Stream data, bool reference = true)
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
string hash = data.ComputeSHA2Hash();
|
||||
|
||||
var existing = context.FileInfo.FirstOrDefault(f => f.Hash == hash);
|
||||
|
||||
var info = existing ?? new FileInfo { Hash = hash };
|
||||
|
||||
string path = info.StoragePath;
|
||||
|
||||
// we may be re-adding a file to fix missing store entries.
|
||||
if (!Storage.Exists(path))
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
string hash = data.ComputeSHA2Hash();
|
||||
|
||||
using (var output = Storage.GetStream(path, FileAccess.Write))
|
||||
data.CopyTo(output);
|
||||
var existing = usage.Context.FileInfo.FirstOrDefault(f => f.Hash == hash);
|
||||
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
var info = existing ?? new FileInfo { Hash = hash };
|
||||
|
||||
string path = info.StoragePath;
|
||||
|
||||
// we may be re-adding a file to fix missing store entries.
|
||||
if (!Storage.Exists(path))
|
||||
{
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using (var output = Storage.GetStream(path, FileAccess.Write))
|
||||
data.CopyTo(output);
|
||||
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
if (reference || existing == null)
|
||||
Reference(info);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
if (reference || existing == null)
|
||||
Reference(info);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public void Reference(params FileInfo[] files) => reference(GetContext(), files);
|
||||
|
||||
private void reference(OsuDbContext context, FileInfo[] files)
|
||||
public void Reference(params FileInfo[] files)
|
||||
{
|
||||
foreach (var f in files.GroupBy(f => f.ID))
|
||||
{
|
||||
var refetch = context.Find<FileInfo>(f.First().ID) ?? f.First();
|
||||
refetch.ReferenceCount += f.Count();
|
||||
context.FileInfo.Update(refetch);
|
||||
}
|
||||
if (files.Length == 0) return;
|
||||
|
||||
context.SaveChanges();
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
var context = usage.Context;
|
||||
|
||||
foreach (var f in files.GroupBy(f => f.ID))
|
||||
{
|
||||
var refetch = context.Find<FileInfo>(f.First().ID) ?? f.First();
|
||||
refetch.ReferenceCount += f.Count();
|
||||
context.FileInfo.Update(refetch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dereference(params FileInfo[] files) => dereference(GetContext(), files);
|
||||
|
||||
private void dereference(OsuDbContext context, FileInfo[] files)
|
||||
public void Dereference(params FileInfo[] files)
|
||||
{
|
||||
foreach (var f in files.GroupBy(f => f.ID))
|
||||
{
|
||||
var refetch = context.FileInfo.Find(f.Key);
|
||||
refetch.ReferenceCount -= f.Count();
|
||||
context.FileInfo.Update(refetch);
|
||||
}
|
||||
if (files.Length == 0) return;
|
||||
|
||||
context.SaveChanges();
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
var context = usage.Context;
|
||||
|
||||
foreach (var f in files.GroupBy(f => f.ID))
|
||||
{
|
||||
var refetch = context.FileInfo.Find(f.Key);
|
||||
refetch.ReferenceCount -= f.Count();
|
||||
context.FileInfo.Update(refetch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Cleanup()
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1))
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
try
|
||||
var context = usage.Context;
|
||||
|
||||
foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1))
|
||||
{
|
||||
Storage.Delete(f.StoragePath);
|
||||
context.FileInfo.Remove(f);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $@"Could not delete beatmap {f}");
|
||||
try
|
||||
{
|
||||
Storage.Delete(f.StoragePath);
|
||||
context.FileInfo.Remove(f);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $@"Could not delete beatmap {f}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 ICanAcceptFiles importer;
|
||||
|
||||
public BeatmapIPCChannel(IIpcHost host, BeatmapManager beatmaps = null)
|
||||
public ArchiveImportIPCChannel(IIpcHost host, ICanAcceptFiles 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;
|
||||
}
|
@ -16,14 +16,17 @@ namespace osu.Game.Input
|
||||
{
|
||||
public event Action KeyBindingChanged;
|
||||
|
||||
public KeyBindingStore(Func<OsuDbContext> createContext, RulesetStore rulesets, Storage storage = null)
|
||||
: base(createContext, storage)
|
||||
public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null)
|
||||
: base(contextFactory, storage)
|
||||
{
|
||||
foreach (var info in rulesets.AvailableRulesets)
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
var ruleset = info.CreateInstance();
|
||||
foreach (var variant in ruleset.AvailableVariants)
|
||||
insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant);
|
||||
foreach (var info in rulesets.AvailableRulesets)
|
||||
{
|
||||
var ruleset = info.CreateInstance();
|
||||
foreach (var variant in ruleset.AvailableVariants)
|
||||
insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,9 +34,7 @@ namespace osu.Game.Input
|
||||
|
||||
private void insertDefaults(IEnumerable<KeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
using (var transaction = context.BeginTransaction())
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
// compare counts in database vs defaults
|
||||
foreach (var group in defaults.GroupBy(k => k.Action))
|
||||
@ -46,7 +47,7 @@ namespace osu.Game.Input
|
||||
|
||||
foreach (var insertable in group.Skip(count).Take(aimCount - count))
|
||||
// insert any defaults which are missing.
|
||||
context.DatabasedKeyBinding.Add(new DatabasedKeyBinding
|
||||
usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding
|
||||
{
|
||||
KeyCombination = insertable.KeyCombination,
|
||||
Action = insertable.Action,
|
||||
@ -54,8 +55,6 @@ namespace osu.Game.Input
|
||||
Variant = variant
|
||||
});
|
||||
}
|
||||
|
||||
context.SaveChanges(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,19 +65,20 @@ namespace osu.Game.Input
|
||||
/// <param name="variant">An optional variant.</param>
|
||||
/// <returns></returns>
|
||||
public List<DatabasedKeyBinding> Query(int? rulesetId = null, int? variant = null) =>
|
||||
GetContext().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||
|
||||
public void Update(KeyBinding keyBinding)
|
||||
{
|
||||
var dbKeyBinding = (DatabasedKeyBinding)keyBinding;
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
var dbKeyBinding = (DatabasedKeyBinding)keyBinding;
|
||||
Refresh(ref dbKeyBinding);
|
||||
|
||||
var context = GetContext();
|
||||
if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination))
|
||||
return;
|
||||
|
||||
Refresh(ref dbKeyBinding);
|
||||
|
||||
dbKeyBinding.KeyCombination = keyBinding.KeyCombination;
|
||||
|
||||
context.SaveChanges();
|
||||
dbKeyBinding.KeyCombination = keyBinding.KeyCombination;
|
||||
}
|
||||
|
||||
KeyBindingChanged?.Invoke();
|
||||
}
|
||||
|
379
osu.Game/Migrations/20180219060912_AddSkins.Designer.cs
generated
Normal file
379
osu.Game/Migrations/20180219060912_AddSkins.Designer.cs
generated
Normal file
@ -0,0 +1,379 @@
|
||||
// <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("20180219060912_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<bool>("DeletePending");
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
73
osu.Game/Migrations/20180219060912_AddSkins.cs
Normal file
73
osu.Game/Migrations/20180219060912_AddSkins.cs
Normal file
@ -0,0 +1,73 @@
|
||||
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),
|
||||
DeletePending = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
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,43 @@ 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<bool>("DeletePending");
|
||||
|
||||
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 +359,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
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
@ -16,7 +15,7 @@ using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
{
|
||||
public class APIAccess : IUpdateable
|
||||
public class APIAccess : IAPIProvider
|
||||
{
|
||||
private readonly OAuth authentication;
|
||||
|
||||
@ -34,7 +33,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
public string Password;
|
||||
|
||||
public Bindable<User> LocalUser = new Bindable<User>(createGuestUser());
|
||||
public Bindable<User> LocalUser { get; } = new Bindable<User>(createGuestUser());
|
||||
|
||||
public string Token
|
||||
{
|
||||
|
33
osu.Game/Online/API/APIDownloadRequest.cs
Normal file
33
osu.Game/Online/API/APIDownloadRequest.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// 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.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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
31
osu.Game/Online/API/DummyAPIAccess.cs
Normal file
31
osu.Game/Online/API/DummyAPIAccess.cs
Normal file
@ -0,0 +1,31 @@
|
||||
// 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.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
{
|
||||
public class DummyAPIAccess : IAPIProvider
|
||||
{
|
||||
public Bindable<User> LocalUser { get; } = new Bindable<User>(new User
|
||||
{
|
||||
Username = @"Dummy",
|
||||
Id = 1,
|
||||
});
|
||||
|
||||
public bool IsLoggedIn => true;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Queue(APIRequest request)
|
||||
{
|
||||
}
|
||||
|
||||
public void Register(IOnlineComponent component)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
34
osu.Game/Online/API/IAPIProvider.cs
Normal file
34
osu.Game/Online/API/IAPIProvider.cs
Normal file
@ -0,0 +1,34 @@
|
||||
// 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;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
{
|
||||
public interface IAPIProvider : IUpdateable
|
||||
{
|
||||
/// <summary>
|
||||
/// The local user.
|
||||
/// </summary>
|
||||
Bindable<User> LocalUser { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the local user is logged in.
|
||||
/// </summary>
|
||||
bool IsLoggedIn { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Queue a new request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request to perform.</param>
|
||||
void Queue(APIRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Register a component to receive state changes.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to register.</param>
|
||||
void Register(IOnlineComponent component);
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
@ -30,6 +31,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Skinning;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace osu.Game
|
||||
@ -79,6 +81,8 @@ namespace osu.Game
|
||||
private Bindable<int> configRuleset;
|
||||
public Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
|
||||
|
||||
private Bindable<int> configSkin;
|
||||
|
||||
private readonly string[] args;
|
||||
|
||||
private SettingsOverlay settings;
|
||||
@ -105,6 +109,8 @@ namespace osu.Game
|
||||
{
|
||||
this.frameworkConfig = frameworkConfig;
|
||||
|
||||
ScoreStore.ScoreImported += score => Schedule(() => LoadScore(score));
|
||||
|
||||
if (!Host.IsPrimaryInstance)
|
||||
{
|
||||
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
|
||||
@ -114,15 +120,24 @@ namespace osu.Game
|
||||
if (args?.Length > 0)
|
||||
{
|
||||
var paths = args.Where(a => !a.StartsWith(@"-"));
|
||||
Task.Run(() => BeatmapManager.Import(paths.ToArray()));
|
||||
|
||||
Task.Run(() => Import(paths.ToArray()));
|
||||
}
|
||||
|
||||
dependencies.CacheAs(this);
|
||||
|
||||
// bind config int to database RulesetInfo
|
||||
configRuleset = LocalConfig.GetBindable<int>(OsuSetting.Ruleset);
|
||||
Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First();
|
||||
Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0;
|
||||
|
||||
// bind config int to database SkinInfo
|
||||
configSkin = LocalConfig.GetBindable<int>(OsuSetting.Skin);
|
||||
|
||||
SkinManager.CurrentSkinInfo.ValueChanged += s => configSkin.Value = s.ID;
|
||||
configSkin.ValueChanged += id => SkinManager.CurrentSkinInfo.Value = SkinManager.Query(s => s.ID == id) ?? SkinInfo.Default;
|
||||
configSkin.TriggerChange();
|
||||
|
||||
LocalConfig.BindWith(OsuSetting.VolumeInactive, inactiveVolumeAdjust);
|
||||
}
|
||||
|
||||
@ -184,7 +199,9 @@ namespace osu.Game
|
||||
CursorOverrideContainer.CanShowCursor = currentScreen?.CursorVisible ?? false;
|
||||
|
||||
// hook up notifications to components.
|
||||
SkinManager.PostNotification = n => notifications?.Post(n);
|
||||
BeatmapManager.PostNotification = n => notifications?.Post(n);
|
||||
|
||||
BeatmapManager.GetStableStorage = GetStorageForStableInstall;
|
||||
|
||||
AddRange(new Drawable[]
|
||||
@ -398,6 +415,7 @@ namespace osu.Game
|
||||
sensitivity.Disabled = true;
|
||||
|
||||
frameworkConfig.Set(FrameworkSetting.ActiveInputHandlers, string.Empty);
|
||||
frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
|
||||
return true;
|
||||
case GlobalAction.ToggleToolbar:
|
||||
Toolbar.ToggleVisibility();
|
||||
|
@ -1,8 +1,11 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// 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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -27,15 +30,18 @@ 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
|
||||
{
|
||||
public class OsuGameBase : Framework.Game, IOnlineComponent
|
||||
public class OsuGameBase : Framework.Game, IOnlineComponent, ICanAcceptFiles
|
||||
{
|
||||
protected OsuConfigManager LocalConfig;
|
||||
|
||||
protected BeatmapManager BeatmapManager;
|
||||
|
||||
protected SkinManager SkinManager;
|
||||
|
||||
protected RulesetStore RulesetStore;
|
||||
|
||||
protected FileStore FileStore;
|
||||
@ -100,20 +106,27 @@ namespace osu.Game
|
||||
|
||||
runMigrations();
|
||||
|
||||
dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio));
|
||||
|
||||
dependencies.Cache(API = new APIAccess
|
||||
{
|
||||
Username = LocalConfig.Get<string>(OsuSetting.Username),
|
||||
Token = LocalConfig.Get<string>(OsuSetting.Token)
|
||||
});
|
||||
dependencies.CacheAs<IAPIProvider>(API);
|
||||
|
||||
dependencies.Cache(RulesetStore = new RulesetStore(contextFactory.GetContext));
|
||||
dependencies.Cache(FileStore = new FileStore(contextFactory.GetContext, Host.Storage));
|
||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory.GetContext, RulesetStore, API, Host));
|
||||
dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory.GetContext, Host, BeatmapManager, RulesetStore));
|
||||
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory.GetContext, RulesetStore));
|
||||
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory.GetContext));
|
||||
dependencies.Cache(RulesetStore = new RulesetStore(contextFactory));
|
||||
dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage));
|
||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Host));
|
||||
dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore));
|
||||
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
|
||||
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
|
||||
dependencies.Cache(new OsuColour());
|
||||
|
||||
fileImporters.Add(BeatmapManager);
|
||||
fileImporters.Add(ScoreStore);
|
||||
fileImporters.Add(SkinManager);
|
||||
|
||||
//this completely overrides the framework default. will need to change once we make a proper FontStore.
|
||||
dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 });
|
||||
|
||||
@ -179,8 +192,8 @@ namespace osu.Game
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var context = contextFactory.GetContext())
|
||||
context.Migrate();
|
||||
using (var db = contextFactory.GetForWrite())
|
||||
db.Context.Migrate();
|
||||
}
|
||||
catch (MigrationFailedException e)
|
||||
{
|
||||
@ -191,8 +204,8 @@ namespace osu.Game
|
||||
contextFactory.ResetDatabase();
|
||||
Logger.Log("Database purged successfully.", LoggingTarget.Database, LogLevel.Important);
|
||||
|
||||
using (var context = contextFactory.GetContext())
|
||||
context.Migrate();
|
||||
using (var db = contextFactory.GetForWrite())
|
||||
db.Context.Migrate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,7 +231,7 @@ namespace osu.Game
|
||||
CursorOverrideContainer.Child = globalBinding = new GlobalActionContainer(this)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both }
|
||||
Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both }
|
||||
};
|
||||
|
||||
base.Content.Add(new DrawSizePreservingFillContainer { Child = CursorOverrideContainer });
|
||||
@ -257,5 +270,17 @@ namespace osu.Game
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();
|
||||
|
||||
public void Import(params string[] paths)
|
||||
{
|
||||
var extension = Path.GetExtension(paths.First());
|
||||
|
||||
foreach (var importer in fileImporters)
|
||||
if (importer.HandledExtensions.Contains(extension)) importer.Import(paths);
|
||||
}
|
||||
|
||||
public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -477,7 +477,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
if (!api.IsLoggedIn)
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Please login to participate in chat!"));
|
||||
target.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Direct
|
||||
public Track Preview { get; private set; }
|
||||
|
||||
private BeatmapSetInfo beatmapSet;
|
||||
|
||||
public BeatmapSetInfo BeatmapSet
|
||||
{
|
||||
get { return beatmapSet; }
|
||||
@ -199,8 +200,7 @@ namespace osu.Game.Overlays.Direct
|
||||
// add back the user's music volume setting (since we are no longer in the global TrackManager's hierarchy).
|
||||
config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume);
|
||||
|
||||
if (!string.IsNullOrEmpty(preview))
|
||||
Preview = trackManager.Get(preview);
|
||||
Preview = trackManager.Get(preview);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -185,7 +185,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
resultCountsContainer.Colour = colours.Yellow;
|
||||
|
||||
beatmaps.BeatmapSetAdded += setAdded;
|
||||
beatmaps.ItemAdded += setAdded;
|
||||
}
|
||||
|
||||
private void setAdded(BeatmapSetInfo set)
|
||||
|
@ -65,6 +65,14 @@ namespace osu.Game.Overlays.Mods
|
||||
Ruleset.TriggerChange();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
Ruleset.UnbindAll();
|
||||
SelectedMods.UnbindAll();
|
||||
}
|
||||
|
||||
private void selectedModsChanged(IEnumerable<Mod> obj)
|
||||
{
|
||||
foreach (ModSection section in ModSectionsContainer.Children)
|
||||
|
@ -34,7 +34,19 @@ namespace osu.Game.Overlays.Music
|
||||
|
||||
public Action<BeatmapSetInfo> OnSelect;
|
||||
|
||||
public bool IsDraggable => handle.IsHovered;
|
||||
public bool IsDraggable { get; private set; }
|
||||
|
||||
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
|
||||
{
|
||||
IsDraggable = handle.IsHovered;
|
||||
return base.OnMouseDown(state, args);
|
||||
}
|
||||
|
||||
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
|
||||
{
|
||||
IsDraggable = false;
|
||||
return base.OnMouseUp(state, args);
|
||||
}
|
||||
|
||||
private bool selected;
|
||||
public bool Selected
|
||||
|
@ -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();
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
@ -65,9 +64,12 @@ namespace osu.Game.Overlays
|
||||
AlwaysPresent = true;
|
||||
}
|
||||
|
||||
private Vector2 dragStart;
|
||||
|
||||
protected override bool OnDragStart(InputState state)
|
||||
{
|
||||
base.OnDragStart(state);
|
||||
dragStart = state.Mouse.Position;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -75,9 +77,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
if (base.OnDrag(state)) return true;
|
||||
|
||||
Trace.Assert(state.Mouse.PositionMouseDown != null, "state.Mouse.PositionMouseDown != null");
|
||||
|
||||
Vector2 change = state.Mouse.Position - state.Mouse.PositionMouseDown.Value;
|
||||
Vector2 change = state.Mouse.Position - dragStart;
|
||||
|
||||
// Diminish the drag distance as we go further to simulate "rubber band" feeling.
|
||||
change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length;
|
||||
|
@ -105,7 +105,7 @@ namespace osu.Game.Overlays.Profile
|
||||
return;
|
||||
}
|
||||
|
||||
int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Rank };
|
||||
int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Ranks.Global };
|
||||
ranks = userRanks.Select((x, index) => new KeyValuePair<int, int>(index, x)).Where(x => x.Value != 0).ToArray();
|
||||
|
||||
if (ranks.Length > 1)
|
||||
@ -124,9 +124,11 @@ namespace osu.Game.Overlays.Profile
|
||||
|
||||
private void updateRankTexts()
|
||||
{
|
||||
rankText.Text = User.Value.Statistics.Rank > 0 ? $"#{User.Value.Statistics.Rank:#,0}" : "no rank";
|
||||
performanceText.Text = User.Value.Statistics.PP != null ? $"{User.Value.Statistics.PP:#,0}pp" : string.Empty;
|
||||
relativeText.Text = $"{User.Value.Country?.FullName} #{User.Value.CountryRank:#,0}";
|
||||
var user = User.Value;
|
||||
|
||||
performanceText.Text = user.Statistics.PP != null ? $"{user.Statistics.PP:#,0}pp" : string.Empty;
|
||||
rankText.Text = user.Statistics.Ranks.Global > 0 ? $"#{user.Statistics.Ranks.Global:#,0}" : "no rank";
|
||||
relativeText.Text = user.Country != null && user.Statistics.Ranks.Country > 0 ? $"{user.Country.FullName} #{user.Statistics.Ranks.Country:#,0}" : "no rank";
|
||||
}
|
||||
|
||||
private void showHistoryRankTexts(int dayIndex)
|
||||
|
@ -208,7 +208,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
{
|
||||
username = new OsuTextBox
|
||||
{
|
||||
PlaceholderText = "Username",
|
||||
PlaceholderText = "Email address",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = api?.Username ?? string.Empty,
|
||||
TabbableContentContainer = this
|
||||
@ -222,12 +222,12 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Remember username",
|
||||
LabelText = "Remember email address",
|
||||
Bindable = config.GetBindable<bool>(OsuSetting.SaveUsername),
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Stay logged in",
|
||||
LabelText = "Stay signed in",
|
||||
Bindable = config.GetBindable<bool>(OsuSetting.SavePassword),
|
||||
},
|
||||
new SettingsButton
|
||||
@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = "Register new account",
|
||||
Text = "Register",
|
||||
//Action = registerLink
|
||||
}
|
||||
};
|
||||
|
@ -33,6 +33,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
LabelText = "Cursor Sensitivity",
|
||||
Bindable = config.GetBindable<double>(FrameworkSetting.CursorSensitivity)
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Map absolute input to window",
|
||||
Bindable = config.GetBindable<bool>(FrameworkSetting.MapAbsoluteInputToWindow)
|
||||
},
|
||||
new SettingsEnumDropdown<ConfineMouseMode>
|
||||
{
|
||||
LabelText = "Confine mouse cursor to window",
|
||||
@ -88,6 +93,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
// this bindable will still act as the "interactive" bindable displayed during a drag.
|
||||
base.Bindable = new BindableDouble(doubleValue.Value)
|
||||
{
|
||||
Default = doubleValue.Default,
|
||||
MinValue = doubleValue.MinValue,
|
||||
MaxValue = doubleValue.MaxValue
|
||||
};
|
||||
|
@ -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.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,26 +1,33 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Skinning;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections
|
||||
{
|
||||
public class SkinSection : SettingsSection
|
||||
{
|
||||
private SettingsDropdown<int> skinDropdown;
|
||||
|
||||
public override string Header => "Skin";
|
||||
|
||||
public override FontAwesome Icon => FontAwesome.fa_paint_brush;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
private void load(OsuConfigManager config, SkinManager skins)
|
||||
{
|
||||
FlowContent.Spacing = new Vector2(0, 5);
|
||||
Children = new Drawable[]
|
||||
{
|
||||
skinDropdown = new SettingsDropdown<int>(),
|
||||
new SettingsSlider<double, SizeSlider>
|
||||
{
|
||||
LabelText = "Menu cursor size",
|
||||
@ -39,6 +46,14 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
Bindable = config.GetBindable<bool>(OsuSetting.AutoCursorSize)
|
||||
},
|
||||
};
|
||||
|
||||
void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair<string, int>(s.Name, s.ID));
|
||||
skins.ItemAdded += _ => reloadSkins();
|
||||
skins.ItemRemoved += _ => reloadSkins();
|
||||
|
||||
reloadSkins();
|
||||
|
||||
skinDropdown.Bindable = config.GetBindable<int>(OsuSetting.Skin);
|
||||
}
|
||||
|
||||
private class SizeSlider : OsuSliderBar<double>
|
||||
|
@ -181,7 +181,7 @@ namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
hovering = true;
|
||||
UpdateState();
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(InputState state)
|
||||
|
@ -4,14 +4,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Layers;
|
||||
using osu.Game.Rulesets.Edit.Layers.Selection;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -25,20 +24,24 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
protected ICompositionTool CurrentTool { get; private set; }
|
||||
|
||||
private RulesetContainer rulesetContainer;
|
||||
private readonly List<Container> layerContainers = new List<Container>();
|
||||
|
||||
protected HitObjectComposer(Ruleset ruleset)
|
||||
{
|
||||
this.ruleset = ruleset;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGameBase osuGame)
|
||||
{
|
||||
RulesetContainer rulesetContainer;
|
||||
try
|
||||
{
|
||||
rulesetContainer = CreateRulesetContainer(ruleset, osuGame.Beatmap.Value);
|
||||
|
||||
// TODO: should probably be done at a RulesetContainer level to share logic with Player.
|
||||
rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -46,6 +49,26 @@ namespace osu.Game.Rulesets.Edit
|
||||
return;
|
||||
}
|
||||
|
||||
HitObjectOverlayLayer hitObjectOverlayLayer = CreateHitObjectOverlayLayer();
|
||||
SelectionLayer selectionLayer = new SelectionLayer(rulesetContainer.Playfield);
|
||||
|
||||
var layerBelowRuleset = new BorderLayer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = CreateLayerContainer()
|
||||
};
|
||||
|
||||
var layerAboveRuleset = CreateLayerContainer();
|
||||
layerAboveRuleset.Children = new Drawable[]
|
||||
{
|
||||
selectionLayer, // Below object overlays for input
|
||||
hitObjectOverlayLayer,
|
||||
selectionLayer.CreateProxy() // Proxy above object overlays for selections
|
||||
};
|
||||
|
||||
layerContainers.Add(layerBelowRuleset);
|
||||
layerContainers.Add(layerAboveRuleset);
|
||||
|
||||
RadioButtonCollection toolboxCollection;
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
@ -66,20 +89,13 @@ namespace osu.Game.Rulesets.Edit
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "Content",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = 2,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
layerBelowRuleset,
|
||||
rulesetContainer,
|
||||
new SelectionLayer(rulesetContainer.Playfield)
|
||||
layerAboveRuleset
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -90,7 +106,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
}
|
||||
};
|
||||
|
||||
rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock());
|
||||
selectionLayer.ObjectSelected += hitObjectOverlayLayer.AddOverlay;
|
||||
selectionLayer.ObjectDeselected += hitObjectOverlayLayer.RemoveOverlay;
|
||||
|
||||
toolboxCollection.Items =
|
||||
new[] { new RadioButton("Select", () => setCompositionTool(null)) }
|
||||
@ -102,10 +119,33 @@ namespace osu.Game.Rulesets.Edit
|
||||
toolboxCollection.Items[0].Select();
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
layerContainers.ForEach(l =>
|
||||
{
|
||||
l.Anchor = rulesetContainer.Playfield.Anchor;
|
||||
l.Origin = rulesetContainer.Playfield.Origin;
|
||||
l.Position = rulesetContainer.Playfield.Position;
|
||||
l.Size = rulesetContainer.Playfield.Size;
|
||||
});
|
||||
}
|
||||
|
||||
private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool;
|
||||
|
||||
protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true);
|
||||
|
||||
protected abstract IReadOnlyList<ICompositionTool> CompositionTools { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ScalableContainer"/> which provides a layer above or below the <see cref="Playfield"/>.
|
||||
/// </summary>
|
||||
protected virtual ScalableContainer CreateLayerContainer() => new ScalableContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="HitObjectOverlayLayer"/> which overlays selected <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
protected virtual HitObjectOverlayLayer CreateHitObjectOverlayLayer() => new HitObjectOverlayLayer();
|
||||
}
|
||||
}
|
||||
|
38
osu.Game/Rulesets/Edit/Layers/BorderLayer.cs
Normal file
38
osu.Game/Rulesets/Edit/Layers/BorderLayer.cs
Normal 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers
|
||||
{
|
||||
public class BorderLayer : Container
|
||||
{
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
|
||||
public BorderLayer()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Name = "Border",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = 2,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
},
|
||||
content = new Container { RelativeSizeAxes = Axes.Both }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
68
osu.Game/Rulesets/Edit/Layers/Selection/CaptureBox.cs
Normal file
68
osu.Game/Rulesets/Edit/Layers/Selection/CaptureBox.cs
Normal file
@ -0,0 +1,68 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
/// <summary>
|
||||
/// A box which encloses <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public class CaptureBox : VisibilityContainer
|
||||
{
|
||||
private readonly IDrawable captureArea;
|
||||
private readonly IReadOnlyList<DrawableHitObject> capturedObjects;
|
||||
|
||||
public CaptureBox(IDrawable captureArea, IReadOnlyList<DrawableHitObject> capturedObjects)
|
||||
{
|
||||
this.captureArea = captureArea;
|
||||
this.capturedObjects = capturedObjects;
|
||||
|
||||
Masking = true;
|
||||
BorderThickness = SelectionBox.BORDER_RADIUS;
|
||||
|
||||
InternalChild = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
};
|
||||
|
||||
State = Visibility.Visible;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BorderColour = colours.Yellow;
|
||||
|
||||
// Move the rectangle to cover the hitobjects
|
||||
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
|
||||
var bottomRight = new Vector2(float.MinValue, float.MinValue);
|
||||
|
||||
foreach (var obj in capturedObjects)
|
||||
{
|
||||
topLeft = Vector2.ComponentMin(topLeft, captureArea.ToLocalSpace(obj.SelectionQuad.TopLeft));
|
||||
bottomRight = Vector2.ComponentMax(bottomRight, captureArea.ToLocalSpace(obj.SelectionQuad.BottomRight));
|
||||
}
|
||||
|
||||
topLeft -= new Vector2(5);
|
||||
bottomRight += new Vector2(5);
|
||||
|
||||
Size = bottomRight - topLeft;
|
||||
Position = topLeft;
|
||||
}
|
||||
|
||||
public override bool DisposeOnDeathRemoval => true;
|
||||
|
||||
protected override void PopIn() => this.FadeIn();
|
||||
protected override void PopOut() => this.FadeOut();
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Graphics;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a marker visible on the border of a <see cref="HandleContainer"/> which exposes
|
||||
/// properties that are used to resize a <see cref="HitObjectSelectionBox"/>.
|
||||
/// </summary>
|
||||
public class Handle : CompositeDrawable
|
||||
{
|
||||
private const float marker_size = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="Handle"/> requires the current drag rectangle.
|
||||
/// </summary>
|
||||
public Func<RectangleF> GetDragRectangle;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="Handle"/> wants to update the drag rectangle.
|
||||
/// </summary>
|
||||
public Action<RectangleF> UpdateDragRectangle;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="Handle"/> has finished updates to the drag rectangle.
|
||||
/// </summary>
|
||||
public Action FinishDrag;
|
||||
|
||||
private Color4 normalColour;
|
||||
private Color4 hoverColour;
|
||||
|
||||
public Handle()
|
||||
{
|
||||
Size = new Vector2(marker_size);
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Child = new Box { RelativeSizeAxes = Axes.Both }
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Colour = normalColour = colours.Yellow;
|
||||
hoverColour = colours.YellowDarker;
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(InputState state) => true;
|
||||
|
||||
protected override bool OnDrag(InputState state)
|
||||
{
|
||||
var currentRectangle = GetDragRectangle();
|
||||
|
||||
float left = currentRectangle.Left;
|
||||
float right = currentRectangle.Right;
|
||||
float top = currentRectangle.Top;
|
||||
float bottom = currentRectangle.Bottom;
|
||||
|
||||
// Apply modifications to the capture rectangle
|
||||
if ((Anchor & Anchor.y0) > 0)
|
||||
top += state.Mouse.Delta.Y;
|
||||
else if ((Anchor & Anchor.y2) > 0)
|
||||
bottom += state.Mouse.Delta.Y;
|
||||
|
||||
if ((Anchor & Anchor.x0) > 0)
|
||||
left += state.Mouse.Delta.X;
|
||||
else if ((Anchor & Anchor.x2) > 0)
|
||||
right += state.Mouse.Delta.X;
|
||||
|
||||
UpdateDragRectangle(RectangleF.FromLTRB(left, top, right, bottom));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnDragEnd(InputState state)
|
||||
{
|
||||
FinishDrag();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnHover(InputState state)
|
||||
{
|
||||
this.FadeColour(hoverColour, 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(InputState state)
|
||||
{
|
||||
this.FadeColour(normalColour, 100);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="CompositeDrawable"/> that has <see cref="Handle"/>s around its border.
|
||||
/// </summary>
|
||||
public class HandleContainer : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="Handle"/> requires the current drag rectangle.
|
||||
/// </summary>
|
||||
public Func<RectangleF> GetDragRectangle;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="Handle"/> wants to update the drag rectangle.
|
||||
/// </summary>
|
||||
public Action<RectangleF> UpdateDragRectangle;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="Handle"/> has finished updates to the drag rectangle.
|
||||
/// </summary>
|
||||
public Action FinishDrag;
|
||||
|
||||
public HandleContainer()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Handle
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new Handle
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new Handle
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new Handle
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new Handle
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new Handle
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new Handle
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new Handle
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new OriginHandle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
};
|
||||
|
||||
InternalChildren.OfType<Handle>().ForEach(m =>
|
||||
{
|
||||
m.GetDragRectangle = () => GetDragRectangle();
|
||||
m.UpdateDragRectangle = r => UpdateDragRectangle(r);
|
||||
m.FinishDrag = () => FinishDrag();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
25
osu.Game/Rulesets/Edit/Layers/Selection/HitObjectOverlay.cs
Normal file
25
osu.Game/Rulesets/Edit/Layers/Selection/HitObjectOverlay.cs
Normal 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 osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
public class HitObjectOverlay : OverlayContainer
|
||||
{
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
// This will be used later to handle drag movement, etc
|
||||
private readonly DrawableHitObject hitObject;
|
||||
|
||||
public HitObjectOverlay(DrawableHitObject hitObject)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
|
||||
State = Visibility.Visible;
|
||||
}
|
||||
|
||||
protected override void PopIn() => Alpha = 1;
|
||||
protected override void PopOut() => Alpha = 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
// 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
public class HitObjectOverlayLayer : CompositeDrawable
|
||||
{
|
||||
private readonly Dictionary<DrawableHitObject, HitObjectOverlay> existingOverlays = new Dictionary<DrawableHitObject, HitObjectOverlay>();
|
||||
|
||||
public HitObjectOverlayLayer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an overlay for a <see cref="DrawableHitObject"/> which adds movement support.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to create an overlay for.</param>
|
||||
public void AddOverlay(DrawableHitObject hitObject)
|
||||
{
|
||||
var overlay = CreateOverlayFor(hitObject);
|
||||
if (overlay == null)
|
||||
return;
|
||||
|
||||
existingOverlays[hitObject] = overlay;
|
||||
AddInternal(overlay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the overlay for a <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to remove the overlay for.</param>
|
||||
public void RemoveOverlay(DrawableHitObject hitObject)
|
||||
{
|
||||
if (!existingOverlays.TryGetValue(hitObject, out var existing))
|
||||
return;
|
||||
|
||||
existing.Hide();
|
||||
existing.Expire();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HitObjectOverlay"/> for a specific <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to create the overlay for.</param>
|
||||
protected virtual HitObjectOverlay CreateOverlayFor(DrawableHitObject hitObject) => null;
|
||||
}
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
using osu.Framework.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
/// <summary>
|
||||
/// A box that represents a drag selection.
|
||||
/// </summary>
|
||||
public class HitObjectSelectionBox : CompositeDrawable
|
||||
{
|
||||
public readonly Bindable<SelectionInfo> Selection = new Bindable<SelectionInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DrawableHitObject"/>s that can be selected through a drag-selection.
|
||||
/// </summary>
|
||||
public IEnumerable<DrawableHitObject> CapturableObjects;
|
||||
|
||||
private readonly Container borderMask;
|
||||
private readonly Drawable background;
|
||||
private readonly HandleContainer handles;
|
||||
|
||||
private Color4 captureFinishedColour;
|
||||
|
||||
private readonly Vector2 startPos;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HitObjectSelectionBox"/>.
|
||||
/// </summary>
|
||||
/// <param name="startPos">The point at which the drag was initiated, in the parent's coordinates.</param>
|
||||
public HitObjectSelectionBox(Vector2 startPos)
|
||||
{
|
||||
this.startPos = startPos;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(-1),
|
||||
Child = borderMask = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = 2,
|
||||
MaskingSmoothness = 1,
|
||||
Child = background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.1f,
|
||||
AlwaysPresent = true
|
||||
},
|
||||
}
|
||||
},
|
||||
handles = new HandleContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
GetDragRectangle = () => dragRectangle,
|
||||
UpdateDragRectangle = updateDragRectangle,
|
||||
FinishDrag = FinishCapture
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
captureFinishedColour = colours.Yellow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The secondary corner of the drag selection box. A rectangle will be fit between the starting position and this value.
|
||||
/// </summary>
|
||||
public Vector2 DragEndPosition { set => updateDragRectangle(RectangleF.FromLTRB(startPos.X, startPos.Y, value.X, value.Y)); }
|
||||
|
||||
private RectangleF dragRectangle;
|
||||
private void updateDragRectangle(RectangleF rectangle)
|
||||
{
|
||||
dragRectangle = rectangle;
|
||||
|
||||
Position = new Vector2(
|
||||
Math.Min(rectangle.Left, rectangle.Right),
|
||||
Math.Min(rectangle.Top, rectangle.Bottom));
|
||||
|
||||
Size = new Vector2(
|
||||
Math.Max(rectangle.Left, rectangle.Right) - Position.X,
|
||||
Math.Max(rectangle.Top, rectangle.Bottom) - Position.Y);
|
||||
}
|
||||
|
||||
private readonly List<DrawableHitObject> capturedHitObjects = new List<DrawableHitObject>();
|
||||
|
||||
/// <summary>
|
||||
/// Processes hitobjects to determine which ones are captured by the drag selection.
|
||||
/// Captured hitobjects will be enclosed by the drag selection upon <see cref="FinishCapture"/>.
|
||||
/// </summary>
|
||||
public void BeginCapture()
|
||||
{
|
||||
capturedHitObjects.Clear();
|
||||
|
||||
foreach (var obj in CapturableObjects)
|
||||
{
|
||||
if (!obj.IsAlive || !obj.IsPresent)
|
||||
continue;
|
||||
|
||||
if (ScreenSpaceDrawQuad.Contains(obj.SelectionPoint))
|
||||
capturedHitObjects.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encloses hitobjects captured through <see cref="BeginCapture"/> in the drag selection box.
|
||||
/// </summary>
|
||||
public void FinishCapture()
|
||||
{
|
||||
if (capturedHitObjects.Count == 0)
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the rectangle to cover the hitobjects
|
||||
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
|
||||
var bottomRight = new Vector2(float.MinValue, float.MinValue);
|
||||
|
||||
foreach (var obj in capturedHitObjects)
|
||||
{
|
||||
topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.SelectionQuad.TopLeft));
|
||||
bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.SelectionQuad.BottomRight));
|
||||
}
|
||||
|
||||
topLeft -= new Vector2(5);
|
||||
bottomRight += new Vector2(5);
|
||||
|
||||
this.MoveTo(topLeft, 200, Easing.OutQuint)
|
||||
.ResizeTo(bottomRight - topLeft, 200, Easing.OutQuint);
|
||||
|
||||
dragRectangle = RectangleF.FromLTRB(topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y);
|
||||
|
||||
borderMask.BorderThickness = 3;
|
||||
borderMask.FadeColour(captureFinishedColour, 200);
|
||||
|
||||
// Transform into markers to let the user modify the drag selection further.
|
||||
background.Delay(50).FadeOut(200);
|
||||
handles.FadeIn(200);
|
||||
|
||||
Selection.Value = new SelectionInfo
|
||||
{
|
||||
Objects = capturedHitObjects,
|
||||
SelectionQuad = Parent.ToScreenSpace(dragRectangle)
|
||||
};
|
||||
}
|
||||
|
||||
private bool isActive = true;
|
||||
public override bool HandleKeyboardInput => isActive;
|
||||
public override bool HandleMouseInput => isActive;
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
isActive = false;
|
||||
this.FadeOut(400, Easing.OutQuint).Expire();
|
||||
|
||||
Selection.Value = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the origin of a <see cref="HandleContainer"/>.
|
||||
/// </summary>
|
||||
public class OriginHandle : CompositeDrawable
|
||||
{
|
||||
private const float marker_size = 10;
|
||||
private const float line_width = 2;
|
||||
|
||||
public OriginHandle()
|
||||
{
|
||||
Size = new Vector2(marker_size);
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = line_width
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = line_width
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Colour = colours.Yellow;
|
||||
}
|
||||
}
|
||||
}
|
49
osu.Game/Rulesets/Edit/Layers/Selection/SelectionBox.cs
Normal file
49
osu.Game/Rulesets/Edit/Layers/Selection/SelectionBox.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// 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;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
/// <summary>
|
||||
/// A box that represents a drag selection.
|
||||
/// </summary>
|
||||
public class SelectionBox : VisibilityContainer
|
||||
{
|
||||
public const float BORDER_RADIUS = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SelectionBox"/>.
|
||||
/// </summary>
|
||||
public SelectionBox()
|
||||
{
|
||||
Masking = true;
|
||||
BorderColour = Color4.White;
|
||||
BorderThickness = BORDER_RADIUS;
|
||||
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.1f
|
||||
};
|
||||
}
|
||||
|
||||
public void SetDragRectangle(RectangleF rectangle)
|
||||
{
|
||||
var topLeft = Parent.ToLocalSpace(rectangle.TopLeft);
|
||||
var bottomRight = Parent.ToLocalSpace(rectangle.BottomRight);
|
||||
|
||||
Position = topLeft;
|
||||
Size = bottomRight - topLeft;
|
||||
}
|
||||
|
||||
public override bool DisposeOnDeathRemoval => true;
|
||||
|
||||
protected override void PopIn() => this.FadeIn(250, Easing.OutQuint);
|
||||
protected override void PopOut() => this.FadeOut(250, Easing.OutQuint);
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
// 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 osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
public class SelectionInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The objects that are captured by the selection.
|
||||
/// </summary>
|
||||
public IEnumerable<DrawableHitObject> Objects;
|
||||
|
||||
/// <summary>
|
||||
/// The screen space quad of the selection box surrounding <see cref="Objects"/>.
|
||||
/// </summary>
|
||||
public Quad SelectionQuad;
|
||||
}
|
||||
}
|
@ -1,17 +1,31 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
{
|
||||
public class SelectionLayer : CompositeDrawable
|
||||
{
|
||||
public readonly Bindable<SelectionInfo> Selection = new Bindable<SelectionInfo>();
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> is selected.
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject> ObjectSelected;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> is deselected.
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject> ObjectDeselected;
|
||||
|
||||
private readonly Playfield playfield;
|
||||
|
||||
@ -22,40 +36,162 @@ namespace osu.Game.Rulesets.Edit.Layers.Selection
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
private HitObjectSelectionBox selectionBoxBox;
|
||||
private SelectionBox selectionBox;
|
||||
private CaptureBox captureBox;
|
||||
|
||||
private readonly HashSet<DrawableHitObject> selectedHitObjects = new HashSet<DrawableHitObject>();
|
||||
|
||||
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
|
||||
{
|
||||
DeselectAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(InputState state)
|
||||
{
|
||||
// Hide the previous drag box - we won't be working with it any longer
|
||||
selectionBoxBox?.Hide();
|
||||
|
||||
AddInternal(selectionBoxBox = new HitObjectSelectionBox(ToLocalSpace(state.Mouse.NativeState.Position))
|
||||
{
|
||||
CapturableObjects = playfield.HitObjects.Objects,
|
||||
});
|
||||
|
||||
Selection.BindTo(selectionBoxBox.Selection);
|
||||
|
||||
AddInternal(selectionBox = new SelectionBox());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnDrag(InputState state)
|
||||
{
|
||||
selectionBoxBox.DragEndPosition = ToLocalSpace(state.Mouse.NativeState.Position);
|
||||
selectionBoxBox.BeginCapture();
|
||||
selectionBox.Show();
|
||||
|
||||
var dragPosition = state.Mouse.NativeState.Position;
|
||||
var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition;
|
||||
|
||||
var screenSpaceDragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y);
|
||||
|
||||
selectionBox.SetDragRectangle(screenSpaceDragQuad.AABBFloat);
|
||||
selectQuad(screenSpaceDragQuad);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnDragEnd(InputState state)
|
||||
{
|
||||
selectionBoxBox.FinishCapture();
|
||||
selectionBox.Hide();
|
||||
selectionBox.Expire();
|
||||
|
||||
finishSelection();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnClick(InputState state)
|
||||
{
|
||||
selectionBoxBox?.Hide();
|
||||
selectPoint(state.Mouse.NativeState.Position);
|
||||
finishSelection();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to select.</param>
|
||||
public void Select(DrawableHitObject hitObject)
|
||||
{
|
||||
if (!select(hitObject))
|
||||
return;
|
||||
|
||||
clearCapture();
|
||||
finishSelection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a <see cref="DrawableHitObject"/> without performing capture updates.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to select.</param>
|
||||
/// <returns>Whether <paramref name="hitObject"/> was selected.</returns>
|
||||
private bool select(DrawableHitObject hitObject)
|
||||
{
|
||||
if (!selectedHitObjects.Add(hitObject))
|
||||
return false;
|
||||
|
||||
ObjectSelected?.Invoke(hitObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deselects a <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to deselect.</param>
|
||||
public void Deselect(DrawableHitObject hitObject)
|
||||
{
|
||||
if (!deselect(hitObject))
|
||||
return;
|
||||
|
||||
clearCapture();
|
||||
finishSelection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deselects a <see cref="DrawableHitObject"/> without performing capture updates.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to deselect.</param>
|
||||
/// <returns>Whether the <see cref="DrawableHitObject"/> was deselected.</returns>
|
||||
private bool deselect(DrawableHitObject hitObject)
|
||||
{
|
||||
if (!selectedHitObjects.Remove(hitObject))
|
||||
return false;
|
||||
|
||||
ObjectDeselected?.Invoke(hitObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deselects all selected <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public void DeselectAll()
|
||||
{
|
||||
selectedHitObjects.ForEach(h => ObjectDeselected?.Invoke(h));
|
||||
selectedHitObjects.Clear();
|
||||
|
||||
clearCapture();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects all hitobjects that are present within the area of a <see cref="Quad"/>.
|
||||
/// </summary>
|
||||
/// <param name="screenSpaceQuad">The selection <see cref="Quad"/>.</param>
|
||||
// Todo: If needed we can severely reduce allocations in this method
|
||||
private void selectQuad(Quad screenSpaceQuad)
|
||||
{
|
||||
var expectedSelection = playfield.HitObjects.Objects.Where(h => h.IsAlive && h.IsPresent && screenSpaceQuad.Contains(h.SelectionPoint)).ToList();
|
||||
|
||||
var toRemove = selectedHitObjects.Except(expectedSelection).ToList();
|
||||
foreach (var obj in toRemove)
|
||||
deselect(obj);
|
||||
|
||||
expectedSelection.ForEach(h => select(h));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the top-most hitobject that is present under a specific point.
|
||||
/// </summary>
|
||||
/// <param name="screenSpacePoint">The <see cref="Vector2"/> to select at.</param>
|
||||
private void selectPoint(Vector2 screenSpacePoint)
|
||||
{
|
||||
var target = playfield.HitObjects.Objects.Reverse().Where(h => h.IsAlive && h.IsPresent).FirstOrDefault(h => h.ReceiveMouseInputAt(screenSpacePoint));
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
select(target);
|
||||
}
|
||||
|
||||
private void clearCapture()
|
||||
{
|
||||
captureBox?.Hide();
|
||||
captureBox?.Expire();
|
||||
}
|
||||
|
||||
private void finishSelection()
|
||||
{
|
||||
if (selectedHitObjects.Count == 0)
|
||||
return;
|
||||
|
||||
AddInternal(captureBox = new CaptureBox(this, selectedHitObjects.ToList()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,20 +3,19 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using Container = osu.Framework.Graphics.Containers.Container;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using OpenTK.Graphics;
|
||||
using osu.Game.Audio;
|
||||
using System.Linq;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using OpenTK;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Drawables
|
||||
{
|
||||
@ -32,7 +31,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
// Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first
|
||||
protected virtual string SampleNamespace => null;
|
||||
|
||||
protected List<SampleChannel> Samples = new List<SampleChannel>();
|
||||
protected SkinnableSound Samples;
|
||||
|
||||
protected virtual IEnumerable<SampleInfo> GetSamples() => HitObject.Samples;
|
||||
|
||||
private List<DrawableHitObject> nestedHitObjects;
|
||||
@ -83,31 +83,22 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
private void load()
|
||||
{
|
||||
var samples = GetSamples();
|
||||
var samples = GetSamples().ToArray();
|
||||
|
||||
if (samples.Any())
|
||||
{
|
||||
if (HitObject.SampleControlPoint == null)
|
||||
throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
|
||||
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
||||
|
||||
foreach (SampleInfo s in samples)
|
||||
AddInternal(Samples = new SkinnableSound(samples.Select(s => new SampleInfo
|
||||
{
|
||||
SampleInfo localSampleInfo = new SampleInfo
|
||||
{
|
||||
Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank,
|
||||
Name = s.Name,
|
||||
Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume
|
||||
};
|
||||
|
||||
SampleChannel channel = localSampleInfo.GetChannel(audio.Sample, SampleNamespace);
|
||||
|
||||
if (channel == null)
|
||||
continue;
|
||||
|
||||
Samples.Add(channel);
|
||||
}
|
||||
Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank,
|
||||
Name = s.Name,
|
||||
Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume,
|
||||
Namespace = SampleNamespace
|
||||
}).ToArray()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,7 +130,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <summary>
|
||||
/// Plays all the hitsounds for this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
public void PlaySamples() => Samples.ForEach(s => s?.Play());
|
||||
public void PlaySamples() => Samples?.Play();
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@ -221,10 +212,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
return false;
|
||||
|
||||
if (NestedHitObjects != null)
|
||||
{
|
||||
foreach (var d in NestedHitObjects)
|
||||
judgementOccurred |= d.UpdateJudgement(userTriggered);
|
||||
}
|
||||
|
||||
if (!ProvidesJudgement || judgementFinalized || judgementOccurred)
|
||||
return judgementOccurred;
|
||||
|
@ -49,6 +49,19 @@ namespace osu.Game.Rulesets.Objects
|
||||
[JsonIgnore]
|
||||
public bool Kiai { get; private set; }
|
||||
|
||||
private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY;
|
||||
|
||||
private HitWindows hitWindows;
|
||||
|
||||
/// <summary>
|
||||
/// The hit windows for this <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
public HitWindows HitWindows
|
||||
{
|
||||
get => hitWindows ?? (hitWindows = new HitWindows(overallDifficulty));
|
||||
protected set => hitWindows = value;
|
||||
}
|
||||
|
||||
private readonly SortedList<HitObject> nestedHitObjects = new SortedList<HitObject>((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
|
||||
|
||||
[JsonIgnore]
|
||||
@ -75,6 +88,9 @@ namespace osu.Game.Rulesets.Objects
|
||||
|
||||
Kiai = effectPoint.KiaiMode;
|
||||
SampleControlPoint = samplePoint;
|
||||
|
||||
overallDifficulty = difficulty.OverallDifficulty;
|
||||
hitWindows = null;
|
||||
}
|
||||
|
||||
protected virtual void CreateNestedHitObjects()
|
||||
|
173
osu.Game/Rulesets/Objects/HitWindows.cs
Normal file
173
osu.Game/Rulesets/Objects/HitWindows.cs
Normal file
@ -0,0 +1,173 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
public class HitWindows
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)>
|
||||
{
|
||||
{ HitResult.Perfect, (44.8, 38.8, 27.8) },
|
||||
{ HitResult.Great, (128, 98, 68 ) },
|
||||
{ HitResult.Good, (194, 164, 134) },
|
||||
{ HitResult.Ok, (254, 224, 194) },
|
||||
{ HitResult.Meh, (302, 272, 242) },
|
||||
{ HitResult.Miss, (376, 346, 316) },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Hit window for a <see cref="HitResult.Perfect"/> result.
|
||||
/// The user can only achieve receive this result if <see cref="AllowsPerfect"/> is true.
|
||||
/// </summary>
|
||||
public double Perfect { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hit window for a <see cref="HitResult.Great"/> result.
|
||||
/// </summary>
|
||||
public double Great { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hit window for a <see cref="HitResult.Good"/> result.
|
||||
/// </summary>
|
||||
public double Good { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hit window for an <see cref="HitResult.Ok"/> result.
|
||||
/// The user can only achieve this result if <see cref="AllowsOk"/> is true.
|
||||
/// </summary>
|
||||
public double Ok { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hit window for a <see cref="HitResult.Meh"/> result.
|
||||
/// </summary>
|
||||
public double Meh { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hit window for a <see cref="HitResult.Miss"/> result.
|
||||
/// </summary>
|
||||
public double Miss { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether it's possible to achieve a <see cref="HitResult.Perfect"/> result.
|
||||
/// </summary>
|
||||
public bool AllowsPerfect;
|
||||
|
||||
/// <summary>
|
||||
/// Whether it's possible to achieve a <see cref="HitResult.Ok"/> result.
|
||||
/// </summary>
|
||||
public bool AllowsOk;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs hit windows by fitting a parameter to a 2-part piecewise linear function for each hit window.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The parameter.</param>
|
||||
public HitWindows(double difficulty)
|
||||
{
|
||||
Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]);
|
||||
Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]);
|
||||
Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]);
|
||||
Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]);
|
||||
Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]);
|
||||
Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="HitResult"/> for a time offset.
|
||||
/// </summary>
|
||||
/// <param name="timeOffset">The time offset.</param>
|
||||
/// <returns>The hit result, or <see cref="HitResult.None"/> if <paramref name="timeOffset"/> doesn't result in a judgement.</returns>
|
||||
public HitResult ResultFor(double timeOffset)
|
||||
{
|
||||
timeOffset = Math.Abs(timeOffset);
|
||||
|
||||
if (AllowsPerfect && timeOffset <= HalfWindowFor(HitResult.Perfect))
|
||||
return HitResult.Perfect;
|
||||
if (timeOffset <= HalfWindowFor(HitResult.Great))
|
||||
return HitResult.Great;
|
||||
if (timeOffset <= HalfWindowFor(HitResult.Good))
|
||||
return HitResult.Good;
|
||||
if (AllowsOk && timeOffset <= HalfWindowFor(HitResult.Ok))
|
||||
return HitResult.Ok;
|
||||
if (timeOffset <= HalfWindowFor(HitResult.Meh))
|
||||
return HitResult.Meh;
|
||||
if (timeOffset <= HalfWindowFor(HitResult.Miss))
|
||||
return HitResult.Miss;
|
||||
|
||||
return HitResult.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves half the hit window for a <see cref="HitResult"/>.
|
||||
/// This is useful if the hit window for one half of the hittable range of a <see cref="HitObject"/> is required.
|
||||
/// </summary>
|
||||
/// <param name="result">The expected <see cref="HitResult"/>.</param>
|
||||
/// <returns>One half of the hit window for <paramref name="result"/>.</returns>
|
||||
public double HalfWindowFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return Perfect / 2;
|
||||
case HitResult.Great:
|
||||
return Great / 2;
|
||||
case HitResult.Good:
|
||||
return Good / 2;
|
||||
case HitResult.Ok:
|
||||
return Ok / 2;
|
||||
case HitResult.Meh:
|
||||
return Meh / 2;
|
||||
case HitResult.Miss:
|
||||
return Miss / 2;
|
||||
default:
|
||||
throw new ArgumentException(nameof(result));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a time offset, whether the <see cref="HitObject"/> can ever be hit in the future with a non-<see cref="HitResult.Miss"/> result.
|
||||
/// This happens if <paramref name="timeOffset"/> is less than what is required for a <see cref="Meh"/> result.
|
||||
/// </summary>
|
||||
/// <param name="timeOffset">The time offset.</param>
|
||||
/// <returns>Whether the <see cref="HitObject"/> can be hit at any point in the future from this time offset.</returns>
|
||||
public bool CanBeHit(double timeOffset) => timeOffset <= HalfWindowFor(HitResult.Meh);
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies all hit windows by a value.
|
||||
/// </summary>
|
||||
/// <param name="windows">The hit windows to multiply.</param>
|
||||
/// <param name="value">The value to multiply each hit window by.</param>
|
||||
public static HitWindows operator *(HitWindows windows, double value)
|
||||
{
|
||||
windows.Perfect *= value;
|
||||
windows.Great *= value;
|
||||
windows.Good *= value;
|
||||
windows.Ok *= value;
|
||||
windows.Meh *= value;
|
||||
windows.Miss *= value;
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides all hit windows by a value.
|
||||
/// </summary>
|
||||
/// <param name="windows">The hit windows to divide.</param>
|
||||
/// <param name="value">The value to divide each hit window by.</param>
|
||||
public static HitWindows operator /(HitWindows windows, double value)
|
||||
{
|
||||
windows.Perfect /= value;
|
||||
windows.Great /= value;
|
||||
windows.Good /= value;
|
||||
windows.Ok /= value;
|
||||
windows.Meh /= value;
|
||||
windows.Miss /= value;
|
||||
|
||||
return windows;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using System.Globalization;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Audio;
|
||||
using System.Linq;
|
||||
using osu.Framework.MathUtils;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
@ -41,9 +42,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
}
|
||||
else if ((type & ConvertHitObjectType.Slider) > 0)
|
||||
{
|
||||
var pos = new Vector2(int.Parse(split[0]), int.Parse(split[1]));
|
||||
|
||||
CurveType curveType = CurveType.Catmull;
|
||||
double length = 0;
|
||||
var points = new List<Vector2> { new Vector2(int.Parse(split[0]), int.Parse(split[1])) };
|
||||
var points = new List<Vector2> { Vector2.Zero };
|
||||
|
||||
string[] pointsplit = split[5].Split('|');
|
||||
foreach (string t in pointsplit)
|
||||
@ -69,9 +72,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
}
|
||||
|
||||
string[] temp = t.Split(':');
|
||||
points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)));
|
||||
points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos);
|
||||
}
|
||||
|
||||
// osu-stable special-cased colinear perfect curves to a CurveType.Linear
|
||||
bool isLinear(List<Vector2> p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
|
||||
if (points.Count == 3 && curveType == CurveType.PerfectCurve && isLinear(points))
|
||||
curveType = CurveType.Linear;
|
||||
|
||||
int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture);
|
||||
|
||||
if (repeatCount > 9000)
|
||||
@ -134,7 +142,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
for (int i = 0; i < nodes; i++)
|
||||
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
|
||||
|
||||
result = CreateSlider(new Vector2(int.Parse(split[0]), int.Parse(split[1])), combo, points, length, curveType, repeatCount, nodeSamples);
|
||||
result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples);
|
||||
}
|
||||
else if ((type & ConvertHitObjectType.Spinner) > 0)
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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 OpenTK;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Collections.Generic;
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
Position = position,
|
||||
NewCombo = newCombo,
|
||||
ControlPoints = controlPoints,
|
||||
Distance = length,
|
||||
Distance = Math.Max(0, length),
|
||||
CurveType = curveType,
|
||||
RepeatSamples = repeatSamples,
|
||||
RepeatCount = repeatCount
|
||||
|
@ -30,19 +30,19 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
public static class HasCurveExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the position on the curve at a given progress, accounting for repeat logic.
|
||||
/// <para>
|
||||
/// Ranges from [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.
|
||||
/// </para>
|
||||
/// Computes the position on the curve relative to how much of the <see cref="HitObject"/> has been completed.
|
||||
/// </summary>
|
||||
/// <param name="progress">[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</param>
|
||||
public static Vector2 PositionAt(this IHasCurve obj, double progress)
|
||||
/// <param name="obj">The curve.</param>
|
||||
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
|
||||
/// <returns>The position on the curve.</returns>
|
||||
public static Vector2 CurvePositionAt(this IHasCurve obj, double progress)
|
||||
=> obj.Curve.PositionAt(obj.ProgressAt(progress));
|
||||
|
||||
/// <summary>
|
||||
/// Finds the progress along the curve, accounting for repeat logic.
|
||||
/// Computes the progress along the curve relative to how much of the <see cref="HitObject"/> has been completed.
|
||||
/// </summary>
|
||||
/// <param name="progress">[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</param>
|
||||
/// <param name="obj">The curve.</param>
|
||||
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
|
||||
/// <returns>[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</returns>
|
||||
public static double ProgressAt(this IHasCurve obj, double progress)
|
||||
{
|
||||
@ -55,6 +55,7 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// Determines which span of the curve the progress point is on.
|
||||
/// </summary>
|
||||
/// <param name="obj">The curve.</param>
|
||||
/// <param name="progress">[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</param>
|
||||
/// <returns>[0, SpanCount) where 0 is the first run.</returns>
|
||||
public static int SpanAt(this IHasCurve obj, double progress)
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets
|
||||
loadRulesetFromFile(file);
|
||||
}
|
||||
|
||||
public RulesetStore(Func<OsuDbContext> factory)
|
||||
public RulesetStore(IDatabaseContextFactory factory)
|
||||
: base(factory)
|
||||
{
|
||||
AddMissingRulesets();
|
||||
@ -56,47 +56,50 @@ namespace osu.Game.Rulesets
|
||||
|
||||
protected void AddMissingRulesets()
|
||||
{
|
||||
var context = GetContext();
|
||||
|
||||
var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList();
|
||||
|
||||
//add all legacy modes in correct order
|
||||
foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID))
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null)
|
||||
context.RulesetInfo.Add(r.RulesetInfo);
|
||||
}
|
||||
var context = usage.Context;
|
||||
|
||||
context.SaveChanges();
|
||||
var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList();
|
||||
|
||||
//add any other modes
|
||||
foreach (var r in instances.Where(r => r.LegacyID < 0))
|
||||
if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
|
||||
context.RulesetInfo.Add(r.RulesetInfo);
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
//perform a consistency check
|
||||
foreach (var r in context.RulesetInfo)
|
||||
{
|
||||
try
|
||||
//add all legacy modes in correct order
|
||||
foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID))
|
||||
{
|
||||
var instance = r.CreateInstance();
|
||||
|
||||
r.Name = instance.Description;
|
||||
r.ShortName = instance.ShortName;
|
||||
|
||||
r.Available = true;
|
||||
if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null)
|
||||
context.RulesetInfo.Add(r.RulesetInfo);
|
||||
}
|
||||
catch
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
//add any other modes
|
||||
foreach (var r in instances.Where(r => r.LegacyID < 0))
|
||||
if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
|
||||
context.RulesetInfo.Add(r.RulesetInfo);
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
//perform a consistency check
|
||||
foreach (var r in context.RulesetInfo)
|
||||
{
|
||||
r.Available = false;
|
||||
try
|
||||
{
|
||||
var instance = r.CreateInstance();
|
||||
|
||||
r.Name = instance.Description;
|
||||
r.ShortName = instance.ShortName;
|
||||
|
||||
r.Available = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
r.Available = false;
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList();
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList();
|
||||
}
|
||||
|
||||
private static void loadRulesetFromFile(string file)
|
||||
|
@ -15,7 +15,7 @@ using SharpCompress.Compressors.LZMA;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
public class ScoreStore : DatabaseBackedStore
|
||||
public class ScoreStore : DatabaseBackedStore, ICanAcceptFiles
|
||||
{
|
||||
private readonly Storage storage;
|
||||
|
||||
@ -24,10 +24,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
private const string replay_folder = @"replays";
|
||||
|
||||
public event Action<Score> ScoreImported;
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
|
||||
private ScoreIPCChannel ipc;
|
||||
|
||||
public ScoreStore(Storage storage, Func<OsuDbContext> factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory)
|
||||
public ScoreStore(Storage storage, DatabaseContextFactory factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory)
|
||||
{
|
||||
this.storage = storage;
|
||||
this.beatmaps = beatmaps;
|
||||
@ -37,6 +39,18 @@ namespace osu.Game.Rulesets.Scoring
|
||||
ipc = new ScoreIPCChannel(importHost, this);
|
||||
}
|
||||
|
||||
public string[] HandledExtensions => new[] { ".osr" };
|
||||
|
||||
public void Import(params string[] paths)
|
||||
{
|
||||
foreach (var path in paths)
|
||||
{
|
||||
var score = ReadReplayFile(path);
|
||||
if (score != null)
|
||||
ScoreImported?.Invoke(score);
|
||||
}
|
||||
}
|
||||
|
||||
public Score ReadReplayFile(string replayFilename)
|
||||
{
|
||||
Score score;
|
||||
@ -160,5 +174,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
return new Replay { Frames = frames };
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -3,52 +3,37 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using OpenTK;
|
||||
using osu.Framework.Allocation;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public abstract class Playfield : Container
|
||||
public abstract class Playfield : ScalableContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// The HitObjects contained in this Playfield.
|
||||
/// </summary>
|
||||
public HitObjectContainer HitObjects { get; private set; }
|
||||
|
||||
public Container<Drawable> ScaledContent;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container<Drawable> content;
|
||||
|
||||
private List<Playfield> nestedPlayfields;
|
||||
|
||||
/// <summary>
|
||||
/// All the <see cref="Playfield"/>s nested inside this playfield.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Playfield> NestedPlayfields => nestedPlayfields;
|
||||
private List<Playfield> nestedPlayfields;
|
||||
|
||||
/// <summary>
|
||||
/// A container for keeping track of DrawableHitObjects.
|
||||
/// </summary>
|
||||
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width.</param>
|
||||
protected Playfield(float? customWidth = null)
|
||||
/// <param name="customWidth">The width to scale the internal coordinate space to.
|
||||
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
|
||||
/// </param>
|
||||
/// <param name="customHeight">The height to scale the internal coordinate space to.
|
||||
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
|
||||
/// </param>
|
||||
protected Playfield(float? customWidth = null, float? customHeight = null)
|
||||
: base(customWidth, customHeight)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(ScaledContent = new ScaledContainer
|
||||
{
|
||||
CustomWidth = customWidth,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -94,22 +79,5 @@ namespace osu.Game.Rulesets.UI
|
||||
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
|
||||
|
||||
private class ScaledContainer : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// A value (in game pixels that we should scale our content to match).
|
||||
/// </summary>
|
||||
public float? CustomWidth;
|
||||
|
||||
//dividing by the customwidth will effectively scale our content to the required container size.
|
||||
protected override Vector2 DrawScale => CustomWidth.HasValue ? new Vector2(DrawSize.X / CustomWidth.Value) : base.DrawScale;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
RelativeChildSize = new Vector2(DrawScale.X, RelativeChildSize.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,11 +33,6 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
public abstract class RulesetContainer : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to apply adjustments to the child <see cref="Playfield"/> based on our own size.
|
||||
/// </summary>
|
||||
public bool AspectAdjust = true;
|
||||
|
||||
/// <summary>
|
||||
/// The selected variant.
|
||||
/// </summary>
|
||||
@ -324,7 +319,7 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Playfield.Size = AspectAdjust ? GetPlayfieldAspectAdjust() : Vector2.One;
|
||||
Playfield.Size = GetAspectAdjustedSize() * PlayfieldArea;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -335,10 +330,17 @@ namespace osu.Game.Rulesets.UI
|
||||
protected virtual BeatmapProcessor<TObject> CreateBeatmapProcessor() => new BeatmapProcessor<TObject>();
|
||||
|
||||
/// <summary>
|
||||
/// In some cases we want to apply changes to the relative size of our contained <see cref="Playfield"/> based on custom conditions.
|
||||
/// Computes the size of the <see cref="Playfield"/> in relative coordinate space after aspect adjustments.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); //a sane default
|
||||
/// <returns>The aspect-adjusted size.</returns>
|
||||
protected virtual Vector2 GetAspectAdjustedSize() => Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
/// The area of this <see cref="RulesetContainer"/> that is available for the <see cref="Playfield"/> to use.
|
||||
/// Must be specified in relative coordinate space to this <see cref="RulesetContainer"/>.
|
||||
/// This affects the final size of the <see cref="Playfield"/> but does not affect the <see cref="Playfield"/>'s scale.
|
||||
/// </summary>
|
||||
protected virtual Vector2 PlayfieldArea => new Vector2(0.75f); // A sane default
|
||||
|
||||
/// <summary>
|
||||
/// Creates a converter to convert Beatmap to a specific mode.
|
||||
|
@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.UI
|
||||
if (!base.UpdateSubTree())
|
||||
return false;
|
||||
|
||||
UpdateSubTreeMasking(ScreenSpaceDrawQuad.AABBFloat);
|
||||
UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
|
||||
|
||||
if (isAttached)
|
||||
{
|
||||
|
86
osu.Game/Rulesets/UI/ScalableContainer.cs
Normal file
86
osu.Game/Rulesets/UI/ScalableContainer.cs
Normal file
@ -0,0 +1,86 @@
|
||||
// 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;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Container"/> which can have its internal coordinate system scaled to a specific size.
|
||||
/// </summary>
|
||||
public class ScalableContainer : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// The scaled content.
|
||||
/// </summary>
|
||||
public readonly Container ScaledContent;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Container"/> which can have its internal coordinate system scaled to a specific size.
|
||||
/// </summary>
|
||||
/// <param name="customWidth">The width to scale the internal coordinate space to.
|
||||
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
|
||||
/// </param>
|
||||
/// <param name="customHeight">The height to scale the internal coordinate space to.
|
||||
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
|
||||
/// </param>
|
||||
public ScalableContainer(float? customWidth = null, float? customHeight = null)
|
||||
{
|
||||
AddInternal(ScaledContent = new ScaledContainer
|
||||
{
|
||||
CustomWidth = customWidth,
|
||||
CustomHeight = customHeight,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = content = new Container { RelativeSizeAxes = Axes.Both }
|
||||
});
|
||||
}
|
||||
|
||||
private class ScaledContainer : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// The value to scale the width of the content to match.
|
||||
/// If null, <see cref="CustomHeight"/> is used.
|
||||
/// </summary>
|
||||
public float? CustomWidth;
|
||||
|
||||
/// <summary>
|
||||
/// The value to scale the height of the content to match.
|
||||
/// if null, <see cref="CustomWidth"/> is used.
|
||||
/// </summary>
|
||||
public float? CustomHeight;
|
||||
|
||||
/// <summary>
|
||||
/// The scale that is required for the size of the content to match <see cref="CustomWidth"/> and <see cref="CustomHeight"/>.
|
||||
/// </summary>
|
||||
private Vector2 sizeScale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CustomWidth.HasValue && CustomHeight.HasValue)
|
||||
return Vector2.Divide(DrawSize, new Vector2(CustomWidth.Value, CustomHeight.Value));
|
||||
if (CustomWidth.HasValue)
|
||||
return new Vector2(DrawSize.X / CustomWidth.Value);
|
||||
if (CustomHeight.HasValue)
|
||||
return new Vector2(DrawSize.Y / CustomHeight.Value);
|
||||
return Vector2.One;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scale the content to the required container size by multiplying by <see cref="sizeScale"/>.
|
||||
/// </summary>
|
||||
protected override Vector2 DrawScale => sizeScale * base.DrawScale;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
RelativeChildSize = new Vector2(CustomWidth.HasValue ? sizeScale.X : RelativeChildSize.X, CustomHeight.HasValue ? sizeScale.Y : RelativeChildSize.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
protected virtual bool UserScrollSpeedAdjustment => true;
|
||||
|
||||
/// <summary>
|
||||
/// The container that contains the <see cref="SpeedAdjustmentContainer"/>s and <see cref="DrawableHitObject"/>s.
|
||||
/// The container that contains the <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects;
|
||||
|
||||
@ -61,10 +61,15 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ScrollingPlayfield"/>.
|
||||
/// </summary>
|
||||
/// <param name="scrollingAxes">The axes on which <see cref="DrawableHitObject"/>s in this container should scroll.</param>
|
||||
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width</param>
|
||||
protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null)
|
||||
: base(customWidth)
|
||||
/// <param name="direction">The direction in which <see cref="DrawableHitObject"/>s in this container should scroll.</param>
|
||||
/// <param name="customWidth">The width to scale the internal coordinate space to.
|
||||
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
|
||||
/// </param>
|
||||
/// <param name="customHeight">The height to scale the internal coordinate space to.
|
||||
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
|
||||
/// </param>
|
||||
protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null, float? customHeight = null)
|
||||
: base(customWidth, customHeight)
|
||||
{
|
||||
this.direction = direction;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -143,7 +143,7 @@ namespace osu.Game.Screens.Menu
|
||||
Alpha = 0.5f,
|
||||
Size = new Vector2(0.96f)
|
||||
},
|
||||
new BufferedContainer
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Screens.Multiplayer
|
||||
{
|
||||
set
|
||||
{
|
||||
var ranks = value.Select(u => u.GlobalRank);
|
||||
var ranks = value.Select(u => u.Statistics.Ranks.Global);
|
||||
levelRangeLower.Text = ranks.Min().ToString();
|
||||
levelRangeHigher.Text = ranks.Max().ToString();
|
||||
}
|
||||
|
@ -52,6 +52,10 @@ namespace osu.Game.Screens
|
||||
|
||||
protected readonly Bindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
|
||||
|
||||
protected virtual float BackgroundParallaxAmount => 1;
|
||||
|
||||
private ParallaxContainer backgroundParallaxContainer;
|
||||
|
||||
public WorkingBeatmap InitialBeatmap
|
||||
{
|
||||
set
|
||||
@ -102,11 +106,10 @@ namespace osu.Game.Screens
|
||||
|
||||
protected override void OnResuming(Screen last)
|
||||
{
|
||||
base.OnResuming(last);
|
||||
logo.AppendAnimatingAction(() => LogoArriving(logo, true), true);
|
||||
sampleExit?.Play();
|
||||
applyArrivingDefaults(true);
|
||||
|
||||
ShowOverlays.Value = ShowOverlaysOnEnter;
|
||||
base.OnResuming(last);
|
||||
}
|
||||
|
||||
protected override void OnSuspending(Screen next)
|
||||
@ -123,6 +126,8 @@ namespace osu.Game.Screens
|
||||
|
||||
if (lastOsu?.Background != null)
|
||||
{
|
||||
backgroundParallaxContainer = lastOsu.backgroundParallaxContainer;
|
||||
|
||||
if (bg == null || lastOsu.Background.Equals(bg))
|
||||
//we can keep the previous mode's background.
|
||||
Background = lastOsu.Background;
|
||||
@ -136,7 +141,7 @@ namespace osu.Game.Screens
|
||||
// this makes up for the fact our padding changes when the global toolbar is visible.
|
||||
bg.Scale = new Vector2(1.06f);
|
||||
|
||||
AddInternal(new ParallaxContainer
|
||||
AddInternal(backgroundParallaxContainer = new ParallaxContainer
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
Children = new[]
|
||||
@ -149,11 +154,9 @@ namespace osu.Game.Screens
|
||||
if ((logo = lastOsu?.logo) == null)
|
||||
LoadComponentAsync(logo = new OsuLogo { Alpha = 0 }, AddInternal);
|
||||
|
||||
logo.AppendAnimatingAction(() => LogoArriving(logo, false), true);
|
||||
applyArrivingDefaults(false);
|
||||
|
||||
base.OnEntering(last);
|
||||
|
||||
ShowOverlays.Value = ShowOverlaysOnEnter;
|
||||
}
|
||||
|
||||
protected override bool OnExiting(Screen next)
|
||||
@ -193,6 +196,16 @@ namespace osu.Game.Screens
|
||||
logo.Ripple = true;
|
||||
}
|
||||
|
||||
private void applyArrivingDefaults(bool isResuming)
|
||||
{
|
||||
logo.AppendAnimatingAction(() => LogoArriving(logo, isResuming), true);
|
||||
|
||||
if (backgroundParallaxContainer != null)
|
||||
backgroundParallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * BackgroundParallaxAmount;
|
||||
|
||||
ShowOverlays.Value = ShowOverlaysOnEnter;
|
||||
}
|
||||
|
||||
private void onExitingLogo()
|
||||
{
|
||||
logo.AppendAnimatingAction(() => { LogoExiting(logo); }, false);
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
//CollectionSettings = new CollectionSettings(),
|
||||
//DiscussionSettings = new DiscussionSettings(),
|
||||
PlaybackSettings = new PlaybackSettings(),
|
||||
VisualSettings = new VisualSettings()
|
||||
VisualSettings = new VisualSettings { Expanded = false }
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -36,6 +36,7 @@ namespace osu.Game.Screens.Play
|
||||
public int Retries { set { pauseOverlay.Retries = value; } }
|
||||
|
||||
public bool CanPause => (CheckCanPause?.Invoke() ?? true) && Time.Current >= lastPauseActionTime + pause_cooldown;
|
||||
public bool IsResuming { get; private set; }
|
||||
|
||||
public Action OnRetry;
|
||||
public Action OnQuit;
|
||||
@ -54,7 +55,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
AddInternal(pauseOverlay = new PauseOverlay
|
||||
{
|
||||
OnResume = () => this.Delay(400).Schedule(Resume),
|
||||
OnResume = () =>
|
||||
{
|
||||
IsResuming = true;
|
||||
this.Delay(400).Schedule(Resume);
|
||||
},
|
||||
OnRetry = () => OnRetry(),
|
||||
OnQuit = () => OnQuit(),
|
||||
});
|
||||
@ -100,6 +105,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
pauseOverlay.Hide();
|
||||
AudioClock.Start();
|
||||
IsResuming = false;
|
||||
}
|
||||
|
||||
private OsuGameBase game;
|
||||
|
@ -37,6 +37,8 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap);
|
||||
|
||||
protected override float BackgroundParallaxAmount => 0.1f;
|
||||
|
||||
public override bool ShowOverlaysOnEnter => false;
|
||||
|
||||
public Action RestartRequested;
|
||||
@ -351,7 +353,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected override bool OnExiting(Screen next)
|
||||
{
|
||||
if (!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false)
|
||||
if ((!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) && (!pauseContainer?.IsResuming ?? true))
|
||||
{
|
||||
// In the case of replays, we may have changed the playback rate.
|
||||
applyRateFromMods();
|
||||
@ -362,7 +364,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
if (loadedSuccessfully)
|
||||
{
|
||||
pauseContainer.Pause();
|
||||
pauseContainer?.Pause();
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -387,7 +389,7 @@ namespace osu.Game.Screens.Play
|
||||
.FadeTo(storyboardVisible && opacity > 0 ? 1 : 0, duration, Easing.OutQuint);
|
||||
|
||||
(Background as BackgroundScreenBeatmap)?.BlurTo(new Vector2((float)blurLevel.Value * 25), duration, Easing.OutQuint);
|
||||
Background?.FadeTo(!storyboardVisible || beatmap.Background == null ? opacity : 0, duration, Easing.OutQuint);
|
||||
Background?.FadeTo(beatmap.Background != null && (!storyboardVisible || !beatmap.Storyboard.ReplacesBackground) ? opacity : 0, duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void fadeOut()
|
||||
|
@ -42,7 +42,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Text = "1x",
|
||||
Font = @"Exo2.0-Bold",
|
||||
}
|
||||
},
|
||||
@ -54,12 +53,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
Default = 1,
|
||||
MinValue = 0.5,
|
||||
MaxValue = 2,
|
||||
Precision = 0.01,
|
||||
Precision = 0.1,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
sliderbar.Bindable.ValueChanged += rateMultiplier => multiplierText.Text = $"{rateMultiplier}x";
|
||||
sliderbar.Bindable.ValueChanged += rateMultiplier => multiplierText.Text = $"{sliderbar.Bar.TooltipText}x";
|
||||
sliderbar.Bindable.TriggerChange();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -31,6 +31,28 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
private bool expanded = true;
|
||||
|
||||
public bool Expanded
|
||||
{
|
||||
get { return expanded; }
|
||||
set
|
||||
{
|
||||
if (expanded == value) return;
|
||||
expanded = value;
|
||||
|
||||
content.ClearTransforms();
|
||||
|
||||
if (expanded)
|
||||
content.AutoSizeAxes = Axes.Y;
|
||||
else
|
||||
{
|
||||
content.AutoSizeAxes = Axes.None;
|
||||
content.ResizeHeightTo(0, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private Color4 buttonActiveColour;
|
||||
|
||||
protected PlayerSettingsGroup()
|
||||
@ -82,7 +104,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
Position = new Vector2(-15, 0),
|
||||
Icon = FontAwesome.fa_bars,
|
||||
Scale = new Vector2(0.75f),
|
||||
Action = toggleContentVisibility,
|
||||
Action = () => Expanded = !Expanded,
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -111,22 +133,5 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
}
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private void toggleContentVisibility()
|
||||
{
|
||||
content.ClearTransforms();
|
||||
|
||||
expanded = !expanded;
|
||||
|
||||
if (expanded)
|
||||
content.AutoSizeAxes = Axes.Y;
|
||||
else
|
||||
{
|
||||
content.AutoSizeAxes = Axes.None;
|
||||
content.ResizeHeightTo(0, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
public class PlayerSliderBar<T> : SettingsSlider<T>
|
||||
where T : struct, IEquatable<T>, IComparable, IConvertible
|
||||
{
|
||||
public OsuSliderBar<T> Bar => (OsuSliderBar<T>)Control;
|
||||
|
||||
protected override Drawable CreateControl() => new Sliderbar
|
||||
{
|
||||
Margin = new MarginPadding { Top = 5, Bottom = 5 },
|
||||
@ -21,8 +23,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
private class Sliderbar : OsuSliderBar<T>
|
||||
{
|
||||
public override string TooltipText => $"{CurrentNumber.Value}";
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
|
@ -32,6 +32,8 @@ namespace osu.Game.Screens.Play
|
||||
private FadeContainer fadeContainer;
|
||||
private double displayTime;
|
||||
|
||||
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
public SkipButton(double startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
@ -153,16 +155,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public Visibility State
|
||||
{
|
||||
get
|
||||
{
|
||||
return state;
|
||||
}
|
||||
get { return state; }
|
||||
set
|
||||
{
|
||||
if (state == value)
|
||||
return;
|
||||
bool stateChanged = value != state;
|
||||
|
||||
var lastState = state;
|
||||
state = value;
|
||||
|
||||
scheduledHide?.Cancel();
|
||||
@ -170,7 +167,8 @@ namespace osu.Game.Screens.Play
|
||||
switch (state)
|
||||
{
|
||||
case Visibility.Visible:
|
||||
if (lastState == Visibility.Hidden)
|
||||
// we may be triggered to become visible mnultiple times but we only want to transform once.
|
||||
if (stateChanged)
|
||||
this.FadeIn(500, Easing.OutExpo);
|
||||
|
||||
if (!IsHovered)
|
||||
|
@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
var xFill = value * UsableWidth;
|
||||
fill.Width = xFill;
|
||||
handleBase.MoveToX(xFill);
|
||||
handleBase.X = xFill;
|
||||
}
|
||||
|
||||
protected override void OnUserChange() => OnSeek?.Invoke(Current);
|
||||
|
@ -192,7 +192,9 @@ namespace osu.Game.Screens.Select
|
||||
/// <param name="skipDifficulties">Whether to skip individual difficulties and only increment over full groups.</param>
|
||||
public void SelectNext(int direction = 1, bool skipDifficulties = true)
|
||||
{
|
||||
if (!Items.Any())
|
||||
var visibleItems = Items.Where(s => !s.Item.Filtered).ToList();
|
||||
|
||||
if (!visibleItems.Any())
|
||||
return;
|
||||
|
||||
DrawableCarouselItem drawable = null;
|
||||
@ -202,15 +204,15 @@ namespace osu.Game.Screens.Select
|
||||
// we can fix this by changing this method to not reference drawables / Items in the first place.
|
||||
return;
|
||||
|
||||
int originalIndex = Items.IndexOf(drawable);
|
||||
int originalIndex = visibleItems.IndexOf(drawable);
|
||||
int currentIndex = originalIndex;
|
||||
|
||||
// local function to increment the index in the required direction, wrapping over extremities.
|
||||
int incrementIndex() => currentIndex = (currentIndex + direction + Items.Count) % Items.Count;
|
||||
int incrementIndex() => currentIndex = (currentIndex + direction + visibleItems.Count) % visibleItems.Count;
|
||||
|
||||
while (incrementIndex() != originalIndex)
|
||||
{
|
||||
var item = Items[currentIndex].Item;
|
||||
var item = visibleItems[currentIndex].Item;
|
||||
|
||||
if (item.Filtered || item.State == CarouselItemState.Selected) continue;
|
||||
|
||||
@ -407,12 +409,14 @@ namespace osu.Game.Screens.Select
|
||||
continue;
|
||||
}
|
||||
|
||||
float depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0);
|
||||
|
||||
// Only add if we're not already part of the content.
|
||||
if (!scrollableContent.Contains(item))
|
||||
{
|
||||
// Makes sure headers are always _below_ items,
|
||||
// and depth flows downward.
|
||||
item.Depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0);
|
||||
item.Depth = depth;
|
||||
|
||||
switch (item.LoadState)
|
||||
{
|
||||
@ -426,6 +430,10 @@ namespace osu.Game.Screens.Select
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
scrollableContent.ChangeChildDepth(item, depth);
|
||||
}
|
||||
}
|
||||
|
||||
// this is not actually useful right now, but once we have groups may well be.
|
||||
|
@ -25,10 +25,10 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu
|
||||
{
|
||||
private Action<BeatmapSetInfo> deleteRequested;
|
||||
private Action<BeatmapSetInfo> restoreHiddenRequested;
|
||||
private Action<int> viewDetails;
|
||||
|
||||
private DialogOverlay dialogOverlay;
|
||||
private readonly BeatmapSetInfo beatmapSet;
|
||||
|
||||
public DrawableCarouselBeatmapSet(CarouselBeatmapSet set)
|
||||
@ -38,13 +38,13 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay)
|
||||
private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay, DialogOverlay overlay)
|
||||
{
|
||||
if (localisation == null)
|
||||
throw new ArgumentNullException(nameof(localisation));
|
||||
|
||||
restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore);
|
||||
deleteRequested = manager.Delete;
|
||||
dialogOverlay = overlay;
|
||||
if (beatmapOverlay != null)
|
||||
viewDetails = beatmapOverlay.ShowBeatmapSet;
|
||||
|
||||
@ -104,7 +104,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
|
||||
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested?.Invoke(beatmapSet)));
|
||||
|
||||
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => deleteRequested?.Invoke(beatmapSet)));
|
||||
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new BeatmapDeleteDialog(beatmapSet))));
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user