Merge branch 'master' of git://github.com/ppy/osu into volume-control

This commit is contained in:
jorolf
2018-03-03 14:10:50 +01:00
273 changed files with 6672 additions and 2452 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View 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;
}
}
}
}

View File

@ -3,11 +3,12 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using osu.Game.Database;
using osu.Game.IO;
namespace osu.Game.Beatmaps
{
public class BeatmapSetFileInfo
public class BeatmapSetFileInfo : INamedFileInfo
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }

View File

@ -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; }

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
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);
}
}

View File

@ -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;

View File

@ -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
}
}

View File

@ -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();
}

View 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);
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}
}

View 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);
}
}
}

View 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; }
}
}

View 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();
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using 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; }
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.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; }
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
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; }
}
}

View 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);
}
}
}
}

View File

@ -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

View File

@ -0,0 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
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);
}
}

View File

@ -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);
}

View File

@ -18,6 +18,8 @@ namespace osu.Game.Graphics.UserInterface
public Action Exit;
public override bool HandleLeftRightArrows => false;
private bool focus;
public bool HoldFocus
{

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -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;
}

View File

@ -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);

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -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();
}

View 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
}
}
}

View 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");
}
}
}

View File

@ -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
}
}

View File

@ -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
{

View 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;
}
}

View File

@ -27,32 +27,6 @@ namespace osu.Game.Online.API
public new event APISuccessHandler<T> Success;
}
public abstract class APIDownloadRequest : APIRequest
{
protected override WebRequest CreateWebRequest()
{
var request = new WebRequest(Uri);
request.DownloadProgress += request_Progress;
return request;
}
private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); });
protected APIDownloadRequest()
{
base.Success += onSuccess;
}
private void onSuccess()
{
Success?.Invoke(WebRequest.ResponseData);
}
public event APIProgressHandler Progress;
public new event APISuccessHandler<byte[]> Success;
}
/// <summary>
/// AN API request with no specified response type.
/// </summary>

View 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)
{
}
}
}

View 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);
}
}

View File

@ -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();

View File

@ -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();
}
}

View File

@ -223,13 +223,13 @@ namespace osu.Game.Overlays.BeatmapSet
tabsBg.Colour = colours.Gray3;
this.beatmaps = beatmaps;
beatmaps.BeatmapSetAdded += handleBeatmapAdd;
beatmaps.ItemAdded += handleBeatmapAdd;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (beatmaps != null) beatmaps.BeatmapSetAdded -= handleBeatmapAdd;
if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd;
}
private void handleBeatmapAdd(BeatmapSetInfo beatmap)

View File

@ -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;
}

View File

@ -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)

View File

@ -185,7 +185,7 @@ namespace osu.Game.Overlays
resultCountsContainer.Colour = colours.Yellow;
beatmaps.BeatmapSetAdded += setAdded;
beatmaps.ItemAdded += setAdded;
}
private void setAdded(BeatmapSetInfo set)

View File

@ -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)

View File

@ -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

View File

@ -74,8 +74,8 @@ namespace osu.Game.Overlays.Music
},
};
beatmaps.BeatmapSetAdded += list.AddBeatmapSet;
beatmaps.BeatmapSetRemoved += list.RemoveBeatmapSet;
beatmaps.ItemAdded += list.AddBeatmapSet;
beatmaps.ItemRemoved += list.RemoveBeatmapSet;
list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets();

View File

@ -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;

View File

@ -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)

View File

@ -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
}
};

View File

@ -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
};

View File

@ -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));
}
},
};

View File

@ -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>

View File

@ -181,7 +181,7 @@ namespace osu.Game.Overlays.Settings
{
hovering = true;
UpdateState();
return true;
return false;
}
protected override void OnHoverLost(InputState state)

View File

@ -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();
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using 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 }
};
}
}
}

View 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();
}
}

View File

@ -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);
}
}
}

View File

@ -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();
});
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View 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);
}
}

View File

@ -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;
}
}

View File

@ -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()));
}
}
}

View File

@ -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;

View File

@ -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()

View 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;
}
}
}

View File

@ -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)
{

View File

@ -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

View File

@ -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)

View File

@ -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)

View 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 };
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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.

View File

@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.UI
if (!base.UpdateSubTree())
return false;
UpdateSubTreeMasking(ScreenSpaceDrawQuad.AABBFloat);
UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
if (isAttached)
{

View 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);
}
}
}
}

View File

@ -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;
}

View File

@ -10,8 +10,8 @@ using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.IO;
using osu.Game.Configuration;
using osu.Game.IO.Archives;
using osu.Game.Screens.Backgrounds;
using OpenTK;
using OpenTK.Graphics;
@ -62,8 +62,10 @@ namespace osu.Game.Screens.Menu
if (setInfo == null)
{
// we need to import the default menu background beatmap
setInfo = beatmaps.Import(new OszArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz")));
setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz"));
setInfo.Protected = true;
beatmaps.Update(setInfo);
}
}
@ -73,9 +75,6 @@ namespace osu.Game.Screens.Menu
welcome = audio.Sample.Get(@"welcome");
seeya = audio.Sample.Get(@"seeya");
if (setInfo.Protected)
beatmaps.Delete(setInfo);
}
protected override void OnEntering(Screen last)

View File

@ -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[]

View File

@ -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();
}

View File

@ -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);

View File

@ -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 }
}
};

View File

@ -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;

View File

@ -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()

View File

@ -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()

View File

@ -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);
}
}
}

View File

@ -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)
{

View File

@ -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)

View File

@ -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);

View File

@ -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.

View File

@ -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