Merge branch 'master' into publicly-expose-hud

This commit is contained in:
Dean Herbert
2020-05-10 20:06:28 +09:00
committed by GitHub
222 changed files with 3175 additions and 1282 deletions

View File

@ -149,6 +149,11 @@ namespace osu.Game.Beatmaps
}
}
public string[] SearchableTerms => new[]
{
Version
}.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty<string>()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
public override string ToString()
{
string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";

View File

@ -17,7 +17,6 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO;
@ -61,7 +60,7 @@ namespace osu.Game.Beatmaps
private readonly BeatmapStore beatmaps;
private readonly AudioManager audioManager;
private readonly GameHost host;
private readonly BeatmapUpdateQueue updateQueue;
private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
private readonly Storage exportStorage;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
@ -78,7 +77,7 @@ namespace osu.Game.Beatmaps
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
updateQueue = new BeatmapUpdateQueue(api);
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
exportStorage = storage.GetStorageForDirectory("exports");
}
@ -105,7 +104,7 @@ namespace osu.Game.Beatmaps
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
await updateQueue.UpdateAsync(beatmapSet, cancellationToken);
await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken);
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
@ -141,7 +140,7 @@ namespace osu.Game.Beatmaps
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
LogForModel(beatmapSet, "Validating online IDs...");
LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps...");
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
@ -246,6 +245,12 @@ namespace osu.Game.Beatmaps
if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
return DefaultBeatmap;
if (beatmapInfo.BeatmapSet.Files == null)
{
var info = beatmapInfo;
beatmapInfo = QueryBeatmap(b => b.ID == info.ID);
}
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
@ -287,13 +292,37 @@ namespace osu.Game.Beatmaps
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List<BeatmapSetInfo> GetAllUsableBeatmapSets() => GetAllUsableBeatmapSetsEnumerable().ToList();
public List<BeatmapSetInfo> GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All) => GetAllUsableBeatmapSetsEnumerable(includes).ToList();
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. Note that files are not populated.
/// </summary>
/// <param name="includes">The level of detail to include in the returned objects.</param>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public IQueryable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected);
public IEnumerable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes)
{
IQueryable<BeatmapSetInfo> queryable;
switch (includes)
{
case IncludedDetails.Minimal:
queryable = beatmaps.BeatmapSetsOverview;
break;
case IncludedDetails.AllButFiles:
queryable = beatmaps.BeatmapSetsWithoutFiles;
break;
default:
queryable = beatmaps.ConsumableItems;
break;
}
// AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY
// clause which causes queries to take 5-10x longer.
// TODO: remove if upgrading to EF core 3.x.
return queryable.AsEnumerable().Where(s => !s.DeletePending && !s.Protected);
}
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
@ -352,7 +381,7 @@ namespace osu.Game.Beatmaps
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu")))
{
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
using (var ms = new MemoryStream()) //we need a memory stream so we can seek
using (var ms = new MemoryStream()) // we need a memory stream so we can seek
using (var sr = new LineBufferedReader(ms))
{
raw.CopyTo(ms);
@ -416,70 +445,26 @@ namespace osu.Game.Beatmaps
protected override Texture GetBackground() => null;
protected override Track GetTrack() => null;
}
}
private class BeatmapUpdateQueue
{
private readonly IAPIProvider api;
/// <summary>
/// The level of detail to include in database results.
/// </summary>
public enum IncludedDetails
{
/// <summary>
/// Only include beatmap difficulties and set level metadata.
/// </summary>
Minimal,
private const int update_queue_request_concurrency = 4;
/// <summary>
/// Include all difficulties, rulesets, difficulty metadata but no files.
/// </summary>
AllButFiles,
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapUpdateQueue));
public BeatmapUpdateQueue(IAPIProvider api)
{
this.api = api;
}
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
if (api?.State != APIState.Online)
return Task.CompletedTask;
LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}
// todo: expose this when we need to do individual difficulty lookups.
protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken)
=> Task.Factory.StartNew(() => update(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler);
private void update(BeatmapSetInfo set, BeatmapInfo beatmap)
{
if (api?.State != APIState.Online)
return;
var req = new GetBeatmapRequest(beatmap);
req.Failure += fail;
try
{
// intentionally blocking to limit web request concurrency
api.Perform(req);
var res = req.Result;
if (res != null)
{
beatmap.Status = res.Status;
beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
}
}
catch (Exception e)
{
fail(e);
}
void fail(Exception e)
{
beatmap.OnlineBeatmapID = null;
LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
}
}
}
/// <summary>
/// Include everything.
/// </summary>
All
}
}

View File

@ -0,0 +1,195 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using Microsoft.Data.Sqlite;
using osu.Framework.Development;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using SharpCompress.Compressors;
using SharpCompress.Compressors.BZip2;
namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
private class BeatmapOnlineLookupQueue
{
private readonly IAPIProvider api;
private readonly Storage storage;
private const int update_queue_request_concurrency = 4;
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue));
private FileWebRequest cacheDownloadRequest;
private const string cache_database_name = "online.db";
public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage)
{
this.api = api;
this.storage = storage;
// avoid downloading / using cache for unit tests.
if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name))
prepareLocalCache();
}
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
if (api?.State != APIState.Online)
return Task.CompletedTask;
LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}
// todo: expose this when we need to do individual difficulty lookups.
protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken)
=> Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler);
private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap)
{
if (checkLocalCache(set, beatmap))
return;
if (api?.State != APIState.Online)
return;
var req = new GetBeatmapRequest(beatmap);
req.Failure += fail;
try
{
// intentionally blocking to limit web request concurrency
api.Perform(req);
var res = req.Result;
if (res != null)
{
beatmap.Status = res.Status;
beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
}
}
catch (Exception e)
{
fail(e);
}
void fail(Exception e)
{
beatmap.OnlineBeatmapID = null;
LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
}
}
private void prepareLocalCache()
{
string cacheFilePath = storage.GetFullPath(cache_database_name);
string compressedCacheFilePath = $"{cacheFilePath}.bz2";
cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2");
cacheDownloadRequest.Failed += ex =>
{
File.Delete(compressedCacheFilePath);
File.Delete(cacheFilePath);
Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database);
};
cacheDownloadRequest.Finished += () =>
{
try
{
using (var stream = File.OpenRead(cacheDownloadRequest.Filename))
using (var outStream = File.OpenWrite(cacheFilePath))
using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
bz2.CopyTo(outStream);
// set to null on completion to allow lookups to begin using the new source
cacheDownloadRequest = null;
}
catch (Exception ex)
{
Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database);
File.Delete(cacheFilePath);
}
finally
{
File.Delete(compressedCacheFilePath);
}
};
cacheDownloadRequest.PerformAsync();
}
private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap)
{
// download is in progress (or was, and failed).
if (cacheDownloadRequest != null)
return false;
// database is unavailable.
if (!storage.Exists(cache_database_name))
return false;
try
{
using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online")))
{
var found = db.QuerySingleOrDefault<CachedOnlineBeatmapLookup>(
"SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap);
if (found != null)
{
var status = (BeatmapSetOnlineStatus)found.approved;
beatmap.Status = status;
beatmap.BeatmapSet.Status = status;
beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id;
beatmap.OnlineBeatmapID = found.beatmap_id;
LogForModel(set, $"Cached local retrieval for {beatmap}.");
return true;
}
}
}
catch (Exception ex)
{
LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}.");
}
return false;
}
[Serializable]
[SuppressMessage("ReSharper", "InconsistentNaming")]
private class CachedOnlineBeatmapLookup
{
public int approved { get; set; }
public int? beatmapset_id { get; set; }
public int? beatmap_id { get; set; }
}
}
}
}

View File

@ -87,6 +87,18 @@ namespace osu.Game.Beatmaps
base.Purge(items, context);
}
public IQueryable<BeatmapSetInfo> BeatmapSetsOverview => ContextFactory.Get().BeatmapSetInfo
.Include(s => s.Metadata)
.Include(s => s.Beatmaps)
.AsNoTracking();
public IQueryable<BeatmapSetInfo> BeatmapSetsWithoutFiles => ContextFactory.Get().BeatmapSetInfo
.Include(s => s.Metadata)
.Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
.AsNoTracking();
public IQueryable<BeatmapInfo> Beatmaps =>
ContextFactory.Get().BeatmapInfo
.Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Beatmaps.Formats
{
@ -124,7 +125,12 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}"));
writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}"));
writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}"));
writer.WriteLine(FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
// Taiko adjusts the slider multiplier (see: TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER)
writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1
? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / 1.4f}")
: FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}"));
}
@ -197,51 +203,63 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[HitObjects]");
// TODO: implement other legacy rulesets
foreach (var h in beatmap.HitObjects)
handleHitObject(writer, h);
}
private void handleHitObject(TextWriter writer, HitObject hitObject)
{
Vector2 position = new Vector2(256, 192);
switch (beatmap.BeatmapInfo.RulesetID)
{
case 0:
foreach (var h in beatmap.HitObjects)
handleOsuHitObject(writer, h);
position = ((IHasPosition)hitObject).Position;
break;
case 2:
position.X = ((IHasXPosition)hitObject).X * 512;
break;
case 3:
int totalColumns = (int)Math.Max(1, beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
position.X = (int)Math.Ceiling(((IHasXPosition)hitObject).X * (512f / totalColumns));
break;
}
}
private void handleOsuHitObject(TextWriter writer, HitObject hitObject)
{
var positionData = (IHasPosition)hitObject;
writer.Write(FormattableString.Invariant($"{positionData.X},"));
writer.Write(FormattableString.Invariant($"{positionData.Y},"));
writer.Write(FormattableString.Invariant($"{position.X},"));
writer.Write(FormattableString.Invariant($"{position.Y},"));
writer.Write(FormattableString.Invariant($"{hitObject.StartTime},"));
writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},"));
writer.Write(hitObject is IHasCurve
? FormattableString.Invariant($"0,")
: FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},"));
writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},"));
if (hitObject is IHasCurve curveData)
{
addCurveData(writer, curveData, positionData);
addCurveData(writer, curveData, position);
writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true));
}
else
{
if (hitObject is IHasEndTime endTimeData)
writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},"));
if (hitObject is IHasEndTime)
addEndTimeData(writer, hitObject);
writer.Write(getSampleBank(hitObject.Samples));
}
writer.WriteLine();
}
private static LegacyHitObjectType getObjectType(HitObject hitObject)
private LegacyHitObjectType getObjectType(HitObject hitObject)
{
var comboData = (IHasCombo)hitObject;
LegacyHitObjectType type = 0;
var type = (LegacyHitObjectType)(comboData.ComboOffset << 4);
if (hitObject is IHasCombo combo)
{
type = (LegacyHitObjectType)(combo.ComboOffset << 4);
if (comboData.NewCombo) type |= LegacyHitObjectType.NewCombo;
if (combo.NewCombo)
type |= LegacyHitObjectType.NewCombo;
}
switch (hitObject)
{
@ -250,7 +268,10 @@ namespace osu.Game.Beatmaps.Formats
break;
case IHasEndTime _:
type |= LegacyHitObjectType.Spinner;
if (beatmap.BeatmapInfo.RulesetID == 3)
type |= LegacyHitObjectType.Hold;
else
type |= LegacyHitObjectType.Spinner;
break;
default:
@ -261,7 +282,7 @@ namespace osu.Game.Beatmaps.Formats
return type;
}
private void addCurveData(TextWriter writer, IHasCurve curveData, IHasPosition positionData)
private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position)
{
PathType? lastType = null;
@ -297,13 +318,13 @@ namespace osu.Game.Beatmaps.Formats
else
{
// New segment with the same type - duplicate the control point
writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}|"));
writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}|"));
}
}
if (i != 0)
{
writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}"));
writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}"));
writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ",");
}
}
@ -324,6 +345,20 @@ namespace osu.Game.Beatmaps.Formats
}
}
private void addEndTimeData(TextWriter writer, HitObject hitObject)
{
var endTimeData = (IHasEndTime)hitObject;
var type = getObjectType(hitObject);
char suffix = ',';
// Holds write the end time as if it's part of sample data.
if (type == LegacyHitObjectType.Hold)
suffix = ':';
writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}"));
}
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false, bool zeroBanks = false)
{
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);

View File

@ -197,7 +197,7 @@ namespace osu.Game.Beatmaps
public override string ToString() => BeatmapInfo.ToString();
public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
public IBeatmap Beatmap
{
@ -233,7 +233,7 @@ namespace osu.Game.Beatmaps
protected abstract Texture GetBackground();
private readonly RecyclableLazy<Texture> background;
public bool TrackLoaded => track.IsResultAvailable;
public virtual bool TrackLoaded => track.IsResultAvailable;
public Track Track => track.Value;
protected abstract Track GetTrack();
private RecyclableLazy<Track> track;

View File

@ -245,7 +245,7 @@ namespace osu.Game.Database
/// </summary>
protected abstract string[] HashableFileTypes { get; }
protected static void LogForModel(TModel model, string message, Exception e = null)
internal static void LogForModel(TModel model, string message, Exception e = null)
{
string prefix = $"[{(model?.Hash ?? "?????").Substring(0, 5)}]";

View File

@ -193,8 +193,8 @@ namespace osu.Game.Graphics.Backgrounds
float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats
float u2 = 1 - RNG.NextSingle();
float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); //random normal(0,1)
var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); //random normal(mean,stdDev^2)
float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1)
var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2)
return new TriangleParticle { Scale = scale };
}

View File

@ -158,7 +158,7 @@ namespace osu.Game.Graphics.Containers
{
if (!base.OnMouseDown(e)) return false;
//note that we are changing the colour of the box here as to not interfere with the hover effect.
// note that we are changing the colour of the box here as to not interfere with the hover effect.
box.FadeColour(highlightColour, 100);
return true;
}

View File

@ -139,7 +139,7 @@ namespace osu.Game.Graphics
return false;
dateText.Text = $"{date:d MMMM yyyy} ";
timeText.Text = $"{date:hh:mm:ss \"UTC\"z}";
timeText.Text = $"{date:HH:mm:ss \"UTC\"z}";
return true;
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.IPC
{
if (importer == null)
{
//we want to contact a remote osu! to handle the import.
// we want to contact a remote osu! to handle the import.
await SendMessageAsync(new ArchiveImportMessage { Path = path });
return;
}

View File

@ -127,7 +127,7 @@ namespace osu.Game.Online.API
case APIState.Offline:
case APIState.Connecting:
//work to restore a connection...
// work to restore a connection...
if (!HasLogin)
{
State = APIState.Offline;
@ -180,7 +180,7 @@ namespace osu.Game.Online.API
break;
}
//hard bail if we can't get a valid access token.
// hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout();
@ -274,7 +274,7 @@ namespace osu.Game.Online.API
{
req.Perform(this);
//we could still be in initialisation, at which point we don't want to say we're Online yet.
// we could still be in initialisation, at which point we don't want to say we're Online yet.
if (IsLoggedIn) State = APIState.Online;
failureCount = 0;
@ -339,7 +339,7 @@ namespace osu.Game.Online.API
log.Add($@"API failure count is now {failureCount}");
if (failureCount < 3)
//we might try again at an api level.
// we might try again at an api level.
return false;
if (State == APIState.Online)

View File

@ -98,7 +98,7 @@ namespace osu.Game.Online.API
if (checkAndScheduleFailure())
return;
if (!WebRequest.Aborted) //could have been aborted by a Cancel() call
if (!WebRequest.Aborted) // could have been aborted by a Cancel() call
{
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
WebRequest.Perform();

View File

@ -61,7 +61,7 @@ namespace osu.Game.Online.Chat
/// </summary>
public event Action<Message> MessageRemoved;
public bool ReadOnly => false; //todo not yet used.
public bool ReadOnly => false; // todo: not yet used.
public override string ToString() => Name;

View File

@ -93,6 +93,12 @@ namespace osu.Game.Online.Chat
{
if (!(e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel))
JoinChannel(e.NewValue);
if (e.NewValue?.MessagesLoaded == false)
{
// let's fetch a small number of messages to bring us up-to-date with the backlog.
fetchInitalMessages(e.NewValue);
}
}
/// <summary>
@ -375,12 +381,6 @@ namespace osu.Game.Online.Chat
if (CurrentChannel.Value == null)
CurrentChannel.Value = channel;
if (!channel.MessagesLoaded)
{
// let's fetch a small number of messages to bring us up-to-date with the backlog.
fetchInitalMessages(channel);
}
return channel;
}

View File

@ -78,13 +78,13 @@ namespace osu.Game.Online.Chat
{
result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText);
//since we just changed the line display text, offset any already processed links.
// since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
var details = GetLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group.
// adjust the offset for processing the current matches group.
captureOffset += m.Length - displayText.Length;
}
}

View File

@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Development;
@ -97,6 +98,7 @@ namespace osu.Game
private MainMenu menuScreen;
[CanBeNull]
private IntroScreen introScreen;
private Bindable<int> configRuleset;
@ -609,7 +611,7 @@ namespace osu.Game
loadComponentSingleFile(screenshotManager, Add);
//overlay elements
// overlay elements
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
@ -781,7 +783,7 @@ namespace osu.Game
{
var previousLoadStream = asyncLoadStream;
//chain with existing load stream
// chain with existing load stream
asyncLoadStream = Task.Run(async () =>
{
if (previousLoadStream != null)
@ -914,10 +916,7 @@ namespace osu.Game
if (ScreenStack.CurrentScreen is Loader)
return false;
if (introScreen == null)
return true;
if (!introScreen.DidLoadMenu || !(ScreenStack.CurrentScreen is IntroScreen))
if (introScreen?.DidLoadMenu == true && !(ScreenStack.CurrentScreen is IntroScreen))
{
Scheduler.Add(introScreen.MakeCurrent);
return true;

View File

@ -50,13 +50,6 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig)
{
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
{
button.Enabled.Value = false;
button.TooltipText = "this beatmap is currently not available for download.";
return;
}
noVideoSetting = osuConfig.GetBindable<bool>(OsuSetting.PreferNoVideo);
button.Action = () =>
@ -81,6 +74,26 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
break;
}
};
State.BindValueChanged(state =>
{
switch (state.NewValue)
{
case DownloadState.LocallyAvailable:
button.Enabled.Value = true;
button.TooltipText = string.Empty;
break;
default:
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
{
button.Enabled.Value = false;
button.TooltipText = "this beatmap is currently not available for download.";
}
break;
}
}, true);
}
}
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
: base(beatmap)
{
Width = 380;
Height = 140 + vertical_padding; //full height of all the elements plus vertical padding (autosize uses the image)
Height = 140 + vertical_padding; // full height of all the elements plus vertical padding (autosize uses the image)
}
protected override void LoadComplete()

View File

@ -140,7 +140,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 3, Bottom = 4 }, //To better lineup with the font
Margin = new MarginPadding { Left = 3, Bottom = 4 }, // To better lineup with the font
},
}
},
@ -264,7 +264,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
if (BeatmapSet.Value == null) return;
if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false)
if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable)
{
downloadButtonsContainer.Clear();
return;

View File

@ -105,6 +105,14 @@ namespace osu.Game.Overlays.Chat
private void newMessagesArrived(IEnumerable<Message> newMessages)
{
if (newMessages.Min(m => m.Id) < chatLines.Max(c => c.Message.Id))
{
// there is a case (on initial population) that we may receive past messages and need to reorder.
// easiest way is to just combine messages and recreate drawables (less worrying about day separators etc.)
newMessages = newMessages.Concat(chatLines.Select(c => c.Message)).OrderBy(m => m.Id).ToList();
ChatLineFlow.Clear();
}
bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage);
// Add up to last Channel.MAX_HISTORY messages

View File

@ -358,7 +358,7 @@ namespace osu.Game.Overlays
protected override void OnFocus(FocusEvent e)
{
//this is necessary as textbox is masked away and therefore can't get focus :(
// this is necessary as textbox is masked away and therefore can't get focus :(
textbox.TakeFocus();
base.OnFocus(e);
}

View File

@ -153,7 +153,7 @@ namespace osu.Game.Overlays.Comments
request?.Cancel();
loadCancellation?.Cancel();
request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0);
request.Success += onSuccess;
request.Success += res => Schedule(() => onSuccess(res));
api.PerformAsync(request);
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Overlays
{
if (v != Visibility.Hidden) return;
//handle the dialog being dismissed.
// handle the dialog being dismissed.
dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
if (dialog == CurrentDialog)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings;
@ -9,7 +10,11 @@ namespace osu.Game.Overlays.KeyBinding
{
public class GlobalKeyBindingsSection : SettingsSection
{
public override IconUsage Icon => FontAwesome.Solid.Globe;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.Globe
};
public override string Header => "Global";
public GlobalKeyBindingsSection(GlobalActionContainer manager)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
@ -10,7 +11,11 @@ namespace osu.Game.Overlays.KeyBinding
{
public class RulesetBindingsSection : SettingsSection
{
public override IconUsage Icon => (ruleset.CreateInstance().CreateIcon() as SpriteIcon)?.Icon ?? OsuIcon.Hot;
public override Drawable CreateIcon() => ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon
{
Icon = OsuIcon.Hot
};
public override string Header => ruleset.Name;
private readonly RulesetInfo ruleset;

View File

@ -37,7 +37,6 @@ namespace osu.Game.Overlays.Mods
protected readonly TriangleButton CloseButton;
protected readonly OsuSpriteText MultiplierLabel;
protected readonly OsuSpriteText UnrankedLabel;
protected override bool BlockNonPositionalInput => false;
@ -57,6 +56,8 @@ namespace osu.Game.Overlays.Mods
protected Color4 HighMultiplierColour;
private const float content_width = 0.8f;
private const float footer_button_spacing = 20;
private readonly FillFlowContainer footerContainer;
private SampleChannel sampleOn, sampleOff;
@ -103,7 +104,7 @@ namespace osu.Game.Overlays.Mods
{
new Dimension(GridSizeMode.Absolute, 90),
new Dimension(GridSizeMode.Distributed),
new Dimension(GridSizeMode.Absolute, 70),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@ -197,7 +198,8 @@ namespace osu.Game.Overlays.Mods
// Footer
new Container
{
RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Children = new Drawable[]
@ -215,7 +217,9 @@ namespace osu.Game.Overlays.Mods
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Width = content_width,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2),
LayoutDuration = 100,
LayoutEasing = Easing.OutQuint,
Padding = new MarginPadding
{
Vertical = 15,
@ -228,10 +232,8 @@ namespace osu.Game.Overlays.Mods
Width = 180,
Text = "Deselect All",
Action = DeselectAll,
Margin = new MarginPadding
{
Right = 20
}
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
CustomiseButton = new TriangleButton
{
@ -239,49 +241,41 @@ namespace osu.Game.Overlays.Mods
Text = "Customisation",
Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1,
Enabled = { Value = false },
Margin = new MarginPadding
{
Right = 20
}
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
CloseButton = new TriangleButton
{
Width = 180,
Text = "Close",
Action = Hide,
Margin = new MarginPadding
{
Right = 20
}
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
new OsuSpriteText
new FillFlowContainer
{
Text = @"Score Multiplier:",
Font = OsuFont.GetFont(size: 30),
Margin = new MarginPadding
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(footer_button_spacing / 2, 0),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Children = new Drawable[]
{
Top = 5,
Right = 10
}
new OsuSpriteText
{
Text = @"Score Multiplier:",
Font = OsuFont.GetFont(size: 30),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
MultiplierLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Width = 70, // make width fixed so reflow doesn't occur when multiplier number changes.
},
},
},
MultiplierLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
Margin = new MarginPadding
{
Top = 5
}
},
UnrankedLabel = new OsuSpriteText
{
Text = @"(Unranked)",
Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
Margin = new MarginPadding
{
Top = 5,
Left = 10
}
}
}
}
},
@ -327,7 +321,6 @@ namespace osu.Game.Overlays.Mods
{
LowMultiplierColour = colours.Red;
HighMultiplierColour = colours.Green;
UnrankedLabel.Colour = colours.Blue;
availableMods = osu.AvailableMods.GetBoundCopy();
@ -431,12 +424,10 @@ namespace osu.Game.Overlays.Mods
private void updateMods()
{
var multiplier = 1.0;
var ranked = true;
foreach (var mod in SelectedMods.Value)
{
multiplier *= mod.ScoreMultiplier;
ranked &= mod.Ranked;
}
MultiplierLabel.Text = $"{multiplier:N2}x";
@ -446,8 +437,6 @@ namespace osu.Game.Overlays.Mods
MultiplierLabel.FadeColour(LowMultiplierColour, 200);
else
MultiplierLabel.FadeColour(Color4.White, 200);
UnrankedLabel.FadeTo(ranked ? 0 : 1, 200);
}
private void updateModSettings(ValueChangedEvent<IReadOnlyList<Mod>> selectedMods)

View File

@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Music
{
text.Clear();
//space after the title to put a space between the title and artist
// space after the title to put a space between the title and artist
titleSprites = text.AddText(title.Value + @" ", sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)).OfType<SpriteText>();
text.AddText(artist.Value, sprite =>

View File

@ -66,7 +66,7 @@ namespace osu.Game.Overlays
beatmaps.ItemAdded += handleBeatmapAdded;
beatmaps.ItemRemoved += handleBeatmapRemoved;
beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()));
beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal).OrderBy(_ => RNG.Next()));
}
protected override void LoadComplete()
@ -172,10 +172,15 @@ namespace osu.Game.Overlays
}
/// <summary>
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
/// </summary>
/// <returns>The <see cref="PreviousTrackResult"/> that indicate the decided action</returns>
public PreviousTrackResult PreviousTrack()
public void PreviousTrack() => Schedule(() => prev());
/// <summary>
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
/// </summary>
/// <returns>The <see cref="PreviousTrackResult"/> that indicate the decided action.</returns>
private PreviousTrackResult prev()
{
var currentTrackPosition = current?.Track.CurrentTime;
@ -204,8 +209,7 @@ namespace osu.Game.Overlays
/// <summary>
/// Play the next random or playlist track.
/// </summary>
/// <returns>Whether the operation was successful.</returns>
public bool NextTrack() => next();
public void NextTrack() => Schedule(() => next());
private bool next(bool instant = false)
{
@ -246,7 +250,7 @@ namespace osu.Game.Overlays
}
else
{
//figure out the best direction based on order in playlist.
// figure out the best direction based on order in playlist.
var last = BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count();
var next = beatmap.NewValue == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count();
@ -319,13 +323,13 @@ namespace osu.Game.Overlays
return true;
case GlobalAction.MusicNext:
if (NextTrack())
if (next())
onScreenDisplay?.Display(new MusicControllerToast("Next track"));
return true;
case GlobalAction.MusicPrev:
switch (PreviousTrack())
switch (prev())
{
case PreviousTrackResult.Restart:
onScreenDisplay?.Display(new MusicControllerToast("Restart track"));

View File

@ -162,7 +162,7 @@ namespace osu.Game.Overlays.News
public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz").ToUpper();
}
//fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now
// fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now
public class ArticleInfo
{
public string Title { get; set; }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Notifications
{
base.LoadComplete();
//we may have received changes before we were displayed.
// we may have received changes before we were displayed.
updateState();
}

View File

@ -261,7 +261,7 @@ namespace osu.Game.Overlays
// todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync()
Task.Run(() =>
{
if (beatmap?.Beatmap == null) //this is not needed if a placeholder exists
if (beatmap?.Beatmap == null) // this is not needed if a placeholder exists
{
title.Text = @"Nothing to play";
artist.Text = @"Nothing to play";

View File

@ -31,7 +31,7 @@ namespace osu.Game.Overlays.OSD
InternalChildren = new Drawable[]
{
new Container //this container exists just to set a minimum width for the toast
new Container // this container exists just to set a minimum width for the toast
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
Font = OsuFont.GetFont(size: big ? 40 : 18, weight: FontWeight.Light)
},
new Container //Add a minimum size to the FillFlowContainer
new Container // Add a minimum size to the FillFlowContainer
{
Width = minimumWidth,
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new Container //artificial shadow
new Container // artificial shadow
{
RelativeSizeAxes = Axes.X,
Height = 3,

View File

@ -91,6 +91,8 @@ namespace osu.Game.Overlays.SearchableList
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
bindable.ValueChanged -= Bindable_ValueChanged;
}
}

View File

@ -94,7 +94,7 @@ namespace osu.Game.Overlays.SearchableList
RelativeSizeAxes = Axes.X,
},
},
new Box //keep the tab strip part of autosize, but don't put it in the flow container
new Box // keep the tab strip part of autosize, but don't put it in the flow container
{
RelativeSizeAxes = Axes.X,
Height = 1,

View File

@ -13,9 +13,12 @@ namespace osu.Game.Overlays.Settings.Sections
{
public override string Header => "Audio";
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "sound" });
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.VolumeUp
};
public override IconUsage Icon => FontAwesome.Solid.VolumeUp;
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "sound" });
public AudioSection()
{

View File

@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class DebugSection : SettingsSection
{
public override string Header => "Debug";
public override IconUsage Icon => FontAwesome.Solid.Bug;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.Bug
};
public DebugSection()
{

View File

@ -13,7 +13,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GameplaySection : SettingsSection
{
public override string Header => "Gameplay";
public override IconUsage Icon => FontAwesome.Regular.Circle;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Regular.Circle
};
public GameplaySection()
{

View File

@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GeneralSection : SettingsSection
{
public override string Header => "General";
public override IconUsage Icon => FontAwesome.Solid.Cog;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.Cog
};
public GeneralSection()
{

View File

@ -209,15 +209,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private IReadOnlyList<Size> getResolutions()
{
var resolutions = new List<Size> { new Size(9999, 9999) };
var currentDisplay = game.Window?.CurrentDisplay.Value;
if (game.Window != null)
if (currentDisplay != null)
{
resolutions.AddRange(game.Window.AvailableResolutions
.Where(r => r.Width >= 800 && r.Height >= 600)
.OrderByDescending(r => r.Width)
.ThenByDescending(r => r.Height)
.Select(res => new Size(res.Width, res.Height))
.Distinct());
resolutions.AddRange(currentDisplay.DisplayModes
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
.OrderByDescending(m => m.Size.Width)
.ThenByDescending(m => m.Size.Height)
.Select(m => m.Size)
.Distinct());
}
return resolutions;

View File

@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GraphicsSection : SettingsSection
{
public override string Header => "Graphics";
public override IconUsage Icon => FontAwesome.Solid.Laptop;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.Laptop
};
public GraphicsSection()
{

View File

@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class InputSection : SettingsSection
{
public override string Header => "Input";
public override IconUsage Icon => FontAwesome.Regular.Keyboard;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.Keyboard
};
public InputSection(KeyBindingPanel keyConfig)
{

View File

@ -11,7 +11,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class MaintenanceSection : SettingsSection
{
public override string Header => "Maintenance";
public override IconUsage Icon => FontAwesome.Solid.Wrench;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.Wrench
};
public MaintenanceSection()
{

View File

@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class OnlineSection : SettingsSection
{
public override string Header => "Online";
public override IconUsage Icon => FontAwesome.Solid.GlobeAsia;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.GlobeAsia
};
public OnlineSection()
{

View File

@ -19,7 +19,10 @@ namespace osu.Game.Overlays.Settings.Sections
public override string Header => "Skin";
public override IconUsage Icon => FontAwesome.Solid.PaintBrush;
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.PaintBrush
};
private readonly Bindable<SkinInfo> dropdownBindable = new Bindable<SkinInfo> { Default = SkinInfo.Default };
private readonly Bindable<int> configBindable = new Bindable<int>();

View File

@ -11,7 +11,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.Settings
{
@ -20,7 +19,7 @@ namespace osu.Game.Overlays.Settings
protected FillFlowContainer FlowContent;
protected override Container<Drawable> Content => FlowContent;
public abstract IconUsage Icon { get; }
public abstract Drawable CreateIcon();
public abstract string Header { get; }
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();

View File

@ -11,12 +11,13 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
public class SidebarButton : OsuButton
{
private readonly SpriteIcon drawableIcon;
private readonly ConstrainedIconContainer iconContainer;
private readonly SpriteText headerText;
private readonly Box selectionIndicator;
private readonly Container text;
@ -30,7 +31,7 @@ namespace osu.Game.Overlays.Settings
{
section = value;
headerText.Text = value.Header;
drawableIcon.Icon = value.Icon;
iconContainer.Icon = value.CreateIcon();
}
}
@ -78,7 +79,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
drawableIcon = new SpriteIcon
iconContainer = new ConstrainedIconContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -33,6 +33,9 @@ namespace osu.Game.Overlays.Toolbar
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
// Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden.
public override bool PropagateNonPositionalInputSubTree => true;
public Toolbar()
{
RelativeSizeAxes = Axes.X;
@ -148,7 +151,7 @@ namespace osu.Game.Overlays.Toolbar
protected override void PopOut()
{
userButton?.StateContainer.Hide();
userButton.StateContainer?.Hide();
this.MoveToY(-DrawSize.Y, transition_time, Easing.OutQuint);
this.FadeOut(transition_time);

View File

@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Toolbar
tooltipContainer = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.Both, //stops us being considered in parent's autosize
RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize
Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight,
Origin = TooltipAnchor,
Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5),

View File

@ -58,7 +58,7 @@ namespace osu.Game.Overlays
{
volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker)
{
Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } //to counter the mute button and re-center the volume meters
Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } // to counter the mute button and re-center the volume meters
},
volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker),
volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker),

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -26,6 +25,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Cached(typeof(DrawableHitObject))]
public abstract class DrawableHitObject : SkinReloadableDrawable
{
public event Action<DrawableHitObject> DefaultsApplied;
public readonly HitObject HitObject;
/// <summary>
@ -149,7 +150,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
samplesBindable.CollectionChanged += (_, __) => loadSamples();
updateState(ArmedState.Idle, true);
onDefaultsApplied();
apply(HitObject);
}
private void loadSamples()
@ -176,15 +177,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
AddInternal(Samples);
}
private void onDefaultsApplied() => apply(HitObject);
private void onDefaultsApplied(HitObject hitObject)
{
apply(hitObject);
DefaultsApplied?.Invoke(this);
}
private void apply(HitObject hitObject)
{
#pragma warning disable 618 // can be removed 20200417
if (GetType().GetMethod(nameof(AddNested), BindingFlags.NonPublic | BindingFlags.Instance)?.DeclaringType != typeof(DrawableHitObject))
return;
#pragma warning restore 618
if (nestedHitObjects.IsValueCreated)
{
nestedHitObjects.Value.Clear();
@ -195,7 +195,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
addNested(drawableNested);
drawableNested.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
drawableNested.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
drawableNested.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);
nestedHitObjects.Value.Add(drawableNested);
AddNestedHitObject(drawableNested);
}
}
@ -208,13 +212,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
}
/// <summary>
/// Adds a nested <see cref="DrawableHitObject"/>. This should not be used except for legacy nested <see cref="DrawableHitObject"/> usages.
/// </summary>
/// <param name="h"></param>
[Obsolete("Use AddNestedHitObject() / ClearNestedHitObjects() / CreateNestedHitObject() instead.")] // can be removed 20200417
protected virtual void AddNested(DrawableHitObject h) => addNested(h);
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to remove all previously-added nested <see cref="DrawableHitObject"/>s.
/// </summary>
@ -229,17 +226,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <returns>The drawable representation for <paramref name="hitObject"/>.</returns>
protected virtual DrawableHitObject CreateNestedHitObject(HitObject hitObject) => null;
private void addNested(DrawableHitObject hitObject)
{
// Todo: Exists for legacy purposes, can be removed 20200417
hitObject.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
hitObject.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
hitObject.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);
nestedHitObjects.Value.Add(hitObject);
}
#region State / Transform Management
/// <summary>

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects
/// <summary>
/// Invoked after <see cref="ApplyDefaults"/> has completed on this <see cref="HitObject"/>.
/// </summary>
public event Action DefaultsApplied;
public event Action<HitObject> DefaultsApplied;
public readonly Bindable<double> StartTimeBindable = new BindableDouble();
@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Objects
foreach (var h in nestedHitObjects)
h.ApplyDefaults(controlPointInfo, difficulty);
DefaultsApplied?.Invoke();
DefaultsApplied?.Invoke(this);
}
protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Replays
{
int newFrame = nextFrameIndex;
//ensure we aren't at an extent.
// ensure we aren't at an extent.
if (newFrame == currentFrameIndex) return false;
currentFrameIndex = newFrame;
@ -99,8 +99,8 @@ namespace osu.Game.Rulesets.Replays
if (frame == null)
return false;
return IsImportant(frame) && //a button is in a pressed state
Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; //the next frame is within an allowable time span
return IsImportant(frame) && // a button is in a pressed state
Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Newtonsoft.Json;
namespace osu.Game.Rulesets
@ -15,7 +16,20 @@ namespace osu.Game.Rulesets
public string ShortName { get; set; }
public string InstantiationInfo { get; set; }
private string instantiationInfo;
public string InstantiationInfo
{
get => instantiationInfo;
set => instantiationInfo = abbreviateInstantiationInfo(value);
}
private string abbreviateInstantiationInfo(string value)
{
// exclude version onwards, matching only on namespace and type.
// this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old.
return string.Join(',', value.Split(',').Take(2));
}
[JsonIgnore]
public bool Available { get; set; }

View File

@ -81,7 +81,7 @@ namespace osu.Game.Rulesets
var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r)).ToList();
//add all legacy rulesets first to ensure they have exclusive choice of primary key.
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
foreach (var r in instances.Where(r => r is ILegacyRuleset))
{
if (context.RulesetInfo.SingleOrDefault(dbRuleset => dbRuleset.ID == r.RulesetInfo.ID) == null)
@ -90,27 +90,23 @@ namespace osu.Game.Rulesets
context.SaveChanges();
//add any other modes
// add any other modes
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{
if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
// todo: StartsWith can be changed to Equals on 2020-11-08
// This is to give users enough time to have their database use new abbreviated info).
if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null)
context.RulesetInfo.Add(r.RulesetInfo);
}
context.SaveChanges();
//perform a consistency check
// perform a consistency check
foreach (var r in context.RulesetInfo)
{
try
{
var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo, asm =>
{
// for the time being, let's ignore the version being loaded.
// this allows for debug builds to successfully load rulesets (even though debug rulesets have a 0.0.0 version).
asm.Version = null;
return Assembly.Load(asm);
}, null))).RulesetInfo;
var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo))).RulesetInfo;
r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName;

View File

@ -43,5 +43,25 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
[Description(@"Perfect")]
Perfect,
/// <summary>
/// Indicates small tick miss.
/// </summary>
SmallTickMiss,
/// <summary>
/// Indicates a small tick hit.
/// </summary>
SmallTickHit,
/// <summary>
/// Indicates a large tick miss.
/// </summary>
LargeTickMiss,
/// <summary>
/// Indicates a large tick hit.
/// </summary>
LargeTickHit
}
}

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public PassThroughInputManager KeyBindingInputManager;
public override double GameplayStartTime => Objects.First().StartTime - 2000;
public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0;
private readonly Lazy<Playfield> playfield;
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.UI
/// The mods which are to be applied.
/// </summary>
[Cached(typeof(IReadOnlyList<Mod>))]
private readonly IReadOnlyList<Mod> mods;
protected readonly IReadOnlyList<Mod> Mods;
private FrameStabilityContainer frameStabilityContainer;
@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.UI
throw new ArgumentException($"{GetType()} expected the beatmap to contain hitobjects of type {typeof(TObject)}.", nameof(beatmap));
Beatmap = tBeatmap;
this.mods = mods?.ToArray() ?? Array.Empty<Mod>();
Mods = mods?.ToArray() ?? Array.Empty<Mod>();
RelativeSizeAxes = Axes.Both;
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.UI
.WithChild(ResumeOverlay)));
}
applyRulesetMods(mods, config);
applyRulesetMods(Mods, config);
loadObjects(cancellationToken);
}
@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.UI
Playfield.PostProcess();
foreach (var mod in mods.OfType<IApplicableToDrawableHitObjects>())
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects);
}

View File

@ -43,6 +43,15 @@ namespace osu.Game.Rulesets.UI
return true;
}
public virtual void Clear(bool disposeChildren = true)
{
ClearInternal(disposeChildren);
foreach (var kvp in startTimeMap)
kvp.Value.bindable.UnbindAll();
startTimeMap.Clear();
}
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
private void onStartTimeChanged(DrawableHitObject hitObject)

View File

@ -16,17 +16,23 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
private readonly IBindable<double> timeRange = new BindableDouble();
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly Dictionary<DrawableHitObject, Cached> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, Cached>();
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
private readonly LayoutValue initialStateCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
// Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
// A combined cache across all hit object states to reduce per-update iterations.
// When invalidated, one or more (but not necessarily all) hitobject states must be re-validated.
private readonly Cached combinedObjCache = new Cached();
public ScrollingHitObjectContainer()
{
RelativeSizeAxes = Axes.Both;
AddLayout(initialStateCache);
AddLayout(layoutCache);
}
[BackgroundDependencyLoader]
@ -35,13 +41,14 @@ namespace osu.Game.Rulesets.UI.Scrolling
direction.BindTo(scrollingInfo.Direction);
timeRange.BindTo(scrollingInfo.TimeRange);
direction.ValueChanged += _ => initialStateCache.Invalidate();
timeRange.ValueChanged += _ => initialStateCache.Invalidate();
direction.ValueChanged += _ => layoutCache.Invalidate();
timeRange.ValueChanged += _ => layoutCache.Invalidate();
}
public override void Add(DrawableHitObject hitObject)
{
initialStateCache.Invalidate();
combinedObjCache.Invalidate();
hitObject.DefaultsApplied += onDefaultsApplied;
base.Add(hitObject);
}
@ -51,24 +58,56 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (result)
{
initialStateCache.Invalidate();
combinedObjCache.Invalidate();
hitObjectInitialStateCache.Remove(hitObject);
hitObject.DefaultsApplied -= onDefaultsApplied;
}
return result;
}
public override void Clear(bool disposeChildren = true)
{
foreach (var h in Objects)
h.DefaultsApplied -= onDefaultsApplied;
base.Clear(disposeChildren);
combinedObjCache.Invalidate();
hitObjectInitialStateCache.Clear();
}
private void onDefaultsApplied(DrawableHitObject drawableObject)
{
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
// In such a case, combinedObjCache will take care of updating the hitobject.
if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var objCache))
{
combinedObjCache.Invalidate();
objCache.Invalidate();
}
}
private float scrollLength;
protected override void Update()
{
base.Update();
if (!initialStateCache.IsValid)
if (!layoutCache.IsValid)
{
foreach (var cached in hitObjectInitialStateCache.Values)
cached.Invalidate();
combinedObjCache.Invalidate();
scrollingInfo.Algorithm.Reset();
layoutCache.Validate();
}
if (!combinedObjCache.IsValid)
{
switch (direction.Value)
{
case ScrollingDirection.Up:
@ -81,15 +120,21 @@ namespace osu.Game.Rulesets.UI.Scrolling
break;
}
scrollingInfo.Algorithm.Reset();
foreach (var obj in Objects)
{
if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache))
objCache = hitObjectInitialStateCache[obj] = new Cached();
if (objCache.IsValid)
continue;
computeLifetimeStartRecursive(obj);
computeInitialStateRecursive(obj);
objCache.Validate();
}
initialStateCache.Validate();
combinedObjCache.Validate();
}
}
@ -101,8 +146,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
computeLifetimeStartRecursive(obj);
}
private readonly Dictionary<DrawableHitObject, Cached> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, Cached>();
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
{
float originAdjustment = 0.0f;
@ -134,12 +177,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Cant use AddOnce() since the delegate is re-constructed every invocation
private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
{
if (!hitObjectInitialStateCache.TryGetValue(hitObject, out var cached))
cached = hitObjectInitialStateCache[hitObject] = new Cached();
if (cached.IsValid)
return;
if (hitObject.HitObject is IHasEndTime e)
{
switch (direction.Value)
@ -163,8 +200,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition(obj, hitObject.HitObject.StartTime);
}
cached.Validate();
});
protected override void UpdateAfterChildrenLife()

View File

@ -12,7 +12,7 @@ namespace osu.Game.Scoring.Legacy
switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
{
case 3:
return scoreInfo.Statistics[HitResult.Perfect];
return getCount(scoreInfo, HitResult.Perfect);
}
return null;
@ -35,10 +35,10 @@ namespace osu.Game.Scoring.Legacy
case 0:
case 1:
case 3:
return scoreInfo.Statistics[HitResult.Great];
return getCount(scoreInfo, HitResult.Great);
case 2:
return scoreInfo.Statistics[HitResult.Perfect];
return getCount(scoreInfo, HitResult.Perfect);
}
return null;
@ -65,7 +65,10 @@ namespace osu.Game.Scoring.Legacy
switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
{
case 3:
return scoreInfo.Statistics[HitResult.Good];
return getCount(scoreInfo, HitResult.Good);
case 2:
return getCount(scoreInfo, HitResult.SmallTickMiss);
}
return null;
@ -78,6 +81,10 @@ namespace osu.Game.Scoring.Legacy
case 3:
scoreInfo.Statistics[HitResult.Good] = value;
break;
case 2:
scoreInfo.Statistics[HitResult.SmallTickMiss] = value;
break;
}
}
@ -87,10 +94,13 @@ namespace osu.Game.Scoring.Legacy
{
case 0:
case 1:
return scoreInfo.Statistics[HitResult.Good];
return getCount(scoreInfo, HitResult.Good);
case 3:
return scoreInfo.Statistics[HitResult.Ok];
return getCount(scoreInfo, HitResult.Ok);
case 2:
return getCount(scoreInfo, HitResult.LargeTickHit);
}
return null;
@ -108,6 +118,10 @@ namespace osu.Game.Scoring.Legacy
case 3:
scoreInfo.Statistics[HitResult.Ok] = value;
break;
case 2:
scoreInfo.Statistics[HitResult.LargeTickHit] = value;
break;
}
}
@ -117,7 +131,10 @@ namespace osu.Game.Scoring.Legacy
{
case 0:
case 3:
return scoreInfo.Statistics[HitResult.Meh];
return getCount(scoreInfo, HitResult.Meh);
case 2:
return getCount(scoreInfo, HitResult.SmallTickHit);
}
return null;
@ -131,13 +148,25 @@ namespace osu.Game.Scoring.Legacy
case 3:
scoreInfo.Statistics[HitResult.Meh] = value;
break;
case 2:
scoreInfo.Statistics[HitResult.SmallTickHit] = value;
break;
}
}
public static int? GetCountMiss(this ScoreInfo scoreInfo) =>
scoreInfo.Statistics[HitResult.Miss];
getCount(scoreInfo, HitResult.Miss);
public static void SetCountMiss(this ScoreInfo scoreInfo, int value) =>
scoreInfo.Statistics[HitResult.Miss] = value;
private static int? getCount(ScoreInfo scoreInfo, HitResult result)
{
if (scoreInfo.Statistics.TryGetValue(result, out var existing))
return existing;
return null;
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Screens
protected override bool OnKeyDown(KeyDownEvent e)
{
//we don't want to handle escape key.
// we don't want to handle escape key.
return false;
}

View File

@ -401,12 +401,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
HitObject draggedObject = movementBlueprint.HitObject;
// The final movement position, relative to screenSpaceMovementStartPosition
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// Retrieve a snapped position.
(Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
// Move the hitobjects
// Move the hitobjects.
if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition))))
return true;

View File

@ -12,14 +12,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class CentreMarker : CompositeDrawable
{
private const float triangle_width = 20;
private const float triangle_width = 15;
private const float triangle_height = 10;
private const float bar_width = 2;
public CentreMarker()
{
RelativeSizeAxes = Axes.Y;
Size = new Vector2(20, 1);
Size = new Vector2(triangle_width, 1);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -39,6 +39,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.BottomCentre,
Size = new Vector2(triangle_width, triangle_height),
Scale = new Vector2(1, -1)
},
new Triangle
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Size = new Vector2(triangle_width, triangle_height),
}
};
}
@ -46,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Red;
Colour = colours.RedDark;
}
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
});
// We don't want the centre marker to scroll
AddInternal(new CentreMarker());
AddInternal(new CentreMarker { Depth = float.MaxValue });
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
@ -60,9 +60,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track;
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
Zoom = getZoomLevelForVisibleMilliseconds(2000);
if (track.Length > 0)
{
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
Zoom = getZoomLevelForVisibleMilliseconds(2000);
}
}, true);
}
@ -135,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void scrollToTrackTime()
{
if (!track.IsLoaded)
if (!track.IsLoaded || track.Length == 0)
return;
ScrollTo((float)(adjustableClock.CurrentTime / track.Length) * Content.DrawWidth, false);

View File

@ -22,6 +22,7 @@ using osu.Game.Screens.Edit.Design;
using osuTK.Input;
using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
@ -37,7 +38,7 @@ using osu.Game.Users;
namespace osu.Game.Screens.Edit
{
[Cached(typeof(IBeatSnapProvider))]
public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IBeatSnapProvider
public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider
{
public override float BackgroundParallaxAmount => 0.1f;
@ -157,8 +158,8 @@ namespace osu.Game.Screens.Edit
{
Items = new[]
{
undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, undo),
redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, redo)
undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo)
}
}
}
@ -230,6 +231,30 @@ namespace osu.Game.Screens.Edit
clock.ProcessFrame();
}
public bool OnPressed(PlatformAction action)
{
switch (action.ActionType)
{
case PlatformActionType.Undo:
Undo();
return true;
case PlatformActionType.Redo:
Redo();
return true;
case PlatformActionType.Save:
saveBeatmap();
return true;
}
return false;
}
public void OnReleased(PlatformAction action)
{
}
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
@ -241,28 +266,6 @@ namespace osu.Game.Screens.Edit
case Key.Right:
seek(e, 1);
return true;
case Key.S:
if (e.ControlPressed)
{
saveBeatmap();
return true;
}
break;
case Key.Z:
if (e.ControlPressed)
{
if (e.ShiftPressed)
redo();
else
undo();
return true;
}
break;
}
return base.OnKeyDown(e);
@ -326,9 +329,9 @@ namespace osu.Game.Screens.Edit
return base.OnExiting(next);
}
private void undo() => changeHandler.RestoreState(-1);
protected void Undo() => changeHandler.RestoreState(-1);
private void redo() => changeHandler.RestoreState(1);
protected void Redo() => changeHandler.RestoreState(1);
private void resetTrack(bool seekToStart = false)
{

View File

@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -136,14 +137,26 @@ namespace osu.Game.Screens.Edit
/// <param name="hitObject">The <see cref="HitObject"/> to add.</param>
public void Add(HitObject hitObject)
{
trackStartTime(hitObject);
// Preserve existing sorting order in the beatmap
var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime);
mutableHitObjects.Insert(insertionIndex + 1, hitObject);
Insert(insertionIndex + 1, hitObject);
}
/// <summary>
/// Inserts a <see cref="HitObject"/> into this <see cref="EditorBeatmap"/>.
/// </summary>
/// <remarks>
/// It is the invoker's responsibility to make sure that <see cref="HitObject"/> sorting order is maintained.
/// </remarks>
/// <param name="index">The index to insert the <see cref="HitObject"/> at.</param>
/// <param name="hitObject">The <see cref="HitObject"/> to insert.</param>
public void Insert(int index, HitObject hitObject)
{
trackStartTime(hitObject);
mutableHitObjects.Insert(index, hitObject);
HitObjectAdded?.Invoke(hitObject);
updateHitObject(hitObject, true);
}
@ -189,6 +202,25 @@ namespace osu.Game.Screens.Edit
updateHitObject(null, true);
}
/// <summary>
/// Clears all <see cref="HitObjects"/> from this <see cref="EditorBeatmap"/>.
/// </summary>
public void Clear()
{
var removed = HitObjects.ToList();
mutableHitObjects.Clear();
foreach (var b in startTimeBindables)
b.Value.UnbindAll();
startTimeBindables.Clear();
foreach (var h in removed)
HitObjectRemoved?.Invoke(h);
updateHitObject(null, true);
}
private void trackStartTime(HitObject hitObject)
{
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();

View File

@ -63,8 +63,10 @@ namespace osu.Game.Screens.Edit
}
}
// Make the removal indices are sorted so that iteration order doesn't get messed up post-removal.
// Sort the indices to ensure that removal + insertion indices don't get jumbled up post-removal or post-insertion.
// This isn't strictly required, but the differ makes no guarantees about order.
toRemove.Sort();
toAdd.Sort();
// Apply the changes.
for (int i = toRemove.Count - 1; i >= 0; i--)
@ -74,7 +76,7 @@ namespace osu.Game.Screens.Edit
{
IBeatmap newBeatmap = readBeatmap(newState);
foreach (var i in toAdd)
editorBeatmap.Add(newBeatmap.HitObjects[i]);
editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
}
}
@ -84,7 +86,11 @@ namespace osu.Game.Screens.Edit
{
using (var stream = new MemoryStream(state))
using (var reader = new LineBufferedReader(stream, true))
return new PassThroughWorkingBeatmap(Decoder.GetDecoder<Beatmap>(reader).Decode(reader)).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset);
{
var decoded = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
decoded.BeatmapInfo.Ruleset = editorBeatmap.BeatmapInfo.Ruleset;
return new PassThroughWorkingBeatmap(decoded).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset);
}
}
private class PassThroughWorkingBeatmap : WorkingBeatmap

View File

@ -73,7 +73,7 @@ namespace osu.Game.Screens.Menu
if (!MenuMusic.Value)
{
var sets = beatmaps.GetAllUsableBeatmapSets();
var sets = beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal);
if (sets.Count > 0)
setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID);
}
@ -96,14 +96,12 @@ namespace osu.Game.Screens.Menu
Track = introBeatmap.Track;
}
public override bool OnExiting(IScreen next) => !DidLoadMenu;
public override void OnResuming(IScreen last)
{
this.FadeIn(300);
double fadeOutTime = exit_delay;
//we also handle the exit transition.
// we also handle the exit transition.
if (MenuVoice.Value)
seeya.Play();
else

View File

@ -162,7 +162,7 @@ namespace osu.Game.Screens.Menu
private IShader shader;
private Texture texture;
//Assuming the logo is a circle, we don't need a second dimension.
// Assuming the logo is a circle, we don't need a second dimension.
private float size;
private Color4 colour;
@ -209,13 +209,13 @@ namespace osu.Game.Screens.Menu
float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds);
float rotationCos = MathF.Cos(rotation);
float rotationSin = MathF.Sin(rotation);
//taking the cos and sin to the 0..1 range
// taking the cos and sin to the 0..1 range
var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size;
var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]);
//The distance between the position and the sides of the bar.
// The distance between the position and the sides of the bar.
var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2);
//The distance between the bottom side of the bar and the top side.
// The distance between the bottom side of the bar and the top side.
var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y);
var rectangle = new Quad(
@ -231,7 +231,7 @@ namespace osu.Game.Screens.Menu
colourInfo,
null,
vertexBatch.AddAction,
//barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that.
// barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that.
Vector2.Divide(inflation, barSize.Yx));
}
}

View File

@ -250,7 +250,7 @@ namespace osu.Game.Screens.Menu
(Background as BackgroundScreenDefault)?.Next();
//we may have consumed our preloaded instance, so let's make another.
// we may have consumed our preloaded instance, so let's make another.
preloadSongSelect();
if (Beatmap.Value.Track != null && music?.IsUserPaused != true)

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Internal;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -24,14 +23,14 @@ namespace osu.Game.Screens
{
/// <summary>
/// The amount of negative padding that should be applied to game background content which touches both the left and right sides of the screen.
/// This allows for the game content to be pushed byt he options/notification overlays without causing black areas to appear.
/// This allows for the game content to be pushed by the options/notification overlays without causing black areas to appear.
/// </summary>
public const float HORIZONTAL_OVERFLOW_PADDING = 50;
/// <summary>
/// A user-facing title for this screen.
/// </summary>
public virtual string Title => GetType().ShortDisplayName();
public virtual string Title => GetType().Name;
public string Description => Title;

View File

@ -119,7 +119,7 @@ namespace osu.Game.Screens.Play
FinishTransforms(true);
Scheduler.CancelDelayedTasks();
if (breaks == null) return; //we need breaks.
if (breaks == null) return; // we need breaks.
foreach (var b in breaks)
{

View File

@ -2,40 +2,37 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Scoring;
using osu.Game.Utils;
namespace osu.Game.Screens.Play
{
public class BreakTracker : Component
{
private readonly ScoreProcessor scoreProcessor;
private readonly double gameplayStartTime;
private PeriodTracker breaks;
/// <summary>
/// Whether the gameplay is currently in a break.
/// </summary>
public IBindable<bool> IsBreakTime => isBreakTime;
protected int CurrentBreakIndex;
private readonly BindableBool isBreakTime = new BindableBool();
private IReadOnlyList<BreakPeriod> breaks;
public IReadOnlyList<BreakPeriod> Breaks
{
get => breaks;
set
{
breaks = value;
// reset index in case the new breaks list is smaller than last one
isBreakTime.Value = false;
CurrentBreakIndex = 0;
breaks = new PeriodTracker(value.Where(b => b.HasEffect)
.Select(b => new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION)));
}
}
@ -49,34 +46,11 @@ namespace osu.Game.Screens.Play
{
base.Update();
isBreakTime.Value = getCurrentBreak()?.HasEffect == true
|| Clock.CurrentTime < gameplayStartTime
var time = Clock.CurrentTime;
isBreakTime.Value = breaks?.IsInAny(time) == true
|| time < gameplayStartTime
|| scoreProcessor?.HasCompleted.Value == true;
}
private BreakPeriod getCurrentBreak()
{
if (breaks?.Count > 0)
{
var time = Clock.CurrentTime;
if (time > breaks[CurrentBreakIndex].EndTime)
{
while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1)
CurrentBreakIndex++;
}
else
{
while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0)
CurrentBreakIndex--;
}
var closest = breaks[CurrentBreakIndex];
return closest.Contains(time) ? closest : null;
}
return null;
}
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void PopIn() => this.FadeIn(fade_duration);
protected override void PopOut() => this.FadeOut(fade_duration);
//We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible
// We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible
public override bool PropagateNonPositionalInputSubTree => true;
protected override bool OnKeyDown(KeyDownEvent e)

View File

@ -124,8 +124,8 @@ namespace osu.Game.Screens.Play
}
}
};
//Set this manually because an element with Alpha=0 won't take it size to AutoSizeContainer,
//so the size can be changing between buttonSprite and glowSprite.
// Set this manually because an element with Alpha=0 won't take it size to AutoSizeContainer,
// so the size can be changing between buttonSprite and glowSprite.
Height = buttonSprite.DrawHeight;
Width = buttonSprite.DrawWidth;
}

View File

@ -314,8 +314,8 @@ namespace osu.Game.Screens.Play
LoadTask = null;
//By default, we want to load the player and never be returned to.
//Note that this may change if the player we load requested a re-run.
// By default, we want to load the player and never be returned to.
// Note that this may change if the player we load requested a re-run.
ValidForResume = false;
if (player.LoadedBeatmapSuccessfully)
@ -360,7 +360,7 @@ namespace osu.Game.Screens.Play
{
if (!muteWarningShownOnce.Value)
{
//Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
// Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue)
{
notificationOverlay?.Post(new MutedNotification());

View File

@ -35,6 +35,8 @@ namespace osu.Game.Screens.Ranking.Expanded
private RollingCounter<long> scoreCounter;
private const float padding = 10;
/// <summary>
/// Creates a new <see cref="ExpandedPanelMiddleContent"/>.
/// </summary>
@ -46,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Expanded
RelativeSizeAxes = Axes.Both;
Masking = true;
Padding = new MarginPadding { Vertical = 10, Horizontal = 10 };
Padding = new MarginPadding(padding);
}
[BackgroundDependencyLoader]
@ -92,13 +94,17 @@ namespace osu.Game.Screens.Ranking.Expanded
Origin = Anchor.TopCentre,
Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold)
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new Container
{

View File

@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking
/// <summary>
/// Width of the panel when expanded.
/// </summary>
private const float expanded_width = 360;
public const float EXPANDED_WIDTH = 360;
/// <summary>
/// Height of the panel when expanded.
@ -183,7 +183,7 @@ namespace osu.Game.Screens.Ranking
switch (state)
{
case PanelState.Expanded:
this.ResizeTo(new Vector2(expanded_width, expanded_height), resize_duration, Easing.OutQuint);
this.ResizeTo(new Vector2(EXPANDED_WIDTH, expanded_height), resize_duration, Easing.OutQuint);
topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);

View File

@ -169,7 +169,7 @@ namespace osu.Game.Screens.Select
loadBeatmapSets(GetLoadableBeatmaps());
}
protected virtual IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable();
protected virtual IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.AllButFiles);
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{
@ -208,7 +208,7 @@ namespace osu.Game.Screens.Select
// without this, during a large beatmap import it is impossible to navigate the carousel.
applyActiveCriteria(false, alwaysResetScrollPosition: false);
//check if we can/need to maintain our current selection.
// check if we can/need to maintain our current selection.
if (previouslySelectedID != null)
select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet);

View File

@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select
set => tabs.Current = value;
}
public Action<BeatmapDetailAreaTabItem, bool> OnFilter; //passed the selected tab and if mods is checked
public Action<BeatmapDetailAreaTabItem, bool> OnFilter; // passed the selected tab and if mods is checked
public IReadOnlyList<BeatmapDetailAreaTabItem> TabItems
{

View File

@ -201,7 +201,7 @@ namespace osu.Game.Screens.Select
Schedule(() =>
{
if (beatmap != requestedBeatmap)
//the beatmap has been changed since we started the lookup.
// the beatmap has been changed since we started the lookup.
return;
var b = res.ToBeatmap(rulesets);
@ -222,7 +222,7 @@ namespace osu.Game.Screens.Select
Schedule(() =>
{
if (beatmap != requestedBeatmap)
//the beatmap has been changed since we started the lookup.
// the beatmap has been changed since we started the lookup.
return;
updateMetrics();

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
@ -55,10 +54,7 @@ namespace osu.Game.Screens.Select.Carousel
if (match)
{
var terms = new List<string>();
terms.AddRange(Beatmap.Metadata.SearchableTerms);
terms.Add(Beatmap.Version);
var terms = Beatmap.SearchableTerms;
foreach (var criteriaTerm in criteria.SearchTerms)
match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0);

View File

@ -52,7 +52,7 @@ namespace osu.Game.Screens.Select.Details
AutoSizeAxes = Axes.Y,
Children = new[]
{
FirstValue = new StatisticRow(), //circle size/key amount
FirstValue = new StatisticRow(), // circle size/key amount
HpDrain = new StatisticRow { Title = "HP Drain" },
Accuracy = new StatisticRow { Title = "Accuracy" },
ApproachRate = new StatisticRow { Title = "Approach Rate" },

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Users;
@ -32,9 +33,12 @@ namespace osu.Game.Screens.Select
Edit();
}, Key.Number4);
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new ResultsScreen(score));
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
}
protected void PresentScore(ScoreInfo score) =>
FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new ResultsScreen(score)));
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
public override void OnResuming(IScreen last)

View File

@ -34,6 +34,7 @@ using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring;
namespace osu.Game.Screens.Select
@ -77,7 +78,7 @@ namespace osu.Game.Screens.Select
protected BeatmapCarousel Carousel { get; private set; }
private DifficultyRecommender recommender;
private readonly DifficultyRecommender recommender = new DifficultyRecommender();
private BeatmapInfoWedge beatmapInfoWedge;
private DialogOverlay dialogOverlay;
@ -92,6 +93,8 @@ namespace osu.Game.Screens.Select
private SampleChannel sampleChangeDifficulty;
private SampleChannel sampleChangeBeatmap;
private Container carouselContainer;
protected BeatmapDetailArea BeatmapDetails { get; private set; }
private readonly Bindable<RulesetInfo> decoupledRuleset = new Bindable<RulesetInfo>();
@ -105,9 +108,22 @@ namespace osu.Game.Screens.Select
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
transferRulesetValue();
LoadComponentAsync(Carousel = new BeatmapCarousel
{
AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
BleedTop = FilterControl.HEIGHT,
BleedBottom = Footer.HEIGHT,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
}, c => carouselContainer.Child = c);
AddRangeInternal(new Drawable[]
{
recommender = new DifficultyRecommender(),
recommender,
new ResetScrollContainer(() => Carousel.ScrollToSelected())
{
RelativeSizeAxes = Axes.Y,
@ -139,7 +155,7 @@ namespace osu.Game.Screens.Select
Padding = new MarginPadding { Right = -150 },
},
},
new Container
carouselContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
@ -147,18 +163,7 @@ namespace osu.Game.Screens.Select
Top = FilterControl.HEIGHT,
Bottom = Footer.HEIGHT
},
Child = Carousel = new BeatmapCarousel
{
AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
BleedTop = FilterControl.HEIGHT,
BleedBottom = Footer.HEIGHT,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
},
Child = new LoadingSpinner(true) { State = { Value = Visibility.Visible } }
}
},
}
@ -286,7 +291,7 @@ namespace osu.Game.Screens.Select
Schedule(() =>
{
// if we have no beatmaps but osu-stable is found, let's prompt the user to import.
if (!beatmaps.GetAllUsableBeatmapSetsEnumerable().Any() && beatmaps.StableInstallationAvailable)
if (!beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.Minimal).Any() && beatmaps.StableInstallationAvailable)
{
dialogOverlay.Push(new ImportFromStablePopup(() =>
{
@ -337,13 +342,17 @@ namespace osu.Game.Screens.Select
/// Call to make a selection and perform the default action for this SongSelect.
/// </summary>
/// <param name="beatmap">An optional beatmap to override the current carousel selection.</param>
/// <param name="performStartAction">Whether to trigger <see cref="OnStart"/>.</param>
public void FinaliseSelection(BeatmapInfo beatmap = null, bool performStartAction = true)
/// <param name="ruleset">An optional ruleset to override the current carousel selection.</param>
/// <param name="customStartAction">An optional custom action to perform instead of <see cref="OnStart"/>.</param>
public void FinaliseSelection(BeatmapInfo beatmap = null, RulesetInfo ruleset = null, Action customStartAction = null)
{
// This is very important as we have not yet bound to screen-level bindables before the carousel load is completed.
if (!Carousel.BeatmapSetsLoaded)
return;
if (ruleset != null)
Ruleset.Value = ruleset;
transferRulesetValue();
// while transferRulesetValue will flush, it only does so if the ruleset changes.
@ -364,7 +373,12 @@ namespace osu.Game.Screens.Select
selectionChangedDebounce = null;
}
if (performStartAction && OnStart())
if (customStartAction != null)
{
customStartAction();
Carousel.AllowSelection = false;
}
else if (OnStart())
Carousel.AllowSelection = false;
}
@ -793,7 +807,7 @@ namespace osu.Game.Screens.Select
Masking = true;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Width = panel_overflow; //avoid horizontal masking so the panels don't clip when screen stack is pushed.
Width = panel_overflow; // avoid horizontal masking so the panels don't clip when screen stack is pushed.
InternalChild = Content = new Container
{
RelativeSizeAxes = Axes.Both,

View File

@ -23,6 +23,7 @@ namespace osu.Game.Tests.Beatmaps
HitObjects = baseBeatmap.HitObjects;
BeatmapInfo.Ruleset = ruleset;
BeatmapInfo.RulesetID = ruleset.ID ?? 0;
BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata;
BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo };
BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo

View File

@ -27,6 +27,10 @@ namespace osu.Game.Tests.Beatmaps
this.storyboard = storyboard;
}
public override bool TrackLoaded => true;
public override bool BeatmapLoaded => true;
protected override IBeatmap GetBeatmap() => beatmap;
protected override Storyboard GetStoryboard() => storyboard ?? base.GetStoryboard();

View File

@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual
/// Provides a clock, beat-divisor, and scrolling capability for test cases of editor components that
/// are preferrably tested within the presence of a clock and seek controls.
/// </summary>
public abstract class EditorClockTestScene : OsuTestScene
public abstract class EditorClockTestScene : OsuManualInputManagerTestScene
{
protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor();
protected new readonly EditorClock Clock;

View File

@ -3,9 +3,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Tests.Visual
{
@ -13,6 +17,8 @@ namespace osu.Game.Tests.Visual
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(Editor), typeof(EditorScreen) };
protected Editor Editor { get; private set; }
private readonly Ruleset ruleset;
protected EditorTestScene(Ruleset ruleset)
@ -30,7 +36,11 @@ namespace osu.Game.Tests.Visual
{
base.SetUpSteps();
AddStep("Load editor", () => LoadScreen(new Editor()));
AddStep("load editor", () => LoadScreen(Editor = CreateEditor()));
AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
}
protected virtual Editor CreateEditor() => new Editor();
}
}

View File

@ -30,6 +30,11 @@ namespace osu.Game.Tests.Visual
set => scrollingInfo.TimeRange.Value = value;
}
public ScrollingDirection Direction
{
set => scrollingInfo.Direction.Value = value;
}
public IScrollingInfo ScrollingInfo => scrollingInfo;
[Cached(Type = typeof(IScrollingInfo))]

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Utils
{
/// <summary>
/// Represents a tracking component used for whether a specific time instant falls into any of the provided periods.
/// </summary>
public class PeriodTracker
{
private readonly List<Period> periods;
private int nearestIndex;
public PeriodTracker(IEnumerable<Period> periods)
{
this.periods = periods.OrderBy(period => period.Start).ToList();
}
/// <summary>
/// Whether the provided time is in any of the added periods.
/// </summary>
/// <param name="time">The time value to check.</param>
public bool IsInAny(double time)
{
if (periods.Count == 0)
return false;
if (time > periods[nearestIndex].End)
{
while (time > periods[nearestIndex].End && nearestIndex < periods.Count - 1)
nearestIndex++;
}
else
{
while (time < periods[nearestIndex].Start && nearestIndex > 0)
nearestIndex--;
}
var nearest = periods[nearestIndex];
return time >= nearest.Start && time <= nearest.End;
}
}
public readonly struct Period
{
/// <summary>
/// The start time of this period.
/// </summary>
public readonly double Start;
/// <summary>
/// The end time of this period.
/// </summary>
public readonly double End;
public Period(double start, double end)
{
if (start >= end)
throw new ArgumentException($"Invalid period provided, {nameof(start)} must be less than {nameof(end)}");
Start = start;
End = end;
}
}
}

View File

@ -18,13 +18,14 @@
</None>
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Dapper" Version="2.0.35" />
<PackageReference Include="DiffPlex" Version="1.6.1" />
<PackageReference Include="Humanizer" Version="2.8.2" />
<PackageReference Include="Humanizer" Version="2.8.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.421.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.412.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.508.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
<PackageReference Include="Sentry" Version="2.1.1" />
<PackageReference Include="SharpCompress" Version="0.25.0" />
<PackageReference Include="NUnit" Version="3.12.0" />