mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 15:16:38 +09:00
Merge branch 'master' into publicly-expose-hud
This commit is contained in:
@ -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}]";
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
195
osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
Normal file
195
osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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)}]";
|
||||
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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 =>
|
||||
|
@ -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"));
|
||||
|
@ -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; }
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -91,6 +91,8 @@ namespace osu.Game.Overlays.SearchableList
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
bindable.ValueChanged -= Bindable_ValueChanged;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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>();
|
||||
|
@ -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>();
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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" },
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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))]
|
||||
|
69
osu.Game/Utils/PeriodTracker.cs
Normal file
69
osu.Game/Utils/PeriodTracker.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
Reference in New Issue
Block a user