Merge branch 'master' into fix-draining-in-between-break-sections

This commit is contained in:
Bartłomiej Dach
2020-05-10 17:57:44 +02:00
497 changed files with 11490 additions and 3592 deletions

View File

@ -149,7 +149,17 @@ namespace osu.Game.Beatmaps
}
}
public override string ToString() => $"{Metadata} [{Version}]".Trim();
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}]";
return $"{Metadata} {version}".Trim();
}
public bool Equals(BeatmapInfo other)
{

View File

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

View File

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

View File

@ -53,7 +53,11 @@ namespace osu.Game.Beatmaps
public string AudioFile { get; set; }
public string BackgroundFile { get; set; }
public override string ToString() => $"{Artist} - {Title} ({Author})";
public override string ToString()
{
string author = Author == null ? string.Empty : $"({Author})";
return $"{Artist} - {Title} {author}".Trim();
}
[JsonIgnore]
public string[] SearchableTerms => new[]

View File

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

View File

@ -5,7 +5,7 @@ using System;
namespace osu.Game.Beatmaps.ControlPoints
{
public abstract class ControlPoint : IComparable<ControlPoint>, IEquatable<ControlPoint>
public abstract class ControlPoint : IComparable<ControlPoint>
{
/// <summary>
/// The time at which the control point takes effect.
@ -19,12 +19,10 @@ namespace osu.Game.Beatmaps.ControlPoints
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
/// <summary>
/// Whether this control point is equivalent to another, ignoring time.
/// Determines whether this <see cref="ControlPoint"/> results in a meaningful change when placed alongside another.
/// </summary>
/// <param name="other">Another control point to compare with.</param>
/// <returns>Whether equivalent.</returns>
public abstract bool EquivalentTo(ControlPoint other);
public bool Equals(ControlPoint other) => Time == other?.Time && EquivalentTo(other);
/// <param name="existing">An existing control point to compare with.</param>
/// <returns>Whether this <see cref="ControlPoint"/> is redundant when placed alongside <paramref name="existing"/>.</returns>
public abstract bool IsRedundant(ControlPoint existing);
}
}

View File

@ -56,6 +56,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// All control points, of all types.
/// </summary>
[JsonIgnore]
public IEnumerable<ControlPoint> AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray();
/// <summary>
@ -247,7 +248,7 @@ namespace osu.Game.Beatmaps.ControlPoints
break;
}
return existing?.EquivalentTo(newPoint) == true;
return newPoint?.IsRedundant(existing) == true;
}
private void groupItemAdded(ControlPoint controlPoint)

View File

@ -27,7 +27,8 @@ namespace osu.Game.Beatmaps.ControlPoints
set => SpeedMultiplierBindable.Value = value;
}
public override bool EquivalentTo(ControlPoint other) =>
other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
public override bool IsRedundant(ControlPoint existing)
=> existing is DifficultyControlPoint existingDifficulty
&& SpeedMultiplier == existingDifficulty.SpeedMultiplier;
}
}

View File

@ -35,8 +35,10 @@ namespace osu.Game.Beatmaps.ControlPoints
set => KiaiModeBindable.Value = value;
}
public override bool EquivalentTo(ControlPoint other) =>
other is EffectControlPoint otherTyped &&
KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine;
public override bool IsRedundant(ControlPoint existing)
=> !OmitFirstBarLine
&& existing is EffectControlPoint existingEffect
&& KiaiMode == existingEffect.KiaiMode
&& OmitFirstBarLine == existingEffect.OmitFirstBarLine;
}
}

View File

@ -68,8 +68,9 @@ namespace osu.Game.Beatmaps.ControlPoints
return newSampleInfo;
}
public override bool EquivalentTo(ControlPoint other) =>
other is SampleControlPoint otherTyped &&
SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume;
public override bool IsRedundant(ControlPoint existing)
=> existing is SampleControlPoint existingSample
&& SampleBank == existingSample.SampleBank
&& SampleVolume == existingSample.SampleVolume;
}
}

View File

@ -48,8 +48,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
public double BPM => 60000 / BeatLength;
public override bool EquivalentTo(ControlPoint other) =>
other is TimingControlPoint otherTyped
&& TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
// Timing points are never redundant as they can change the time signature.
public override bool IsRedundant(ControlPoint existing) => false;
}
}

View File

@ -386,17 +386,10 @@ namespace osu.Game.Beatmaps.Formats
SampleVolume = sampleVolume,
CustomSampleBank = customSampleBank,
}, timingChange);
// To handle the scenario where a non-timing line shares the same time value as a subsequent timing line but
// appears earlier in the file, we buffer non-timing control points and rewrite them *after* control points from the timing line
// with the same time value (allowing them to overwrite as necessary).
//
// The expected outcome is that we prefer the non-timing line's adjustments over the timing line's adjustments when time is equal.
if (timingChange)
flushPendingPoints();
}
private readonly List<ControlPoint> pendingControlPoints = new List<ControlPoint>();
private readonly HashSet<Type> pendingControlPointTypes = new HashSet<Type>();
private double pendingControlPointsTime;
private void addControlPoint(double time, ControlPoint point, bool timingChange)
@ -405,21 +398,28 @@ namespace osu.Game.Beatmaps.Formats
flushPendingPoints();
if (timingChange)
{
beatmap.ControlPointInfo.Add(time, point);
return;
}
pendingControlPoints.Insert(0, point);
else
pendingControlPoints.Add(point);
pendingControlPoints.Add(point);
pendingControlPointsTime = time;
}
private void flushPendingPoints()
{
foreach (var p in pendingControlPoints)
beatmap.ControlPointInfo.Add(pendingControlPointsTime, p);
// Changes from non-timing-points are added to the end of the list (see addControlPoint()) and should override any changes from timing-points (added to the start of the list).
for (int i = pendingControlPoints.Count - 1; i >= 0; i--)
{
var type = pendingControlPoints[i].GetType();
if (pendingControlPointTypes.Contains(type))
continue;
pendingControlPointTypes.Add(type);
beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]);
}
pendingControlPoints.Clear();
pendingControlPointTypes.Clear();
}
private void handleHitObject(string line)

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
@ -10,7 +11,9 @@ using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
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
{
@ -48,7 +51,7 @@ namespace osu.Game.Beatmaps.Formats
handleEvents(writer);
writer.WriteLine();
handleTimingPoints(writer);
handleControlPoints(writer);
writer.WriteLine();
handleHitObjects(writer);
@ -58,7 +61,7 @@ namespace osu.Game.Beatmaps.Formats
{
writer.WriteLine("[General]");
writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}"));
if (beatmap.Metadata.AudioFile != null) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}"));
writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}"));
writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}"));
// Todo: Not all countdown types are supported by lazer yet
@ -103,15 +106,15 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[Metadata]");
writer.WriteLine(FormattableString.Invariant($"Title: {beatmap.Metadata.Title}"));
writer.WriteLine(FormattableString.Invariant($"TitleUnicode: {beatmap.Metadata.TitleUnicode}"));
if (beatmap.Metadata.TitleUnicode != null) writer.WriteLine(FormattableString.Invariant($"TitleUnicode: {beatmap.Metadata.TitleUnicode}"));
writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}"));
writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}"));
if (beatmap.Metadata.ArtistUnicode != null) writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}"));
writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.AuthorString}"));
writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.Version}"));
writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}"));
writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}"));
writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID ?? -1}"));
if (beatmap.Metadata.Source != null) writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}"));
if (beatmap.Metadata.Tags != null) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
if (beatmap.BeatmapInfo.OnlineBeatmapID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID}"));
if (beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID}"));
}
private void handleDifficulty(TextWriter writer)
@ -122,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}"));
}
@ -137,7 +145,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}"));
}
private void handleTimingPoints(TextWriter writer)
private void handleControlPoints(TextWriter writer)
{
if (beatmap.ControlPointInfo.Groups.Count == 0)
return;
@ -146,20 +154,30 @@ namespace osu.Game.Beatmaps.Formats
foreach (var group in beatmap.ControlPointInfo.Groups)
{
var timingPoint = group.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
var samplePoint = beatmap.ControlPointInfo.SamplePointAt(group.Time);
var effectPoint = beatmap.ControlPointInfo.EffectPointAt(group.Time);
var groupTimingPoint = group.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
// Convert beat length the legacy format
double beatLength;
if (timingPoint != null)
beatLength = timingPoint.BeatLength;
else
beatLength = -100 / difficultyPoint.SpeedMultiplier;
// If the group contains a timing control point, it needs to be output separately.
if (groupTimingPoint != null)
{
writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},"));
writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},"));
outputControlPointEffectsAt(groupTimingPoint.Time, true);
}
// Output any remaining effects as secondary non-timing control point.
var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
writer.Write(FormattableString.Invariant($"{group.Time},"));
writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},"));
outputControlPointEffectsAt(group.Time, false);
}
void outputControlPointEffectsAt(double time, bool isTimingPoint)
{
var samplePoint = beatmap.ControlPointInfo.SamplePointAt(time);
var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
HitSampleInfo tempHitSample = samplePoint.ApplyTo(new HitSampleInfo());
HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo());
// Convert effect flags to the legacy format
LegacyEffectFlags effectFlags = LegacyEffectFlags.None;
@ -168,13 +186,11 @@ namespace osu.Game.Beatmaps.Formats
if (effectPoint.OmitFirstBarLine)
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
writer.Write(FormattableString.Invariant($"{group.Time},"));
writer.Write(FormattableString.Invariant($"{beatLength},"));
writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(group.Time).TimeSignature},"));
writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature},"));
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample.Suffix)},"));
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
writer.Write(FormattableString.Invariant($"{(timingPoint != null ? '1' : '0')},"));
writer.Write(FormattableString.Invariant($"{(isTimingPoint ? '1' : '0')},"));
writer.Write(FormattableString.Invariant($"{(int)effectFlags}"));
writer.WriteLine();
}
@ -187,65 +203,63 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[HitObjects]");
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);
break;
case 1:
foreach (var h in beatmap.HitObjects)
handleTaikoHitObject(writer, h);
position = ((IHasPosition)hitObject).Position;
break;
case 2:
foreach (var h in beatmap.HitObjects)
handleCatchHitObject(writer, h);
position.X = ((IHasXPosition)hitObject).X * 512;
break;
case 3:
foreach (var h in beatmap.HitObjects)
handleManiaHitObject(writer, h);
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)
{
@ -254,7 +268,10 @@ namespace osu.Game.Beatmaps.Formats
break;
case IHasEndTime _:
type |= LegacyHitObjectType.Spinner | LegacyHitObjectType.NewCombo;
if (beatmap.BeatmapInfo.RulesetID == 3)
type |= LegacyHitObjectType.Hold;
else
type |= LegacyHitObjectType.Spinner;
break;
default:
@ -265,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;
@ -301,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 ? "|" : ",");
}
}
@ -328,11 +345,19 @@ namespace osu.Game.Beatmaps.Formats
}
}
private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
private void addEndTimeData(TextWriter writer, HitObject hitObject)
{
var endTimeData = (IHasEndTime)hitObject;
var type = getObjectType(hitObject);
private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
char suffix = ',';
private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
// 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)
{
@ -346,7 +371,7 @@ namespace osu.Game.Beatmaps.Formats
if (!banksOnly)
{
string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))?.Suffix);
string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty;
int volume = samples.FirstOrDefault()?.Volume ?? 100;
@ -402,6 +427,15 @@ namespace osu.Game.Beatmaps.Formats
}
}
private string toLegacyCustomSampleBank(string sampleSuffix) => string.IsNullOrEmpty(sampleSuffix) ? "0" : sampleSuffix;
private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
{
if (hitSampleInfo == null)
return "0";
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture);
return "0";
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO;
using osu.Game.Rulesets.Objects.Legacy;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Formats
@ -149,7 +150,8 @@ namespace osu.Game.Beatmaps.Formats
HitObjects,
Variables,
Fonts,
Mania
CatchTheBeat,
Mania,
}
internal class LegacyDifficultyControlPoint : DifficultyControlPoint
@ -168,15 +170,19 @@ namespace osu.Game.Beatmaps.Formats
{
var baseInfo = base.ApplyTo(hitSampleInfo);
if (string.IsNullOrEmpty(baseInfo.Suffix) && CustomSampleBank > 1)
baseInfo.Suffix = CustomSampleBank.ToString();
if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy
&& legacy.CustomSampleBank == 0)
{
legacy.CustomSampleBank = CustomSampleBank;
}
return baseInfo;
}
public override bool EquivalentTo(ControlPoint other) =>
base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped &&
CustomSampleBank == otherTyped.CustomSampleBank;
public override bool IsRedundant(ControlPoint existing)
=> base.IsRedundant(existing)
&& existing is LegacySampleControlPoint existingSample
&& CustomSampleBank == existingSample.CustomSampleBank;
}
}
}

View File

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

View File

@ -49,6 +49,7 @@ namespace osu.Game.Configuration
};
Set(OsuSetting.ExternalLinkWarning, true);
Set(OsuSetting.PreferNoVideo, false);
// Audio
Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
@ -87,7 +88,9 @@ namespace osu.Game.Configuration
Set(OsuSetting.ShowInterface, true);
Set(OsuSetting.ShowProgressGraph, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
Set(OsuSetting.KeyOverlay, false);
Set(OsuSetting.PositionalHitSounds, true);
Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
Set(OsuSetting.FloatingComments, false);
@ -176,11 +179,13 @@ namespace osu.Game.Configuration
LightenDuringBreaks,
ShowStoryboard,
KeyOverlay,
PositionalHitSounds,
ScoreMeter,
FloatingComments,
ShowInterface,
ShowProgressGraph,
ShowHealthDisplayWhenCantFail,
FadePlayfieldWhenHealthLow,
MouseDisableButtons,
MouseDisableWheel,
AudioOffset,
@ -212,6 +217,7 @@ namespace osu.Game.Configuration
IncreaseFirstObjectVisibility,
ScoreDisplayMode,
ExternalLinkWarning,
PreferNoVideo,
Scaling,
ScalingPositionX,
ScalingPositionY,

View File

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

View File

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

View File

@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat)
if (timingPoint == lastTimingPoint && beatIndex == lastBeat)
return;
using (BeginDelayedSequence(-TimeSinceLastBeat, true))

View File

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

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -16,12 +17,7 @@ namespace osu.Game.Graphics.Containers
public class SectionsContainer<T> : Container<T>
where T : Drawable
{
private Drawable expandableHeader, fixedHeader, footer, headerBackground;
private readonly OsuScrollContainer scrollContainer;
private readonly Container headerBackgroundContainer;
private readonly FlowContainer<T> scrollContentContainer;
protected override Container<T> Content => scrollContentContainer;
public Bindable<T> SelectedSection { get; } = new Bindable<T>();
public Drawable ExpandableHeader
{
@ -83,6 +79,7 @@ namespace osu.Game.Graphics.Containers
headerBackgroundContainer.Clear();
headerBackground = value;
if (value == null) return;
headerBackgroundContainer.Add(headerBackground);
@ -91,15 +88,37 @@ namespace osu.Game.Graphics.Containers
}
}
public Bindable<T> SelectedSection { get; } = new Bindable<T>();
protected override Container<T> Content => scrollContentContainer;
protected virtual FlowContainer<T> CreateScrollContentContainer()
=> new FillFlowContainer<T>
private readonly OsuScrollContainer scrollContainer;
private readonly Container headerBackgroundContainer;
private readonly MarginPadding originalSectionsMargin;
private Drawable expandableHeader, fixedHeader, footer, headerBackground;
private FlowContainer<T> scrollContentContainer;
private float headerHeight, footerHeight;
private float lastKnownScroll;
public SectionsContainer()
{
AddRangeInternal(new Drawable[]
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
};
scrollContainer = CreateScrollContainer().With(s =>
{
s.RelativeSizeAxes = Axes.Both;
s.Masking = true;
s.ScrollbarVisible = false;
s.Child = scrollContentContainer = CreateScrollContentContainer();
}),
headerBackgroundContainer = new Container
{
RelativeSizeAxes = Axes.X
}
});
originalSectionsMargin = scrollContentContainer.Margin;
}
public override void Add(T drawable)
{
@ -109,40 +128,23 @@ namespace osu.Game.Graphics.Containers
footerHeight = float.NaN;
}
private float headerHeight, footerHeight;
private readonly MarginPadding originalSectionsMargin;
private void updateSectionsMargin()
{
if (!Children.Any()) return;
var newMargin = originalSectionsMargin;
newMargin.Top += headerHeight;
newMargin.Bottom += footerHeight;
scrollContentContainer.Margin = newMargin;
}
public SectionsContainer()
{
AddInternal(scrollContainer = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
ScrollbarVisible = false,
Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() }
});
AddInternal(headerBackgroundContainer = new Container
{
RelativeSizeAxes = Axes.X
});
originalSectionsMargin = scrollContentContainer.Margin;
}
public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0));
public void ScrollTo(Drawable section) =>
scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0));
public void ScrollToTop() => scrollContainer.ScrollTo(0);
[NotNull]
protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer();
[NotNull]
protected virtual FlowContainer<T> CreateScrollContentContainer() =>
new FillFlowContainer<T>
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
};
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
var result = base.OnInvalidate(invalidation, source);
@ -156,8 +158,6 @@ namespace osu.Game.Graphics.Containers
return result;
}
private float lastKnownScroll;
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@ -208,5 +208,16 @@ namespace osu.Game.Graphics.Containers
SelectedSection.Value = bestMatch;
}
}
private void updateSectionsMargin()
{
if (!Children.Any()) return;
var newMargin = originalSectionsMargin;
newMargin.Top += headerHeight;
newMargin.Bottom += footerHeight;
scrollContentContainer.Margin = newMargin;
}
}
}

View File

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

View File

@ -42,6 +42,8 @@ namespace osu.Game.Graphics.UserInterface
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
updateTextColour();
Item.Action.BindDisabledChanged(_ => updateState(), true);
}
private void updateTextColour()
@ -65,19 +67,33 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
sampleHover.Play();
text.BoldText.FadeIn(transition_length, Easing.OutQuint);
text.NormalText.FadeOut(transition_length, Easing.OutQuint);
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
text.BoldText.FadeOut(transition_length, Easing.OutQuint);
text.NormalText.FadeIn(transition_length, Easing.OutQuint);
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
Alpha = Item.Action.Disabled ? 0.2f : 1;
if (IsHovered && !Item.Action.Disabled)
{
sampleHover.Play();
text.BoldText.FadeIn(transition_length, Easing.OutQuint);
text.NormalText.FadeOut(transition_length, Easing.OutQuint);
}
else
{
text.BoldText.FadeOut(transition_length, Easing.OutQuint);
text.NormalText.FadeIn(transition_length, Easing.OutQuint);
}
}
protected override bool OnClick(ClickEvent e)
{
sampleClick.Play();

View File

@ -113,13 +113,13 @@ namespace osu.Game.Graphics.UserInterface
private const float transition_length = 500;
private void fadeActive()
protected void FadeHovered()
{
Bar.FadeIn(transition_length, Easing.OutQuint);
Text.FadeColour(Color4.White, transition_length, Easing.OutQuint);
}
private void fadeInactive()
protected void FadeUnhovered()
{
Bar.FadeOut(transition_length, Easing.OutQuint);
Text.FadeColour(AccentColour, transition_length, Easing.OutQuint);
@ -128,14 +128,14 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
if (!Active.Value)
fadeActive();
FadeHovered();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (!Active.Value)
fadeInactive();
FadeUnhovered();
}
[BackgroundDependencyLoader]
@ -172,13 +172,19 @@ namespace osu.Game.Graphics.UserInterface
},
new HoverClickSounds()
};
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
}
protected override void OnActivated() => fadeActive();
protected override void OnActivated()
{
Text.Font = Text.Font.With(weight: FontWeight.Bold);
FadeHovered();
}
protected override void OnDeactivated() => fadeInactive();
protected override void OnDeactivated()
{
Text.Font = Text.Font.With(weight: FontWeight.Medium);
FadeUnhovered();
}
}
}
}

View File

@ -1,102 +0,0 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public abstract class ScreenTitle : CompositeDrawable, IHasAccentColour
{
public const float ICON_WIDTH = ICON_SIZE + spacing;
public const float ICON_SIZE = 25;
private const float spacing = 6;
private const int text_offset = 2;
private SpriteIcon iconSprite;
private readonly OsuSpriteText titleText, pageText;
protected IconUsage Icon
{
set
{
if (iconSprite == null)
throw new InvalidOperationException($"Cannot use {nameof(Icon)} with a custom {nameof(CreateIcon)} function.");
iconSprite.Icon = value;
}
}
protected string Title
{
set => titleText.Text = value;
}
protected string Section
{
set => pageText.Text = value;
}
public Color4 AccentColour
{
get => pageText.Colour;
set => pageText.Colour = value;
}
protected virtual Drawable CreateIcon() => iconSprite = new SpriteIcon
{
Size = new Vector2(ICON_SIZE),
};
protected ScreenTitle()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(spacing, 0),
Direction = FillDirection.Horizontal,
Children = new[]
{
CreateIcon().With(t =>
{
t.Anchor = Anchor.Centre;
t.Origin = Anchor.Centre;
}),
titleText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold),
Margin = new MarginPadding { Bottom = text_offset }
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(4),
Colour = Color4.Gray,
},
pageText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20),
Margin = new MarginPadding { Bottom = text_offset }
}
}
},
};
}
}
}

View File

@ -1,40 +0,0 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A custom icon class for use with <see cref="ScreenTitle.CreateIcon()"/> based off a texture resource.
/// </summary>
public class ScreenTitleTextureIcon : CompositeDrawable
{
private readonly string textureName;
public ScreenTitleTextureIcon(string textureName)
{
this.textureName = textureName;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Size = new Vector2(ScreenTitle.ICON_SIZE);
InternalChild = new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(textureName),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fit
};
}
}
}

View File

@ -43,13 +43,13 @@ namespace osu.Game.IO.Serialization.Converters
var list = new List<T>();
var obj = JObject.Load(reader);
var lookupTable = serializer.Deserialize<List<string>>(obj["lookup_table"].CreateReader());
var lookupTable = serializer.Deserialize<List<string>>(obj["$lookup_table"].CreateReader());
foreach (var tok in obj["items"])
foreach (var tok in obj["$items"])
{
var itemReader = tok.CreateReader();
var typeName = lookupTable[(int)tok["type"]];
var typeName = lookupTable[(int)tok["$type"]];
var instance = (T)Activator.CreateInstance(Type.GetType(typeName));
serializer.Populate(itemReader, instance);
@ -61,7 +61,7 @@ namespace osu.Game.IO.Serialization.Converters
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var list = (List<T>)value;
var list = (IEnumerable<T>)value;
var lookupTable = new List<string>();
var objects = new List<JObject>();
@ -84,16 +84,16 @@ namespace osu.Game.IO.Serialization.Converters
}
var itemObject = JObject.FromObject(item, serializer);
itemObject.AddFirst(new JProperty("type", typeId));
itemObject.AddFirst(new JProperty("$type", typeId));
objects.Add(itemObject);
}
writer.WriteStartObject();
writer.WritePropertyName("lookup_table");
writer.WritePropertyName("$lookup_table");
serializer.Serialize(writer, lookupTable);
writer.WritePropertyName("items");
writer.WritePropertyName("$items");
serializer.Serialize(writer, objects);
writer.WriteEndObject();

View File

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

View File

@ -62,6 +62,14 @@ namespace osu.Game.Input.Bindings
store.KeyBindingChanged -= ReloadMappings;
}
protected override void ReloadMappings() => KeyBindings = store.Query(ruleset?.ID, variant).ToList();
protected override void ReloadMappings()
{
if (ruleset != null && !ruleset.ID.HasValue)
// if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
// fallback to defaults instead.
KeyBindings = DefaultKeyBindings;
else
KeyBindings = store.Query(ruleset?.ID, variant).ToList();
}
}
}

View File

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

View File

@ -16,20 +16,35 @@ namespace osu.Game.Online.API
{
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest<T>(Uri);
public T Result => ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
protected APIRequest()
{
base.Success += onSuccess;
}
private void onSuccess() => Success?.Invoke(Result);
public T Result { get; private set; }
/// <summary>
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary>
public new event APISuccessHandler<T> Success;
protected override void PostProcess()
{
base.PostProcess();
Result = ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
}
internal void TriggerSuccess(T result)
{
if (Result != null)
throw new InvalidOperationException("Attempted to trigger success more than once");
Result = result;
TriggerSuccess();
}
internal override void TriggerSuccess()
{
base.TriggerSuccess();
Success?.Invoke(Result);
}
}
/// <summary>
@ -83,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();
@ -92,14 +107,28 @@ namespace osu.Game.Online.API
if (checkAndScheduleFailure())
return;
PostProcess();
API.Schedule(delegate
{
if (cancelled) return;
Success?.Invoke();
TriggerSuccess();
});
}
/// <summary>
/// Perform any post-processing actions after a successful request.
/// </summary>
protected virtual void PostProcess()
{
}
internal virtual void TriggerSuccess()
{
Success?.Invoke();
}
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@ -30,6 +31,11 @@ namespace osu.Game.Online.API
private readonly List<IOnlineComponent> components = new List<IOnlineComponent>();
/// <summary>
/// Provide handling logic for an arbitrary API request.
/// </summary>
public Action<APIRequest> HandleRequest;
public APIState State
{
get => state;
@ -55,11 +61,16 @@ namespace osu.Game.Online.API
public virtual void Queue(APIRequest request)
{
HandleRequest?.Invoke(request);
}
public void Perform(APIRequest request) { }
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
public Task PerformAsync(APIRequest request) => Task.CompletedTask;
public Task PerformAsync(APIRequest request)
{
HandleRequest?.Invoke(request);
return Task.CompletedTask;
}
public void Register(IOnlineComponent component)
{

View File

@ -1,30 +1,40 @@
// 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.ComponentModel;
using osu.Framework.IO.Network;
using osu.Game.Overlays;
using osu.Game.Overlays.Direct;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse>
{
public SearchCategory SearchCategory { get; set; }
public SortCriteria SortCriteria { get; set; }
public SortDirection SortDirection { get; set; }
public SearchGenre Genre { get; set; }
public SearchLanguage Language { get; set; }
private readonly string query;
private readonly RulesetInfo ruleset;
private readonly BeatmapSearchCategory searchCategory;
private readonly DirectSortCriteria sortCriteria;
private readonly SortDirection direction;
private string directionString => direction == SortDirection.Descending ? @"desc" : @"asc";
public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, BeatmapSearchCategory searchCategory = BeatmapSearchCategory.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending)
private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc";
public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset)
{
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
this.ruleset = ruleset;
this.searchCategory = searchCategory;
this.sortCriteria = sortCriteria;
this.direction = direction;
SearchCategory = SearchCategory.Any;
SortCriteria = SortCriteria.Ranked;
SortDirection = SortDirection.Descending;
Genre = SearchGenre.Any;
Language = SearchLanguage.Any;
}
protected override WebRequest CreateWebRequest()
@ -35,31 +45,19 @@ namespace osu.Game.Online.API.Requests
if (ruleset.ID.HasValue)
req.AddParameter("m", ruleset.ID.Value.ToString());
req.AddParameter("s", searchCategory.ToString().ToLowerInvariant());
req.AddParameter("sort", $"{sortCriteria.ToString().ToLowerInvariant()}_{directionString}");
req.AddParameter("s", SearchCategory.ToString().ToLowerInvariant());
if (Genre != SearchGenre.Any)
req.AddParameter("g", ((int)Genre).ToString());
if (Language != SearchLanguage.Any)
req.AddParameter("l", ((int)Language).ToString());
req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
return req;
}
protected override string Target => @"beatmapsets/search";
}
public enum BeatmapSearchCategory
{
Any,
[Description("Has Leaderboard")]
Leaderboard,
Ranked,
Qualified,
Loved,
Favourites,
[Description("Pending & WIP")]
Pending,
Graveyard,
[Description("My Maps")]
Mine,
}
}

View File

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

View File

@ -18,7 +18,7 @@ namespace osu.Game.Online.Chat
/// <summary>
/// Manages everything channel related
/// </summary>
public class ChannelManager : PollingComponent
public class ChannelManager : PollingComponent, IChannelPostTarget
{
/// <summary>
/// The channels the player joins on startup
@ -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>
@ -204,6 +210,10 @@ namespace osu.Game.Online.Chat
switch (command)
{
case "np":
AddInternal(new NowPlayingCommand());
break;
case "me":
if (string.IsNullOrWhiteSpace(content))
{
@ -234,7 +244,7 @@ namespace osu.Game.Online.Chat
break;
case "help":
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel]"));
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /np"));
break;
default:
@ -371,12 +381,6 @@ namespace osu.Game.Online.Chat
if (CurrentChannel.Value == null)
CurrentChannel.Value = channel;
if (!channel.MessagesLoaded)
{
// let's fetch a small number of messages to bring us up-to-date with the backlog.
fetchInitalMessages(channel);
}
return channel;
}

View File

@ -0,0 +1,19 @@
// 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.Allocation;
namespace osu.Game.Online.Chat
{
[Cached(typeof(IChannelPostTarget))]
public interface IChannelPostTarget
{
/// <summary>
/// Posts a message to the currently opened channel.
/// </summary>
/// <param name="text">The message text that is going to be posted</param>
/// <param name="isAction">Is true if the message is an action, e.g.: user is currently eating </param>
/// <param name="target">An optional target channel. If null, <see cref="ChannelManager.CurrentChannel"/> will be used.</param>
void PostMessage(string text, bool isAction = false, Channel target = null);
}
}

View File

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

View File

@ -0,0 +1,55 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class NowPlayingCommand : Component
{
[Resolved]
private IChannelPostTarget channelManager { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private Bindable<WorkingBeatmap> currentBeatmap { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
string verb;
BeatmapInfo beatmap;
switch (api.Activity.Value)
{
case UserActivity.SoloGame solo:
verb = "playing";
beatmap = solo.Beatmap;
break;
case UserActivity.Editing edit:
verb = "editing";
beatmap = edit.Beatmap;
break;
default:
verb = "listening to";
beatmap = currentBeatmap.Value.BeatmapInfo;
break;
}
var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[https://osu.ppy.sh/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString();
channelManager.PostMessage($"is {verb} {beatmapString}", true);
Expire();
}
}
}

View File

@ -7,23 +7,31 @@ using osu.Game.Scoring;
namespace osu.Game.Online.Leaderboards
{
public class UpdateableRank : ModelBackedDrawable<ScoreRank>
public class UpdateableRank : ModelBackedDrawable<ScoreRank?>
{
public ScoreRank Rank
public ScoreRank? Rank
{
get => Model;
set => Model = value;
}
public UpdateableRank(ScoreRank rank)
public UpdateableRank(ScoreRank? rank)
{
Rank = rank;
}
protected override Drawable CreateDrawable(ScoreRank rank) => new DrawableRank(rank)
protected override Drawable CreateDrawable(ScoreRank? rank)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
if (rank.HasValue)
{
return new DrawableRank(rank.Value)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
return null;
}
}
}

View File

@ -3,7 +3,7 @@
using System;
using System.Threading.Tasks;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
namespace osu.Game.Online
@ -11,7 +11,7 @@ namespace osu.Game.Online
/// <summary>
/// A component which requires a constant polling process.
/// </summary>
public abstract class PollingComponent : Component
public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages.
{
private double? lastTimePolled;

View File

@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Development;
@ -65,9 +66,9 @@ namespace osu.Game
private NowPlayingOverlay nowPlaying;
private DirectOverlay direct;
private BeatmapListingOverlay beatmapListing;
private SocialOverlay social;
private DashboardOverlay dashboard;
private UserProfileOverlay userProfile;
@ -97,6 +98,7 @@ namespace osu.Game
private MainMenu menuScreen;
[CanBeNull]
private IntroScreen introScreen;
private Bindable<int> configRuleset;
@ -315,8 +317,15 @@ namespace osu.Game
/// The user should have already requested this interactively.
/// </summary>
/// <param name="beatmap">The beatmap to select.</param>
public void PresentBeatmap(BeatmapSetInfo beatmap)
/// <param name="difficultyCriteria">
/// Optional predicate used to try and find a difficulty to select.
/// If omitted, this will try to present the first beatmap from the current ruleset.
/// In case of failure the first difficulty of the set will be presented, ignoring the predicate.
/// </param>
public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate<BeatmapInfo> difficultyCriteria = null)
{
difficultyCriteria ??= b => b.Ruleset.Equals(Ruleset.Value);
var databasedSet = beatmap.OnlineBeatmapSetID != null
? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID)
: BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash);
@ -334,13 +343,13 @@ namespace osu.Game
menuScreen.LoadToSolo();
// we might even already be at the song
if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash)
if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && difficultyCriteria(Beatmap.Value.BeatmapInfo))
{
return;
}
// Use first beatmap available for current ruleset, else switch ruleset.
var first = databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First();
// Find first beatmap that matches our predicate.
var first = databasedSet.Beatmaps.Find(difficultyCriteria) ?? databasedSet.Beatmaps.First();
Ruleset.Value = first.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first);
@ -602,9 +611,9 @@ namespace osu.Game
loadComponentSingleFile(screenshotManager, Add);
//overlay elements
loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true);
loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true);
// overlay elements
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
@ -663,7 +672,7 @@ namespace osu.Game
}
// ensure only one of these overlays are open at once.
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct, changelogOverlay, rankingsOverlay };
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, beatmapListing, changelogOverlay, rankingsOverlay };
foreach (var overlay in singleDisplayOverlays)
{
@ -774,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)
@ -835,7 +844,7 @@ namespace osu.Game
return true;
case GlobalAction.ToggleSocial:
social.ToggleVisibility();
dashboard.ToggleVisibility();
return true;
case GlobalAction.ResetInputSettings:
@ -858,7 +867,7 @@ namespace osu.Game
return true;
case GlobalAction.ToggleDirect:
direct.ToggleVisibility();
beatmapListing.ToggleVisibility();
return true;
case GlobalAction.ToggleGameplayMouseButtons:
@ -907,10 +916,7 @@ namespace osu.Game
if (ScreenStack.CurrentScreen is Loader)
return false;
if (introScreen == null)
return true;
if (!introScreen.DidLoadMenu || !(ScreenStack.CurrentScreen is IntroScreen))
if (introScreen?.DidLoadMenu == true && !(ScreenStack.CurrentScreen is IntroScreen))
{
Scheduler.Add(introScreen.MakeCurrent);
return true;

View File

@ -168,7 +168,7 @@ namespace osu.Game
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
dependencies.Cache(RulesetStore = new RulesetStore(contextFactory));
dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage));
dependencies.Cache(FileStore = new FileStore(contextFactory, Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()

View File

@ -5,7 +5,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online;
namespace osu.Game.Overlays.Direct
namespace osu.Game.Overlays
{
public abstract class BeatmapDownloadTrackingComposite : DownloadTrackingComposite<BeatmapSetInfo, BeatmapManager>
{

View File

@ -0,0 +1,164 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingFilterControl : CompositeDrawable
{
public Action<List<BeatmapSetInfo>> SearchFinished;
public Action SearchStarted;
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
private readonly BeatmapListingSearchControl searchControl;
private readonly BeatmapListingSortTabControl sortControl;
private readonly Box sortControlBackground;
private SearchBeatmapSetsRequest getSetsRequest;
public BeatmapListingFilterControl()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 3,
Offset = new Vector2(0f, 1f),
},
Child = searchControl = new BeatmapListingSearchControl(),
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = 40,
Children = new Drawable[]
{
sortControlBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
sortControl = new BeatmapListingSortTabControl
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 20 }
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
sortControlBackground.Colour = colourProvider.Background5;
}
protected override void LoadComplete()
{
base.LoadComplete();
var sortCriteria = sortControl.Current;
var sortDirection = sortControl.SortDirection;
searchControl.Query.BindValueChanged(query =>
{
sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? SortCriteria.Ranked : SortCriteria.Relevance;
sortDirection.Value = SortDirection.Descending;
queueUpdateSearch(true);
});
searchControl.Ruleset.BindValueChanged(_ => queueUpdateSearch());
searchControl.Category.BindValueChanged(_ => queueUpdateSearch());
searchControl.Genre.BindValueChanged(_ => queueUpdateSearch());
searchControl.Language.BindValueChanged(_ => queueUpdateSearch());
sortCriteria.BindValueChanged(_ => queueUpdateSearch());
sortDirection.BindValueChanged(_ => queueUpdateSearch());
}
private ScheduledDelegate queryChangedDebounce;
private void queueUpdateSearch(bool queryTextChanged = false)
{
SearchStarted?.Invoke();
getSetsRequest?.Cancel();
queryChangedDebounce?.Cancel();
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
}
private void updateSearch()
{
getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value)
{
SearchCategory = searchControl.Category.Value,
SortCriteria = sortControl.Current.Value,
SortDirection = sortControl.SortDirection.Value,
Genre = searchControl.Genre.Value,
Language = searchControl.Language.Value
};
getSetsRequest.Success += response => Schedule(() => onSearchFinished(response));
api.Queue(getSetsRequest);
}
private void onSearchFinished(SearchBeatmapSetsResponse response)
{
var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First();
SearchFinished?.Invoke(beatmaps);
}
protected override void Dispose(bool isDisposing)
{
getSetsRequest?.Cancel();
queryChangedDebounce?.Cancel();
base.Dispose(isDisposing);
}
public void TakeFocus() => searchControl.TakeFocus();
}
}

View File

@ -1,24 +1,19 @@
// 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.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingHeader : OverlayHeader
{
protected override ScreenTitle CreateTitle() => new BeatmapListingTitle();
protected override OverlayTitle CreateTitle() => new BeatmapListingTitle();
private class BeatmapListingTitle : ScreenTitle
private class BeatmapListingTitle : OverlayTitle
{
public BeatmapListingTitle()
{
Title = @"beatmap";
Section = @"listing";
Title = "beatmap listing";
IconTexture = "Icons/changelog";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}

View File

@ -5,8 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osuTK;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.Drawables;
@ -14,16 +12,21 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingSearchSection : CompositeDrawable
public class BeatmapListingSearchControl : CompositeDrawable
{
public Bindable<string> Query => textBox.Current;
public Bindable<RulesetInfo> Ruleset => modeFilter.Current;
public Bindable<BeatmapSearchCategory> Category => categoryFilter.Current;
public Bindable<SearchCategory> Category => categoryFilter.Current;
public Bindable<SearchGenre> Genre => genreFilter.Current;
public Bindable<SearchLanguage> Language => languageFilter.Current;
public BeatmapSetInfo BeatmapSet
{
@ -42,12 +45,14 @@ namespace osu.Game.Overlays.BeatmapListing
private readonly BeatmapSearchTextBox textBox;
private readonly BeatmapSearchRulesetFilterRow modeFilter;
private readonly BeatmapSearchFilterRow<BeatmapSearchCategory> categoryFilter;
private readonly BeatmapSearchFilterRow<SearchCategory> categoryFilter;
private readonly BeatmapSearchFilterRow<SearchGenre> genreFilter;
private readonly BeatmapSearchFilterRow<SearchLanguage> languageFilter;
private readonly Box background;
private readonly UpdateableBeatmapSetCover beatmapCover;
public BeatmapListingSearchSection()
public BeatmapListingSearchControl()
{
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
@ -97,7 +102,9 @@ namespace osu.Game.Overlays.BeatmapListing
Children = new Drawable[]
{
modeFilter = new BeatmapSearchRulesetFilterRow(),
categoryFilter = new BeatmapSearchFilterRow<BeatmapSearchCategory>(@"Categories"),
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(@"Categories"),
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(@"Genre"),
languageFilter = new BeatmapSearchFilterRow<SearchLanguage>(@"Language"),
}
}
}
@ -105,7 +112,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
});
Category.Value = BeatmapSearchCategory.Leaderboard;
categoryFilter.Current.Value = SearchCategory.Leaderboard;
}
[BackgroundDependencyLoader]
@ -114,6 +121,8 @@ namespace osu.Game.Overlays.BeatmapListing
background.Colour = colourProvider.Dark6;
}
public void TakeFocus() => textBox.TakeFocus();
private class BeatmapSearchTextBox : SearchTextBox
{
protected override Color4 SelectionColour => Color4.Gray;

View File

@ -8,17 +8,16 @@ using osu.Framework.Graphics;
using osuTK.Graphics;
using osuTK;
using osu.Framework.Input.Events;
using osu.Game.Overlays.Direct;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingSortTabControl : OverlaySortTabControl<DirectSortCriteria>
public class BeatmapListingSortTabControl : OverlaySortTabControl<SortCriteria>
{
public readonly Bindable<SortDirection> SortDirection = new Bindable<SortDirection>(Overlays.SortDirection.Descending);
public BeatmapListingSortTabControl()
{
Current.Value = DirectSortCriteria.Ranked;
Current.Value = SortCriteria.Ranked;
}
protected override SortTabControl CreateControl() => new BeatmapSortTabControl
@ -30,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
public readonly Bindable<SortDirection> SortDirection = new Bindable<SortDirection>();
protected override TabItem<DirectSortCriteria> CreateTabItem(DirectSortCriteria value) => new BeatmapSortTabItem(value)
protected override TabItem<SortCriteria> CreateTabItem(SortCriteria value) => new BeatmapSortTabItem(value)
{
SortDirection = { BindTarget = SortDirection }
};
@ -40,12 +39,12 @@ namespace osu.Game.Overlays.BeatmapListing
{
public readonly Bindable<SortDirection> SortDirection = new Bindable<SortDirection>();
public BeatmapSortTabItem(DirectSortCriteria value)
public BeatmapSortTabItem(SortCriteria value)
: base(value)
{
}
protected override TabButton CreateTabButton(DirectSortCriteria value) => new BeatmapTabButton(value)
protected override TabButton CreateTabButton(SortCriteria value) => new BeatmapTabButton(value)
{
Active = { BindTarget = Active },
SortDirection = { BindTarget = SortDirection }
@ -67,7 +66,7 @@ namespace osu.Game.Overlays.BeatmapListing
private readonly SpriteIcon icon;
public BeatmapTabButton(DirectSortCriteria value)
public BeatmapTabButton(SortCriteria value)
: base(value)
{
Add(icon = new SpriteIcon

View File

@ -15,6 +15,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
using Humanizer;
using osu.Game.Utils;
namespace osu.Game.Overlays.BeatmapListing
{
@ -53,8 +55,8 @@ namespace osu.Game.Overlays.BeatmapListing
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 10),
Text = headerName.ToUpper()
Font = OsuFont.GetFont(size: 13),
Text = headerName.Titleize()
},
CreateFilter().With(f =>
{
@ -81,7 +83,7 @@ namespace osu.Game.Overlays.BeatmapListing
if (typeof(T).IsEnum)
{
foreach (var val in (T[])Enum.GetValues(typeof(T)))
foreach (var val in OrderAttributeUtils.GetValuesInOrder<T>())
AddItem(val);
}
}

View File

@ -1,32 +0,0 @@
// 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.UserInterface;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapSearchSmallFilterRow<T> : BeatmapSearchFilterRow<T>
{
public BeatmapSearchSmallFilterRow(string headerName)
: base(headerName)
{
}
protected override BeatmapSearchFilter CreateFilter() => new SmallBeatmapSearchFilter();
private class SmallBeatmapSearchFilter : BeatmapSearchFilter
{
protected override TabItem<T> CreateTabItem(T value) => new SmallTabItem(value);
private class SmallTabItem : FilterTabItem
{
public SmallTabItem(T value)
: base(value)
{
}
protected override float TextSize => 10;
}
}
}
}

View File

@ -26,9 +26,9 @@ using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
namespace osu.Game.Overlays.BeatmapListing.Panels
{
public abstract class DirectPanel : OsuClickableContainer, IHasContextMenu
public abstract class BeatmapPanel : OsuClickableContainer, IHasContextMenu
{
public readonly BeatmapSetInfo SetInfo;
@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Direct
protected Action ViewBeatmap;
protected DirectPanel(BeatmapSetInfo setInfo)
protected BeatmapPanel(BeatmapSetInfo setInfo)
{
Debug.Assert(setInfo.OnlineBeatmapSetID != null);
@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Direct
if (SetInfo.Beatmaps.Count > maximum_difficulty_icons)
{
foreach (var ruleset in SetInfo.Beatmaps.Select(b => b.Ruleset).Distinct())
icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is DirectListPanel ? Color4.White : colours.Gray5));
icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is ListBeatmapPanel ? Color4.White : colours.Gray5));
}
else
{

View File

@ -0,0 +1,99 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class BeatmapPanelDownloadButton : BeatmapDownloadTrackingComposite
{
protected bool DownloadEnabled => button.Enabled.Value;
/// <summary>
/// Currently selected beatmap. Used to present the correct difficulty after completing a download.
/// </summary>
public readonly IBindable<BeatmapInfo> SelectedBeatmap = new Bindable<BeatmapInfo>();
private readonly ShakeContainer shakeContainer;
private readonly DownloadButton button;
private Bindable<bool> noVideoSetting;
public BeatmapPanelDownloadButton(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{
InternalChild = shakeContainer = new ShakeContainer
{
RelativeSizeAxes = Axes.Both,
Child = button = new DownloadButton
{
RelativeSizeAxes = Axes.Both,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
button.State.BindTo(State);
FinishTransforms(true);
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig)
{
noVideoSetting = osuConfig.GetBindable<bool>(OsuSetting.PreferNoVideo);
button.Action = () =>
{
switch (State.Value)
{
case DownloadState.Downloading:
case DownloadState.Downloaded:
shakeContainer.Shake();
break;
case DownloadState.LocallyAvailable:
Predicate<BeatmapInfo> findPredicate = null;
if (SelectedBeatmap.Value != null)
findPredicate = b => b.OnlineBeatmapID == SelectedBeatmap.Value.OnlineBeatmapID;
game?.PresentBeatmap(BeatmapSet.Value, findPredicate);
break;
default:
beatmaps.Download(BeatmapSet.Value, noVideoSetting.Value);
break;
}
};
State.BindValueChanged(state =>
{
switch (state.NewValue)
{
case DownloadState.LocallyAvailable:
button.Enabled.Value = true;
button.TooltipText = string.Empty;
break;
default:
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
{
button.Enabled.Value = false;
button.TooltipText = "this beatmap is currently not available for download.";
}
break;
}
}, true);
}
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class DownloadProgressBar : BeatmapDownloadTrackingComposite
{

View File

@ -1,25 +1,25 @@
// 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 osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class DirectGridPanel : DirectPanel
public class GridBeatmapPanel : BeatmapPanel
{
private const float horizontal_padding = 10;
private const float vertical_padding = 5;
@ -31,11 +31,11 @@ namespace osu.Game.Overlays.Direct
protected override PlayButton PlayButton => playButton;
protected override Box PreviewBar => progressBar;
public DirectGridPanel(BeatmapSetInfo beatmap)
public GridBeatmapPanel(BeatmapSetInfo beatmap)
: 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()
@ -156,7 +156,7 @@ namespace osu.Game.Overlays.Direct
},
},
},
new PanelDownloadButton(SetInfo)
new BeatmapPanelDownloadButton(SetInfo)
{
Size = new Vector2(50, 30),
Margin = new MarginPadding(horizontal_padding),

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class IconPill : CircularContainer
{

View File

@ -1,25 +1,25 @@
// 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 osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Colour;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class DirectListPanel : DirectPanel
public class ListBeatmapPanel : BeatmapPanel
{
private const float transition_duration = 120;
private const float horizontal_padding = 10;
@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Direct
private const float height = 70;
private FillFlowContainer statusContainer;
protected PanelDownloadButton DownloadButton;
protected BeatmapPanelDownloadButton DownloadButton;
private PlayButton playButton;
private Box progressBar;
@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Direct
protected override PlayButton PlayButton => playButton;
protected override Box PreviewBar => progressBar;
public DirectListPanel(BeatmapSetInfo beatmap)
public ListBeatmapPanel(BeatmapSetInfo beatmap)
: base(beatmap)
{
RelativeSizeAxes = Axes.X;
@ -151,7 +151,7 @@ namespace osu.Game.Overlays.Direct
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Child = DownloadButton = new PanelDownloadButton(SetInfo)
Child = DownloadButton = new BeatmapPanelDownloadButton(SetInfo)
{
Size = new Vector2(height - vertical_padding * 3),
Margin = new MarginPadding { Left = vertical_padding * 2, Right = vertical_padding },

View File

@ -14,7 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class PlayButton : Container
{

View File

@ -0,0 +1,26 @@
// 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.ComponentModel;
namespace osu.Game.Overlays.BeatmapListing
{
public enum SearchCategory
{
Any,
[Description("Has Leaderboard")]
Leaderboard,
Ranked,
Qualified,
Loved,
Favourites,
[Description("Pending & WIP")]
Pending,
Graveyard,
[Description("My Maps")]
Mine,
}
}

View File

@ -0,0 +1,25 @@
// 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.ComponentModel;
namespace osu.Game.Overlays.BeatmapListing
{
public enum SearchGenre
{
Any = 0,
Unspecified = 1,
[Description("Video Game")]
VideoGame = 2,
Anime = 3,
Rock = 4,
Pop = 5,
Other = 6,
Novelty = 7,
[Description("Hip Hop")]
HipHop = 9,
Electronic = 10
}
}

View File

@ -0,0 +1,47 @@
// 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.Game.Utils;
namespace osu.Game.Overlays.BeatmapListing
{
[HasOrderedElements]
public enum SearchLanguage
{
[Order(0)]
Any,
[Order(11)]
Other,
[Order(1)]
English,
[Order(6)]
Japanese,
[Order(2)]
Chinese,
[Order(10)]
Instrumental,
[Order(7)]
Korean,
[Order(3)]
French,
[Order(4)]
German,
[Order(9)]
Swedish,
[Order(8)]
Spanish,
[Order(5)]
Italian
}
}

View File

@ -0,0 +1,17 @@
// 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.
namespace osu.Game.Overlays.BeatmapListing
{
public enum SortCriteria
{
Title,
Artist,
Difficulty,
Ranked,
Rating,
Plays,
Favourites,
Relevance
}
}

View File

@ -1,27 +1,24 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Threading;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
using osu.Game.Overlays.BeatmapListing.Panels;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
@ -30,20 +27,17 @@ namespace osu.Game.Overlays
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
private SearchBeatmapSetsRequest getSetsRequest;
private Drawable currentContent;
private BeatmapListingSearchSection searchSection;
private BeatmapListingSortTabControl sortControl;
private LoadingLayer loadingLayer;
private Container panelTarget;
public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue)
{
}
private BeatmapListingFilterControl filterControl;
[BackgroundDependencyLoader]
private void load()
{
@ -54,7 +48,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
},
new BasicScrollContainer
new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
@ -63,27 +57,13 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new FillFlowContainer
new BeatmapListingHeader(),
filterControl = new BeatmapListingFilterControl
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 3,
Offset = new Vector2(0f, 1f),
},
Children = new Drawable[]
{
new BeatmapListingHeader(),
searchSection = new BeatmapListingSearchSection(),
}
SearchStarted = onSearchStarted,
SearchFinished = onSearchFinished,
},
new Container
{
@ -96,154 +76,70 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background4,
},
new FillFlowContainer
panelTarget = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
Height = 40,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background5
},
sortControl = new BeatmapListingSortTabControl
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 20 }
}
}
},
new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[]
{
panelTarget = new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
},
loadingLayer = new LoadingLayer(panelTarget),
}
},
}
}
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Horizontal = 20 }
},
loadingLayer = new LoadingLayer(panelTarget)
}
}
},
}
}
}
};
}
protected override void LoadComplete()
protected override void OnFocus(FocusEvent e)
{
base.LoadComplete();
base.OnFocus(e);
var sortCriteria = sortControl.Current;
var sortDirection = sortControl.SortDirection;
searchSection.Query.BindValueChanged(query =>
{
sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance;
sortDirection.Value = SortDirection.Descending;
queueUpdateSearch(true);
});
searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch());
searchSection.Category.BindValueChanged(_ => queueUpdateSearch());
sortCriteria.BindValueChanged(_ => queueUpdateSearch());
sortDirection.BindValueChanged(_ => queueUpdateSearch());
filterControl.TakeFocus();
}
private ScheduledDelegate queryChangedDebounce;
private CancellationTokenSource cancellationToken;
private LoadingLayer loadingLayer;
private Container panelTarget;
private void queueUpdateSearch(bool queryTextChanged = false)
private void onSearchStarted()
{
getSetsRequest?.Cancel();
queryChangedDebounce?.Cancel();
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
}
private void updateSearch()
{
if (!IsLoaded)
return;
if (State.Value == Visibility.Hidden)
return;
if (API == null)
return;
cancellationToken?.Cancel();
previewTrackManager.StopAnyPlaying(this);
loadingLayer.Show();
getSetsRequest = new SearchBeatmapSetsRequest(
searchSection.Query.Value,
searchSection.Ruleset.Value,
searchSection.Category.Value,
sortControl.Current.Value,
sortControl.SortDirection.Value);
getSetsRequest.Success += response => Schedule(() => recreatePanels(response));
API.Queue(getSetsRequest);
if (panelTarget.Any())
loadingLayer.Show();
}
private void recreatePanels(SearchBeatmapSetsResponse response)
private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
{
if (response.Total == 0)
if (!beatmaps.Any())
{
searchSection.BeatmapSet = null;
LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder);
LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
return;
}
var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
var newPanels = new FillFlowContainer<DirectPanel>
var newPanels = new FillFlowContainer<BeatmapPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Alpha = 0,
Margin = new MarginPadding { Vertical = 15 },
ChildrenEnumerable = beatmaps.Select<BeatmapSetInfo, DirectPanel>(b => new DirectGridPanel(b)
ChildrenEnumerable = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
})
};
LoadComponentAsync(newPanels, loaded =>
{
addContentToPlaceholder(loaded);
searchSection.BeatmapSet = beatmaps.First();
});
LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
}
private void addContentToPlaceholder(Drawable content)
{
loadingLayer.Hide();
Drawable lastContent = currentContent;
var lastContent = currentContent;
if (lastContent != null)
{
@ -262,9 +158,7 @@ namespace osu.Game.Overlays
protected override void Dispose(bool isDisposing)
{
getSetsRequest?.Cancel();
queryChangedDebounce?.Cancel();
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}

View File

@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapSet
@ -14,22 +13,20 @@ namespace osu.Game.Overlays.BeatmapSet
public BeatmapRulesetSelector RulesetSelector { get; private set; }
protected override ScreenTitle CreateTitle() => new BeatmapHeaderTitle();
protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle();
protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector
{
Current = Ruleset
};
private class BeatmapHeaderTitle : ScreenTitle
private class BeatmapHeaderTitle : OverlayTitle
{
public BeatmapHeaderTitle()
{
Title = @"beatmap";
Section = @"info";
Title = "beatmap info";
IconTexture = "Icons/changelog";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}

View File

@ -13,7 +13,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Overlays.Direct;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;

View File

@ -11,7 +11,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Direct;
using osu.Game.Overlays.BeatmapListing.Panels;
using osuTK;
namespace osu.Game.Overlays.BeatmapSet.Buttons

View File

@ -15,8 +15,8 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@ -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;
@ -274,10 +274,11 @@ namespace osu.Game.Overlays.BeatmapSet
{
case DownloadState.LocallyAvailable:
// temporary for UX until new design is implemented.
downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value)
downloadButtonsContainer.Child = new BeatmapPanelDownloadButton(BeatmapSet.Value)
{
Width = 50,
RelativeSizeAxes = Axes.Y
RelativeSizeAxes = Axes.Y,
SelectedBeatmap = { BindTarget = Picker.Beatmap }
};
break;

View File

@ -39,7 +39,7 @@ namespace osu.Game.Overlays
public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue)
{
OsuScrollContainer scroll;
OverlayScrollContainer scroll;
Info info;
CommentsSection comments;
@ -49,7 +49,7 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both
},
scroll = new OsuScrollContainer
scroll = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
@ -16,6 +17,13 @@ namespace osu.Game.Overlays
public OverlayHeaderBreadcrumbControl()
{
RelativeSizeAxes = Axes.X;
Height = 47;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
AccentColour = colourProvider.Light2;
}
protected override TabItem<string> CreateTabItem(string value) => new ControlTabItem(value);
@ -27,10 +35,18 @@ namespace osu.Game.Overlays
public ControlTabItem(string value)
: base(value)
{
RelativeSizeAxes = Axes.Y;
Text.Font = Text.Font.With(size: 14);
Chevron.Y = 3;
Text.Anchor = Anchor.CentreLeft;
Text.Origin = Anchor.CentreLeft;
Chevron.Y = 1;
Bar.Height = 0;
}
// base OsuTabItem makes font bold on activation, we don't want that here
protected override void OnActivated() => FadeHovered();
protected override void OnDeactivated() => FadeUnhovered();
}
}
}

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Changelog
@ -50,8 +49,6 @@ namespace osu.Game.Overlays.Changelog
streamsBackground.Colour = colourProvider.Background5;
}
private ChangelogHeaderTitle title;
private void showBuild(ValueChangedEvent<APIChangelogBuild> e)
{
if (e.OldValue != null)
@ -63,14 +60,11 @@ namespace osu.Game.Overlays.Changelog
Current.Value = e.NewValue.ToString();
updateCurrentStream();
title.Version = e.NewValue.UpdateStream.DisplayName;
}
else
{
Current.Value = listing_string;
Streams.Current.Value = null;
title.Version = null;
}
}
@ -100,7 +94,7 @@ namespace osu.Game.Overlays.Changelog
}
};
protected override ScreenTitle CreateTitle() => title = new ChangelogHeaderTitle();
protected override OverlayTitle CreateTitle() => new ChangelogHeaderTitle();
public void Populate(List<APIUpdateStream> streams)
{
@ -116,20 +110,13 @@ namespace osu.Game.Overlays.Changelog
Streams.Current.Value = Streams.Items.FirstOrDefault(s => s.Name == Build.Value.UpdateStream.Name);
}
private class ChangelogHeaderTitle : ScreenTitle
private class ChangelogHeaderTitle : OverlayTitle
{
public string Version
{
set => Section = value ?? listing_string;
}
public ChangelogHeaderTitle()
{
Title = "changelog";
Version = null;
IconTexture = "Icons/changelog";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}

View File

@ -1,79 +0,0 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK.Graphics;
namespace osu.Game.Overlays.Changelog
{
public class Comments : CompositeDrawable
{
private readonly APIChangelogBuild build;
public Comments(APIChangelogBuild build)
{
this.build = build;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding
{
Horizontal = 50,
Vertical = 20,
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
LinkFlowContainer text;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreyVioletDarker
},
},
text = new LinkFlowContainer(t =>
{
t.Colour = colours.PinkLighter;
t.Font = OsuFont.Default.With(size: 14);
})
{
Padding = new MarginPadding(20),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
};
text.AddParagraph("Got feedback?", t =>
{
t.Colour = Color4.White;
t.Font = OsuFont.Default.With(italics: true, size: 20);
t.Padding = new MarginPadding { Bottom = 20 };
});
text.AddParagraph("We would love to hear what you think of this update! ");
text.AddIcon(FontAwesome.Regular.GrinHearts);
text.AddParagraph("Please visit the ");
text.AddLink("web version", $"{build.Url}#comments");
text.AddText(" of this changelog to leave any comments.");
}
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background4,
},
new OsuScrollContainer
new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
// 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.
namespace osu.Game.Overlays.Dashboard
{
public class DashboardOverlayHeader : TabControlOverlayHeader<DashboardOverlayTabs>
{
protected override OverlayTitle CreateTitle() => new DashboardTitle();
private class DashboardTitle : OverlayTitle
{
public DashboardTitle()
{
Title = "dashboard";
IconTexture = "Icons/changelog";
}
}
}
public enum DashboardOverlayTabs
{
Friends
}
}

View File

@ -16,7 +16,7 @@ using osuTK;
namespace osu.Game.Overlays.Dashboard.Friends
{
public class FriendDisplay : CompositeDrawable
public class FriendDisplay : OverlayView<List<User>>
{
private List<User> users = new List<User>();
@ -26,34 +26,29 @@ namespace osu.Game.Overlays.Dashboard.Friends
set
{
users = value;
onlineStreamControl.Populate(value);
}
}
[Resolved]
private IAPIProvider api { get; set; }
private GetFriendsRequest request;
private CancellationTokenSource cancellationToken;
private Drawable currentContent;
private readonly FriendOnlineStreamControl onlineStreamControl;
private readonly Box background;
private readonly Box controlBackground;
private readonly UserListToolbar userListToolbar;
private readonly Container itemsPlaceholder;
private readonly LoadingLayer loading;
private FriendOnlineStreamControl onlineStreamControl;
private Box background;
private Box controlBackground;
private UserListToolbar userListToolbar;
private Container itemsPlaceholder;
private LoadingLayer loading;
public FriendDisplay()
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
@ -134,11 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
background.Colour = colourProvider.Background4;
controlBackground.Colour = colourProvider.Background5;
}
@ -152,14 +143,11 @@ namespace osu.Game.Overlays.Dashboard.Friends
userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels());
}
public void Fetch()
{
if (!api.IsLoggedIn)
return;
protected override APIRequest<List<User>> CreateRequest() => new GetFriendsRequest();
request = new GetFriendsRequest();
request.Success += response => Schedule(() => Users = response);
api.Queue(request);
protected override void OnSuccess(List<User> response)
{
Users = response;
}
private void recreatePanels()
@ -258,9 +246,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
protected override void Dispose(bool isDisposing)
{
request?.Cancel();
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}

View File

@ -0,0 +1,150 @@
// 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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Overlays
{
public class DashboardOverlay : FullscreenOverlay
{
private CancellationTokenSource cancellationToken;
private Box background;
private Container content;
private DashboardOverlayHeader header;
private LoadingLayer loading;
private OverlayScrollContainer scrollFlow;
public DashboardOverlay()
: base(OverlayColourScheme.Purple)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
header = new DashboardOverlayHeader
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Depth = -float.MaxValue
},
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
},
loading = new LoadingLayer(content),
};
background.Colour = ColourProvider.Background5;
}
protected override void LoadComplete()
{
base.LoadComplete();
header.Current.BindValueChanged(onTabChanged);
}
private bool displayUpdateRequired = true;
protected override void PopIn()
{
base.PopIn();
// We don't want to create a new display on every call, only when exiting from fully closed state.
if (displayUpdateRequired)
{
header.Current.TriggerChange();
displayUpdateRequired = false;
}
}
protected override void PopOutComplete()
{
base.PopOutComplete();
loadDisplay(Empty());
displayUpdateRequired = true;
}
private void loadDisplay(Drawable display)
{
scrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
if (API.IsLoggedIn)
loading.Hide();
content.Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
private void onTabChanged(ValueChangedEvent<DashboardOverlayTabs> tab)
{
cancellationToken?.Cancel();
loading.Show();
if (!API.IsLoggedIn)
{
loadDisplay(Empty());
return;
}
switch (tab.NewValue)
{
case DashboardOverlayTabs.Friends:
loadDisplay(new FriendDisplay());
break;
default:
throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
}
}
public override void APIStateChanged(IAPIProvider api, APIState state)
{
if (State.Value == Visibility.Hidden)
return;
header.Current.TriggerChange();
}
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}
}

View File

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

View File

@ -1,93 +0,0 @@
// 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.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
{
public class DirectRulesetSelector : RulesetSelector
{
public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput;
public override bool HandlePositionalInput => !Current.Disabled && base.HandlePositionalInput;
public override bool PropagatePositionalInputSubTree => !Current.Disabled && base.PropagatePositionalInputSubTree;
public DirectRulesetSelector()
{
TabContainer.Masking = false;
TabContainer.Spacing = new Vector2(10, 0);
AutoSizeAxes = Axes.Both;
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindDisabledChanged(value => SelectedTab.FadeColour(value ? Color4.DarkGray : Color4.White, 200, Easing.OutQuint), true);
}
protected override TabItem<RulesetInfo> CreateTabItem(RulesetInfo value) => new DirectRulesetTabItem(value);
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
};
private class DirectRulesetTabItem : TabItem<RulesetInfo>
{
private readonly ConstrainedIconContainer iconContainer;
public DirectRulesetTabItem(RulesetInfo value)
: base(value)
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
iconContainer = new ConstrainedIconContainer
{
Icon = value.CreateInstance().CreateIcon(),
Size = new Vector2(32),
},
new HoverClickSounds()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
}
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
protected override void OnActivated() => updateState();
protected override void OnDeactivated() => updateState();
private void updateState() => iconContainer.FadeColour(IsHovered || Active.Value ? Color4.White : Color4.Gray, 120, Easing.InQuad);
}
}
}

View File

@ -1,47 +0,0 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.SearchableList;
using osu.Game.Rulesets;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
{
public class FilterControl : SearchableListFilterControl<DirectSortCriteria, BeatmapSearchCategory>
{
private DirectRulesetSelector rulesetSelector;
protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"384552");
protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked;
protected override BeatmapSearchCategory DefaultCategory => BeatmapSearchCategory.Leaderboard;
protected override Drawable CreateSupplementaryControls() => rulesetSelector = new DirectRulesetSelector();
public Bindable<RulesetInfo> Ruleset => rulesetSelector.Current;
[BackgroundDependencyLoader(true)]
private void load(OsuColour colours, Bindable<RulesetInfo> ruleset)
{
DisplayStyleControl.Dropdown.AccentColour = colours.BlueDark;
rulesetSelector.Current.BindTo(ruleset);
}
}
public enum DirectSortCriteria
{
Title,
Artist,
Difficulty,
Ranked,
Rating,
Plays,
Favourites,
Relevance,
}
}

View File

@ -1,43 +0,0 @@
// 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.ComponentModel;
using osu.Framework.Extensions.Color4Extensions;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.SearchableList;
namespace osu.Game.Overlays.Direct
{
public class Header : SearchableListHeader<DirectTab>
{
protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"252f3a");
protected override DirectTab DefaultTab => DirectTab.Search;
protected override Drawable CreateHeaderText() => new OsuSpriteText { Text = @"osu!direct", Font = OsuFont.GetFont(size: 25) };
protected override IconUsage Icon => OsuIcon.ChevronDownCircle;
public Header()
{
Tabs.Current.Value = DirectTab.NewestMaps;
Tabs.Current.TriggerChange();
}
}
public enum DirectTab
{
Search,
[Description("Newest Maps")]
NewestMaps = DirectSortCriteria.Ranked,
[Description("Top Rated")]
TopRated = DirectSortCriteria.Rating,
[Description("Most Played")]
MostPlayed = DirectSortCriteria.Plays,
}
}

View File

@ -1,75 +0,0 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
namespace osu.Game.Overlays.Direct
{
public class PanelDownloadButton : BeatmapDownloadTrackingComposite
{
protected bool DownloadEnabled => button.Enabled.Value;
private readonly bool noVideo;
private readonly ShakeContainer shakeContainer;
private readonly DownloadButton button;
public PanelDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
: base(beatmapSet)
{
this.noVideo = noVideo;
InternalChild = shakeContainer = new ShakeContainer
{
RelativeSizeAxes = Axes.Both,
Child = button = new DownloadButton
{
RelativeSizeAxes = Axes.Both,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
button.State.BindTo(State);
FinishTransforms(true);
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, BeatmapManager beatmaps)
{
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
{
button.Enabled.Value = false;
button.TooltipText = "this beatmap is currently not available for download.";
return;
}
button.Action = () =>
{
switch (State.Value)
{
case DownloadState.Downloading:
case DownloadState.Downloaded:
shakeContainer.Shake();
break;
case DownloadState.LocallyAvailable:
game?.PresentBeatmap(BeatmapSet.Value);
break;
default:
beatmaps.Download(BeatmapSet.Value, noVideo);
break;
}
};
}
}
}

View File

@ -1,298 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Direct;
using osu.Game.Overlays.SearchableList;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public class DirectOverlay : SearchableListOverlay<DirectTab, DirectSortCriteria, BeatmapSearchCategory>
{
private const float panel_padding = 10f;
[Resolved]
private RulesetStore rulesets { get; set; }
private readonly FillFlowContainer resultCountsContainer;
private readonly OsuSpriteText resultCountsText;
private FillFlowContainer<DirectPanel> panels;
protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"485e74");
protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"465b71");
protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"3f5265");
protected override SearchableListHeader<DirectTab> CreateHeader() => new Header();
protected override SearchableListFilterControl<DirectSortCriteria, BeatmapSearchCategory> CreateFilterControl() => new FilterControl();
private IEnumerable<BeatmapSetInfo> beatmapSets;
public IEnumerable<BeatmapSetInfo> BeatmapSets
{
get => beatmapSets;
set
{
if (ReferenceEquals(beatmapSets, value)) return;
beatmapSets = value?.ToList();
if (beatmapSets == null) return;
var artists = new List<string>();
var songs = new List<string>();
var tags = new List<string>();
foreach (var s in beatmapSets)
{
artists.Add(s.Metadata.Artist);
songs.Add(s.Metadata.Title);
tags.AddRange(s.Metadata.Tags.Split(' '));
}
ResultAmounts = new ResultCounts(distinctCount(artists), distinctCount(songs), distinctCount(tags));
}
}
private ResultCounts resultAmounts;
public ResultCounts ResultAmounts
{
get => resultAmounts;
set
{
if (value == ResultAmounts) return;
resultAmounts = value;
updateResultCounts();
}
}
public DirectOverlay()
: base(OverlayColourScheme.Blue)
{
ScrollFlow.Children = new Drawable[]
{
resultCountsContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Top = 5 },
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Found ",
Font = OsuFont.GetFont(size: 15)
},
resultCountsText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold)
},
}
},
};
Filter.Search.Current.ValueChanged += text =>
{
if (!string.IsNullOrEmpty(text.NewValue))
{
Header.Tabs.Current.Value = DirectTab.Search;
if (Filter.Tabs.Current.Value == DirectSortCriteria.Ranked)
Filter.Tabs.Current.Value = DirectSortCriteria.Relevance;
}
else
{
Header.Tabs.Current.Value = DirectTab.NewestMaps;
if (Filter.Tabs.Current.Value == DirectSortCriteria.Relevance)
Filter.Tabs.Current.Value = DirectSortCriteria.Ranked;
}
};
((FilterControl)Filter).Ruleset.ValueChanged += _ => queueUpdateSearch();
Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue);
Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdateSearch();
Header.Tabs.Current.ValueChanged += tab =>
{
if (tab.NewValue != DirectTab.Search)
{
currentQuery.Value = string.Empty;
Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value;
queueUpdateSearch();
}
};
currentQuery.ValueChanged += text => queueUpdateSearch(!string.IsNullOrEmpty(text.NewValue));
currentQuery.BindTo(Filter.Search.Current);
Filter.Tabs.Current.ValueChanged += tab =>
{
if (Header.Tabs.Current.Value != DirectTab.Search && tab.NewValue != (DirectSortCriteria)Header.Tabs.Current.Value)
Header.Tabs.Current.Value = DirectTab.Search;
queueUpdateSearch();
};
updateResultCounts();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
resultCountsContainer.Colour = colours.Yellow;
}
private void updateResultCounts()
{
resultCountsContainer.FadeTo(ResultAmounts == null ? 0f : 1f, 200, Easing.OutQuint);
if (ResultAmounts == null) return;
resultCountsText.Text = "Artist".ToQuantity(ResultAmounts.Artists) + ", " +
"Song".ToQuantity(ResultAmounts.Songs) + ", " +
"Tag".ToQuantity(ResultAmounts.Tags);
}
private void recreatePanels(PanelDisplayStyle displayStyle)
{
if (panels != null)
{
panels.FadeOut(200);
panels.Expire();
panels = null;
}
if (BeatmapSets == null) return;
var newPanels = new FillFlowContainer<DirectPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(panel_padding),
Margin = new MarginPadding { Top = 10 },
ChildrenEnumerable = BeatmapSets.Select<BeatmapSetInfo, DirectPanel>(b =>
{
switch (displayStyle)
{
case PanelDisplayStyle.Grid:
return new DirectGridPanel(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
};
default:
return new DirectListPanel(b);
}
})
};
LoadComponentAsync(newPanels, p =>
{
if (panels != null) ScrollFlow.Remove(panels);
ScrollFlow.Add(panels = newPanels);
});
}
protected override void PopIn()
{
base.PopIn();
// Queries are allowed to be run only on the first pop-in
if (getSetsRequest == null)
queueUpdateSearch();
}
private SearchBeatmapSetsRequest getSetsRequest;
private readonly Bindable<string> currentQuery = new Bindable<string>(string.Empty);
private ScheduledDelegate queryChangedDebounce;
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
private void queueUpdateSearch(bool queryTextChanged = false)
{
BeatmapSets = null;
ResultAmounts = null;
getSetsRequest?.Cancel();
queryChangedDebounce?.Cancel();
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
}
private void updateSearch()
{
if (!IsLoaded)
return;
if (State.Value == Visibility.Hidden)
return;
if (API == null)
return;
previewTrackManager.StopAnyPlaying(this);
getSetsRequest = new SearchBeatmapSetsRequest(
currentQuery.Value,
((FilterControl)Filter).Ruleset.Value,
Filter.DisplayStyleControl.Dropdown.Current.Value,
Filter.Tabs.Current.Value); //todo: sort direction (?)
getSetsRequest.Success += response =>
{
Task.Run(() =>
{
var sets = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
// may not need scheduling; loads async internally.
Schedule(() =>
{
BeatmapSets = sets;
recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value);
});
});
};
API.Queue(getSetsRequest);
}
private int distinctCount(List<string> list) => list.Distinct().ToArray().Length;
public class ResultCounts
{
public readonly int Artists;
public readonly int Songs;
public readonly int Tags;
public ResultCounts(int artists, int songs, int tags)
{
Artists = artists;
Songs = songs;
Tags = tags;
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -29,14 +29,8 @@ namespace osu.Game.Overlays.Music
{
public CollectionsMenu()
{
Masking = true;
CornerRadius = 5;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.3f),
Radius = 3,
Offset = new Vector2(0f, 1f),
};
}
[BackgroundDependencyLoader]

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using System;
namespace osu.Game.Overlays.News
@ -12,8 +11,6 @@ namespace osu.Game.Overlays.News
{
private const string front_page_string = "frontpage";
private NewsHeaderTitle title;
public readonly Bindable<string> Post = new Bindable<string>(null);
public Action ShowFrontPage;
@ -40,36 +37,24 @@ namespace osu.Game.Overlays.News
{
TabControl.AddItem(e.NewValue);
Current.Value = e.NewValue;
title.IsReadingPost = true;
}
else
{
Current.Value = front_page_string;
title.IsReadingPost = false;
}
}
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/news");
protected override ScreenTitle CreateTitle() => title = new NewsHeaderTitle();
protected override OverlayTitle CreateTitle() => new NewsHeaderTitle();
private class NewsHeaderTitle : ScreenTitle
private class NewsHeaderTitle : OverlayTitle
{
private const string post_string = "post";
public bool IsReadingPost
{
set => Section = value ? post_string : front_page_string;
}
public NewsHeaderTitle()
{
Title = "news";
IsReadingPost = false;
IconTexture = "Icons/news";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/news");
}
}
}

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.News;
namespace osu.Game.Overlays
@ -36,7 +35,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = colours.PurpleDarkAlternative
},
new OsuScrollContainer
new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer

View File

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

View File

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

View File

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

View File

@ -6,15 +6,15 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public abstract class OverlayHeader : Container
{
public const int CONTENT_X_MARGIN = 50;
private readonly Box titleBackground;
private readonly ScreenTitle title;
protected readonly FillFlowContainer HeaderInfo;
@ -56,12 +56,11 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Horizontal = UserProfileOverlay.CONTENT_X_MARGIN,
Vertical = 10,
Horizontal = CONTENT_X_MARGIN,
},
Children = new[]
{
title = CreateTitle().With(title =>
CreateTitle().With(title =>
{
title.Anchor = Anchor.CentreLeft;
title.Origin = Anchor.CentreLeft;
@ -86,7 +85,6 @@ namespace osu.Game.Overlays
private void load(OverlayColourProvider colourProvider)
{
titleBackground.Colour = colourProvider.Dark5;
title.AccentColour = colourProvider.Highlight1;
}
[NotNull]
@ -96,11 +94,11 @@ namespace osu.Game.Overlays
protected virtual Drawable CreateBackground() => Empty();
/// <summary>
/// Creates a <see cref="Drawable"/> on the opposite side of the <see cref="ScreenTitle"/>. Used mostly to create <see cref="OverlayRulesetSelector"/>.
/// Creates a <see cref="Drawable"/> on the opposite side of the <see cref="OverlayTitle"/>. Used mostly to create <see cref="OverlayRulesetSelector"/>.
/// </summary>
[NotNull]
protected virtual Drawable CreateTitleContent() => Empty();
protected abstract ScreenTitle CreateTitle();
protected abstract OverlayTitle CreateTitle();
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(25, 0),
Spacing = new Vector2(20, 0),
};
}
}

View File

@ -12,6 +12,7 @@ using osu.Game.Rulesets;
using osuTK.Graphics;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
namespace osu.Game.Overlays
{
@ -53,6 +54,8 @@ namespace osu.Game.Overlays
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Text = value.Name,
Font = OsuFont.GetFont(size: 14),
ShadowColour = Color4.Black.Opacity(0.75f)
}
},
new HoverClickSounds()

View File

@ -0,0 +1,149 @@
// 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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
/// <summary>
/// <see cref="OsuScrollContainer"/> which provides <see cref="ScrollToTopButton"/>. Mostly used in <see cref="FullscreenOverlay"/>.
/// </summary>
public class OverlayScrollContainer : OsuScrollContainer
{
/// <summary>
/// Scroll position at which the <see cref="ScrollToTopButton"/> will be shown.
/// </summary>
private const int button_scroll_position = 200;
protected readonly ScrollToTopButton Button;
public OverlayScrollContainer()
{
AddInternal(Button = new ScrollToTopButton
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding(20),
Action = () =>
{
ScrollToStart();
Button.State = Visibility.Hidden;
}
});
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight)
{
Button.State = Visibility.Hidden;
return;
}
Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden;
}
public class ScrollToTopButton : OsuHoverContainer
{
private const int fade_duration = 500;
private Visibility state;
public Visibility State
{
get => state;
set
{
if (value == state)
return;
state = value;
Enabled.Value = state == Visibility.Visible;
this.FadeTo(state == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint);
}
}
protected override IEnumerable<Drawable> EffectTargets => new[] { background };
private Color4 flashColour;
private readonly Container content;
private readonly Box background;
public ScrollToTopButton()
{
Size = new Vector2(50);
Alpha = 0;
Add(content = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Offset = new Vector2(0f, 1f),
Radius = 3f,
Colour = Color4.Black.Opacity(0.25f),
},
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(15),
Icon = FontAwesome.Solid.ChevronUp
}
}
});
TooltipText = "Scroll to top";
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
IdleColour = colourProvider.Background6;
HoverColour = colourProvider.Background5;
flashColour = colourProvider.Light1;
}
protected override bool OnClick(ClickEvent e)
{
background.FlashColour(flashColour, 800, Easing.OutQuint);
return base.OnClick(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
content.ScaleTo(0.75f, 2000, Easing.OutQuint);
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
content.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
@ -35,17 +36,22 @@ namespace osu.Game.Overlays
protected OverlayTabControl()
{
TabContainer.Masking = false;
TabContainer.Spacing = new Vector2(15, 0);
TabContainer.Spacing = new Vector2(20, 0);
AddInternal(bar = new Box
{
RelativeSizeAxes = Axes.X,
Height = 2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.CentreLeft
Origin = Anchor.BottomLeft
});
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
AccentColour = colourProvider.Highlight1;
}
protected override Dropdown<T> CreateDropdown() => null;
protected override TabItem<T> CreateTabItem(T value) => new OverlayTabItem(value);
@ -90,7 +96,7 @@ namespace osu.Game.Overlays
Bar = new ExpandingBar
{
Anchor = Anchor.BottomCentre,
ExpandedSize = 7.5f,
ExpandedSize = 5f,
CollapsedSize = 0
},
new HoverClickSounds()
@ -119,6 +125,7 @@ namespace osu.Game.Overlays
{
HoverAction();
Text.Font = Text.Font.With(weight: FontWeight.Bold);
Text.FadeColour(Color4.White, 120, Easing.InQuad);
}
protected override void OnDeactivated()
@ -135,11 +142,7 @@ namespace osu.Game.Overlays
OnDeactivated();
}
protected virtual void HoverAction()
{
Bar.Expand();
Text.FadeColour(Color4.White, 120, Easing.InQuad);
}
protected virtual void HoverAction() => Bar.Expand();
protected virtual void UnhoverAction()
{

View File

@ -0,0 +1,80 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays
{
public abstract class OverlayTitle : CompositeDrawable
{
private readonly OsuSpriteText title;
private readonly Container icon;
protected string Title
{
set => title.Text = value;
}
protected string IconTexture
{
set => icon.Child = new OverlayTitleIcon(value);
}
protected OverlayTitle()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10, 0),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
icon = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 5 }, // compensates for osu-web sprites having around 5px of whitespace on each side
Size = new Vector2(30)
},
title = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular),
Margin = new MarginPadding { Vertical = 17.5f } // 15px padding + 2.5px line-height difference compensation
}
}
};
}
private class OverlayTitleIcon : Sprite
{
private readonly string textureName;
public OverlayTitleIcon(string textureName)
{
this.textureName = textureName;
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
FillMode = FillMode.Fit;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Texture = textures.Get(textureName);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More