Merge branch 'master' into channelselection-minimum-size

This commit is contained in:
Dean Herbert
2017-09-13 14:21:44 +09:00
139 changed files with 3157 additions and 1708 deletions

View File

@ -100,6 +100,11 @@ namespace osu.Game.Beatmaps
public bool Equals(BeatmapInfo other)
{
if (ID == 0 || other?.ID == 0)
// one of the two BeatmapInfos we are comparing isn't sourced from a database.
// fall back to reference equality.
return ReferenceEquals(this, other);
return ID == other?.ID;
}

View File

@ -20,6 +20,9 @@ using osu.Game.IPC;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using SQLite.Net;
using osu.Game.Online.API.Requests;
using System.Threading.Tasks;
using osu.Game.Online.API;
namespace osu.Game.Beatmaps
{
@ -63,6 +66,10 @@ namespace osu.Game.Beatmaps
private readonly BeatmapStore beatmaps;
private readonly APIAccess api;
private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private BeatmapIPCChannel ipc;
@ -76,7 +83,7 @@ namespace osu.Game.Beatmaps
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
public BeatmapManager(Storage storage, FileStore files, SQLiteConnection connection, RulesetStore rulesets, IIpcHost importHost = null)
public BeatmapManager(Storage storage, FileStore files, SQLiteConnection connection, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
{
beatmaps = new BeatmapStore(connection);
beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s);
@ -88,6 +95,7 @@ namespace osu.Game.Beatmaps
this.files = files;
this.connection = connection;
this.rulesets = rulesets;
this.api = api;
if (importHost != null)
ipc = new BeatmapIPCChannel(importHost, this);
@ -177,6 +185,74 @@ namespace osu.Game.Beatmaps
beatmaps.Add(beatmapSetInfo);
}
/// <summary>
/// Downloads a beatmap.
/// </summary>
/// <param name="beatmapSetInfo">The <see cref="BeatmapSetInfo"/> to be downloaded.</param>
/// <returns>A new <see cref="DownloadBeatmapSetRequest"/>, or an existing one if a download is already in progress.</returns>
public DownloadBeatmapSetRequest Download(BeatmapSetInfo beatmapSetInfo)
{
var existing = GetExistingDownload(beatmapSetInfo);
if (existing != null) return existing;
if (api == null) return null;
ProgressNotification downloadNotification = new ProgressNotification
{
Text = $"Downloading {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}",
};
var request = new DownloadBeatmapSetRequest(beatmapSetInfo);
request.DownloadProgressed += progress =>
{
downloadNotification.State = ProgressNotificationState.Active;
downloadNotification.Progress = progress;
};
request.Success += data =>
{
downloadNotification.State = ProgressNotificationState.Completed;
using (var stream = new MemoryStream(data))
using (var archive = new OszArchiveReader(stream))
Import(archive);
currentDownloads.Remove(request);
};
request.Failure += data =>
{
downloadNotification.State = ProgressNotificationState.Completed;
Logger.Error(data, "Failed to get beatmap download information");
currentDownloads.Remove(request);
};
downloadNotification.CancelRequested += () =>
{
request.Cancel();
currentDownloads.Remove(request);
downloadNotification.State = ProgressNotificationState.Cancelled;
return true;
};
currentDownloads.Add(request);
PostNotification?.Invoke(downloadNotification);
// don't run in the main api queue as this is a long-running task.
Task.Run(() => request.Perform(api));
return request;
}
/// <summary>
/// Get an existing download request if it exists.
/// </summary>
/// <param name="beatmap">The <see cref="BeatmapSetInfo"/> whose download request is wanted.</param>
/// <returns>The <see cref="DownloadBeatmapSetRequest"/> object if it exists, or null.</returns>
public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID);
/// <summary>
/// Delete a beatmap from the manager.
/// Is a no-op for already deleted beatmaps.

View File

@ -87,7 +87,9 @@ namespace osu.Game.Beatmaps
{
if (track != null) return track;
track = GetTrack();
// we want to ensure that we always have a track, even if it's a fake one.
track = GetTrack() ?? new TrackVirtual();
applyRateAdjustments();
return track;
}
@ -107,10 +109,17 @@ namespace osu.Game.Beatmaps
public virtual void Dispose()
{
track?.Dispose();
track = null;
background?.Dispose();
background = null;
}
public void DisposeTrack()
{
lock (trackLock)
{
track?.Dispose();
track = null;
}
}
}
}

View File

@ -131,7 +131,7 @@ namespace osu.Game.Graphics.Cursor
},
AdditiveLayer = new Sprite
{
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Colour = colour.Pink,
Alpha = 0,
Texture = textures.Get(@"Cursor/menu-cursor-additive"),

View File

@ -57,6 +57,12 @@ namespace osu.Game.Graphics
private void load(FontStore store)
{
this.store = store;
}
protected override void LoadComplete()
{
base.LoadComplete();
updateTexture();
}

View File

@ -99,7 +99,8 @@ namespace osu.Game.Graphics.UserInterface
colourContainer.ResizeTo(new Vector2(1.5f, 1f), click_duration, Easing.In);
flash();
this.Delay(click_duration).Schedule(delegate {
this.Delay(click_duration).Schedule(delegate
{
colourContainer.ResizeTo(new Vector2(0.8f, 1f));
spriteText.Spacing = Vector2.Zero;
glowContainer.FadeOut();
@ -140,7 +141,7 @@ namespace osu.Game.Graphics.UserInterface
colourContainer.Add(flash);
flash.Colour = ButtonColour;
flash.BlendingMode = BlendingMode.Additive;
flash.Blending = BlendingMode.Additive;
flash.Alpha = 0.3f;
flash.FadeOutFromOne(click_duration);
flash.Expire();

View File

@ -63,7 +63,7 @@ namespace osu.Game.Graphics.UserInterface
hover = new Box
{
RelativeSizeAxes = Axes.Both,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Colour = Color4.White.Opacity(0.1f),
Alpha = 0,
},

View File

@ -96,7 +96,7 @@ namespace osu.Game.Graphics.UserInterface
protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item) { AccentColour = accentColour };
#region DrawableOsuDropdownMenuItem
protected class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem, IHasAccentColour
public class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem, IHasAccentColour
{
private Color4? accentColour;
public Color4 AccentColour

View File

@ -15,11 +15,7 @@ namespace osu.Game.Graphics.UserInterface.Volume
{
private readonly VolumeMeter volumeMeterMaster;
private void volumeChanged(double newVolume)
{
Show();
schedulePopOut();
}
protected override bool BlockPassThroughMouse => false;
public VolumeControl()
{
@ -85,6 +81,12 @@ namespace osu.Game.Graphics.UserInterface.Volume
return false;
}
private void volumeChanged(double newVolume)
{
Show();
schedulePopOut();
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{

View File

@ -0,0 +1,24 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using System;
namespace osu.Game.Online.API.Requests
{
public class DownloadBeatmapSetRequest : APIDownloadRequest
{
public readonly BeatmapSetInfo BeatmapSet;
public Action<float> DownloadProgressed;
public DownloadBeatmapSetRequest(BeatmapSetInfo set)
{
BeatmapSet = set;
Progress += (current, total) => DownloadProgressed?.Invoke((float) current / total);
}
protected override string Target => $@"beatmapsets/{BeatmapSet.OnlineBeatmapSetID}/download";
}
}

View File

@ -106,9 +106,15 @@ namespace osu.Game
connection.CreateTable<StoreVersion>();
dependencies.Cache(API = new APIAccess
{
Username = LocalConfig.Get<string>(OsuSetting.Username),
Token = LocalConfig.Get<string>(OsuSetting.Token)
});
dependencies.Cache(RulesetStore = new RulesetStore(connection));
dependencies.Cache(FileStore = new FileStore(connection, Host.Storage));
dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, FileStore, connection, RulesetStore, Host));
dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, FileStore, connection, RulesetStore, API, Host));
dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, connection, Host, BeatmapManager, RulesetStore));
dependencies.Cache(KeyBindingStore = new KeyBindingStore(connection, RulesetStore));
dependencies.Cache(new OsuColour());
@ -144,18 +150,21 @@ namespace osu.Game
Beatmap = new NonNullableBindable<WorkingBeatmap>(defaultBeatmap);
BeatmapManager.DefaultBeatmap = defaultBeatmap;
dependencies.Cache(API = new APIAccess
{
Username = LocalConfig.Get<string>(OsuSetting.Username),
Token = LocalConfig.Get<string>(OsuSetting.Token)
});
Beatmap.ValueChanged += b =>
{
var trackLoaded = lastBeatmap?.TrackLoaded ?? false;
// compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo)
if (lastBeatmap?.Track != b.Track)
if (!trackLoaded || lastBeatmap?.Track != b.Track)
{
lastBeatmap?.Track?.Dispose();
if (trackLoaded)
{
Debug.Assert(lastBeatmap != null);
Debug.Assert(lastBeatmap.Track != null);
lastBeatmap.DisposeTrack();
}
Audio.Track.AddItem(b.Track);
}

View File

@ -4,8 +4,6 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using System.IO;
using System.Threading.Tasks;
using OpenTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -20,8 +18,8 @@ using osu.Framework.Input;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Framework.Logging;
using osu.Game.Beatmaps.IO;
using osu.Game.Overlays.Notifications;
using osu.Game.Online.API.Requests;
namespace osu.Game.Overlays.Direct
{
@ -97,6 +95,11 @@ namespace osu.Game.Overlays.Direct
},
}
});
var downloadRequest = beatmaps.GetExistingDownload(SetInfo);
if (downloadRequest != null)
attachDownload(downloadRequest);
}
protected override bool OnHover(InputState state)
@ -115,23 +118,8 @@ namespace osu.Game.Overlays.Direct
base.OnHoverLost(state);
}
// this should eventually be moved to a more central place, like BeatmapManager.
private DownloadBeatmapSetRequest downloadRequest;
protected void StartDownload()
{
if (api == null) return;
// we already have an active download running.
if (downloadRequest != null)
{
content.MoveToX(-5, 50, Easing.OutSine).Then()
.MoveToX(5, 100, Easing.InOutSine).Then()
.MoveToX(-5, 100, Easing.InOutSine).Then()
.MoveToX(0, 50, Easing.InSine).Then();
return;
}
if (!api.LocalUser.Value.IsSupporter)
{
notifications.Post(new SimpleNotification
@ -142,72 +130,43 @@ namespace osu.Game.Overlays.Direct
return;
}
if (beatmaps.GetExistingDownload(SetInfo) != null)
{
// we already have an active download running.
content.MoveToX(-5, 50, Easing.OutSine).Then()
.MoveToX(5, 100, Easing.InOutSine).Then()
.MoveToX(-5, 100, Easing.InOutSine).Then()
.MoveToX(0, 50, Easing.InSine).Then();
return;
}
var request = beatmaps.Download(SetInfo);
attachDownload(request);
}
private void attachDownload(DownloadBeatmapSetRequest request)
{
progressBar.FadeIn(400, Easing.OutQuint);
progressBar.ResizeHeightTo(4, 400, Easing.OutQuint);
progressBar.Current.Value = 0;
ProgressNotification downloadNotification = new ProgressNotification
{
Text = $"Downloading {SetInfo.Metadata.Artist} - {SetInfo.Metadata.Title}",
};
downloadRequest = new DownloadBeatmapSetRequest(SetInfo);
downloadRequest.Failure += e =>
request.Failure += e =>
{
progressBar.Current.Value = 0;
progressBar.FadeOut(500);
downloadNotification.State = ProgressNotificationState.Completed;
Logger.Error(e, "Failed to get beatmap download information");
downloadRequest = null;
};
downloadRequest.Progress += (current, total) =>
{
float progress = (float)current / total;
request.DownloadProgressed += progress => progressBar.Current.Value = progress;
progressBar.Current.Value = progress;
downloadNotification.State = ProgressNotificationState.Active;
downloadNotification.Progress = progress;
};
downloadRequest.Success += data =>
request.Success += data =>
{
progressBar.Current.Value = 1;
progressBar.FadeOut(500);
downloadNotification.State = ProgressNotificationState.Completed;
using (var stream = new MemoryStream(data))
using (var archive = new OszArchiveReader(stream))
beatmaps.Import(archive);
};
downloadNotification.CancelRequested += () =>
{
downloadRequest.Cancel();
downloadRequest = null;
return true;
};
notifications.Post(downloadNotification);
// don't run in the main api queue as this is a long-running task.
Task.Run(() => downloadRequest.Perform(api));
}
public class DownloadBeatmapSetRequest : APIDownloadRequest
{
private readonly BeatmapSetInfo beatmapSet;
public DownloadBeatmapSetRequest(BeatmapSetInfo beatmapSet)
{
this.beatmapSet = beatmapSet;
}
protected override string Target => $@"beatmapsets/{beatmapSet.OnlineBeatmapSetID}/download";
}
protected override void LoadComplete()

View File

@ -27,6 +27,7 @@ namespace osu.Game.Overlays
private APIAccess api;
private RulesetStore rulesets;
private BeatmapManager beatmaps;
private readonly FillFlowContainer resultCountsContainer;
private readonly OsuSpriteText resultCountsText;
@ -46,9 +47,26 @@ namespace osu.Game.Overlays
set
{
if (beatmapSets?.Equals(value) ?? false) return;
beatmapSets = value;
recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value);
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));
if (beatmapSets.Any() && panels == null)
// real use case? currently only seems to be for test case
recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value);
}
}
@ -147,6 +165,8 @@ namespace osu.Game.Overlays
{
this.api = api;
this.rulesets = rulesets;
this.beatmaps = beatmaps;
resultCountsContainer.Colour = colours.Yellow;
beatmaps.BeatmapSetAdded += setAdded;
@ -156,6 +176,7 @@ namespace osu.Game.Overlays
{
// if a new map was imported, we should remove it from search results (download completed etc.)
panels?.FirstOrDefault(p => p.SetInfo.OnlineBeatmapSetID == set.OnlineBeatmapSetID)?.FadeOut(400).Expire();
BeatmapSets = BeatmapSets?.Where(b => b.OnlineBeatmapSetID != set.OnlineBeatmapSetID);
}
private void updateResultCounts()
@ -237,20 +258,11 @@ namespace osu.Game.Overlays
getSetsRequest.Success += r =>
{
BeatmapSets = r?.Select(response => response.ToBeatmapSet(rulesets));
if (BeatmapSets == null) return;
BeatmapSets = r?.
Select(response => response.ToBeatmapSet(rulesets)).
Where(b => beatmaps.QueryBeatmapSet(q => q.OnlineBeatmapSetID == b.OnlineBeatmapSetID) == null);
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));
recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value);
};
api.Queue(getSetsRequest);

View File

@ -302,8 +302,8 @@ namespace osu.Game.Overlays
else
{
//figure out the best direction based on order in playlist.
var last = playlist.BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo.ID).Count();
var next = beatmap == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != beatmap.BeatmapSetInfo.ID).Count();
var last = playlist.BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count();
var next = beatmap == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != beatmap.BeatmapSetInfo?.ID).Count();
direction = last > next ? TransformDirection.Prev : TransformDirection.Next;
}

View File

@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Settings
backgroundBox = new Box
{
RelativeSizeAxes = Axes.Both,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Colour = OsuColour.Gray(60),
Alpha = 0,
},

View File

@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Toolbar
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(80).Opacity(180),
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Alpha = 0,
},
Flow = new FillFlowContainer

View File

@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Toolbar
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(150).Opacity(180),
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Depth = 2,
Alpha = 0,
});

View File

@ -16,11 +16,9 @@ namespace osu.Game.Rulesets.Judgements
/// <summary>
/// A drawable object which visualises the hit result of a <see cref="Judgements.Judgement"/>.
/// </summary>
/// <typeparam name="TJudgement">The type of judgement to visualise.</typeparam>
public class DrawableJudgement<TJudgement> : Container
where TJudgement : Judgement
public class DrawableJudgement : Container
{
protected readonly TJudgement Judgement;
protected readonly Judgement Judgement;
protected readonly SpriteText JudgementText;
@ -28,21 +26,19 @@ namespace osu.Game.Rulesets.Judgements
/// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>.
/// </summary>
/// <param name="judgement">The judgement to visualise.</param>
public DrawableJudgement(TJudgement judgement)
public DrawableJudgement(Judgement judgement)
{
Judgement = judgement;
AutoSizeAxes = Axes.Both;
string resultString = judgement.Result == HitResult.Hit ? judgement.ResultString : judgement.Result.GetDescription();
Children = new[]
{
JudgementText = new OsuSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Text = resultString.ToUpper(),
Text = judgement.Result.GetDescription().ToUpper(),
Font = @"Venera",
TextSize = 16
}
@ -68,6 +64,8 @@ namespace osu.Game.Rulesets.Judgements
switch (Judgement.Result)
{
case HitResult.None:
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
@ -77,7 +75,7 @@ namespace osu.Game.Rulesets.Judgements
this.Delay(600).FadeOut(200);
break;
case HitResult.Hit:
default:
this.ScaleTo(0.9f);
this.ScaleTo(1, 500, Easing.OutElastic);

View File

@ -1,19 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Judgements
{
/// <summary>
/// Inidicates that the judgement this is attached to is a partial judgement and the scoring value may change.
/// </summary>
public interface IPartialJudgement
{
/// <summary>
/// Indicates that this partial judgement has changed and requires reprocessing.
/// <para>
/// This is set to false once the judgement has been re-processed.
/// </para>
/// </summary>
bool Changed { get; set; }
}
}

View File

@ -5,7 +5,7 @@ using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Judgements
{
public abstract class Judgement
public class Judgement
{
/// <summary>
/// Whether this judgement is the result of a hit or a miss.
@ -13,20 +13,35 @@ namespace osu.Game.Rulesets.Judgements
public HitResult Result;
/// <summary>
/// The offset at which this judgement occurred.
/// The maximum <see cref="HitResult"/> that can be achieved.
/// </summary>
public double TimeOffset;
public virtual HitResult MaxResult => HitResult.Perfect;
public bool IsHit => Result > HitResult.Miss;
/// <summary>
/// The offset from a perfect hit at which this judgement occurred.
/// Populated when added via <see cref="DrawableHitObject{TObject}.AddJudgement"/>.
/// </summary>
public double TimeOffset { get; internal set; }
public virtual bool AffectsCombo => true;
/// <summary>
/// The string representation for the result achieved.
/// The numeric representation for the result achieved.
/// </summary>
public abstract string ResultString { get; }
public int NumericResult => NumericResultFor(Result);
/// <summary>
/// The string representation for the max result achievable.
/// The numeric representation for the maximum achievable result.
/// </summary>
public abstract string MaxResultString { get; }
public int MaxNumericResult => NumericResultFor(MaxResult);
/// <summary>
/// Convert a <see cref="HitResult"/> to a numeric score representation.
/// </summary>
/// <param name="result">The value to convert.</param>
/// <returns>The number.</returns>
protected virtual int NumericResultFor(HitResult result) => result > HitResult.Miss ? 1 : 0;
}
}
}

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
{
protected abstract Score CreateReplayScore(Beatmap<T> beatmap);
public void ApplyToRulesetContainer(RulesetContainer<T> rulesetContainer)
public virtual void ApplyToRulesetContainer(RulesetContainer<T> rulesetContainer)
{
rulesetContainer.SetReplay(CreateReplayScore(rulesetContainer.Beatmap)?.Replay);
}

View File

@ -12,17 +12,18 @@ using osu.Game.Rulesets.Objects.Types;
using OpenTK.Graphics;
using osu.Game.Audio;
using System.Linq;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables
{
public abstract class DrawableHitObject : Container
public abstract class DrawableHitObject : Container, IHasAccentColour
{
public readonly HitObject HitObject;
/// <summary>
/// The colour used for various elements of this DrawableHitObject.
/// </summary>
public virtual Color4 AccentColour { get; set; }
public virtual Color4 AccentColour { get; set; } = Color4.Gray;
protected DrawableHitObject(HitObject hitObject)
{
@ -33,32 +34,29 @@ namespace osu.Game.Rulesets.Objects.Drawables
public abstract class DrawableHitObject<TObject> : DrawableHitObject
where TObject : HitObject
{
public event Action<DrawableHitObject, Judgement> OnJudgement;
public new readonly TObject HitObject;
protected DrawableHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
}
}
public abstract class DrawableHitObject<TObject, TJudgement> : DrawableHitObject<TObject>
where TObject : HitObject
where TJudgement : Judgement
{
public event Action<DrawableHitObject<TObject, TJudgement>> OnJudgement;
public override bool HandleInput => Interactive;
public bool Interactive = true;
public TJudgement Judgement;
/// <summary>
/// Whether this <see cref="DrawableHitObject"/> can be judged.
/// </summary>
protected virtual bool ProvidesJudgement => true;
private readonly List<Judgement> judgements = new List<Judgement>();
public IReadOnlyList<Judgement> Judgements => judgements;
protected List<SampleChannel> Samples = new List<SampleChannel>();
protected DrawableHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
Depth = (float)hitObject.StartTime;
}
private ArmedState state;
@ -95,69 +93,80 @@ namespace osu.Game.Rulesets.Objects.Drawables
UpdateState(State);
}
/// <summary>
/// Whether this hit object and all of its nested hit objects have been judged.
/// </summary>
public bool Judged => (Judgement?.Result ?? HitResult.None) != HitResult.None && (NestedHitObjects?.All(h => h.Judged) ?? true);
private bool hasJudgementResult;
private bool judgementOccurred;
/// <summary>
/// Process a hit of this hitobject. Carries out judgement.
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
/// </summary>
/// <returns>Whether a hit was processed.</returns>
protected bool UpdateJudgement(bool userTriggered)
public virtual bool AllJudged => (!ProvidesJudgement || hasJudgementResult) && (NestedHitObjects?.All(h => h.AllJudged) ?? true);
/// <summary>
/// Notifies that a new judgement has occurred for this <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="judgement">The <see cref="Judgement"/>.</param>
protected void AddJudgement(Judgement judgement)
{
if (Judgement == null)
return false;
hasJudgementResult = judgement.Result >= HitResult.Miss;
judgementOccurred = true;
var partial = Judgement as IPartialJudgement;
// Ensure that the judgement is given a valid time offset, because this may not get set by the caller
var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
judgement.TimeOffset = Time.Current - endTime;
// Never re-process non-partial hits
if (Judgement.Result != HitResult.None && partial == null)
return false;
judgements.Add(judgement);
// Update the judgement state
double endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
Judgement.TimeOffset = Time.Current - endTime;
// Update the judgement state
bool hadResult = Judgement.Result != HitResult.None;
CheckJudgement(userTriggered);
// Don't process judgements with no result
if (Judgement.Result == HitResult.None)
return false;
// Don't process judgements that previously had results but the results were unchanged
if (hadResult && partial?.Changed != true)
return false;
switch (Judgement.Result)
switch (judgement.Result)
{
default:
State = ArmedState.Hit;
case HitResult.None:
break;
case HitResult.Miss:
State = ArmedState.Miss;
break;
default:
State = ArmedState.Hit;
break;
}
OnJudgement?.Invoke(this);
if (partial != null)
partial.Changed = false;
return true;
OnJudgement?.Invoke(this, judgement);
}
protected virtual void CheckJudgement(bool userTriggered)
/// <summary>
/// Processes this <see cref="DrawableHitObject"/>, checking if any judgements have occurred.
/// </summary>
/// <param name="userTriggered">Whether the user triggered this process.</param>
/// <returns>Whether a judgement has occurred from this <see cref="DrawableHitObject"/> or any nested <see cref="DrawableHitObject"/>s.</returns>
protected bool UpdateJudgement(bool userTriggered)
{
judgementOccurred = false;
if (AllJudged || State != ArmedState.Idle)
return false;
if (NestedHitObjects != null)
{
foreach (var d in NestedHitObjects)
d.CheckJudgement(userTriggered);
judgementOccurred |= d.UpdateJudgement(userTriggered);
}
if (!ProvidesJudgement || hasJudgementResult || judgementOccurred)
return judgementOccurred;
var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
CheckForJudgements(userTriggered, Time.Current - endTime);
return judgementOccurred;
}
/// <summary>
/// Checks if any judgements have occurred for this <see cref="DrawableHitObject"/>. This method must construct
/// all <see cref="Judgement"/>s and notify of them through <see cref="AddJudgement"/>.
/// </summary>
/// <param name="userTriggered">Whether the user triggered this check.</param>
/// <param name="timeOffset">The offset from the <see cref="HitObject"/> end time at which this check occurred. A <paramref name="timeOffset"/> &gt; 0
/// implies that this check occurred after the end time of <see cref="HitObject"/>. </param>
protected virtual void CheckForJudgements(bool userTriggered, double timeOffset) { }
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@ -178,25 +187,20 @@ namespace osu.Game.Rulesets.Objects.Drawables
channel.Volume.Value = sample.Volume;
Samples.Add(channel);
}
//we may be setting a custom judgement in test cases or what not.
if (Judgement == null)
Judgement = CreateJudgement();
}
private List<DrawableHitObject<TObject, TJudgement>> nestedHitObjects;
protected IEnumerable<DrawableHitObject<TObject, TJudgement>> NestedHitObjects => nestedHitObjects;
private List<DrawableHitObject<TObject>> nestedHitObjects;
protected IEnumerable<DrawableHitObject<TObject>> NestedHitObjects => nestedHitObjects;
protected virtual void AddNested(DrawableHitObject<TObject, TJudgement> h)
protected virtual void AddNested(DrawableHitObject<TObject> h)
{
if (nestedHitObjects == null)
nestedHitObjects = new List<DrawableHitObject<TObject, TJudgement>>();
nestedHitObjects = new List<DrawableHitObject<TObject>>();
h.OnJudgement += d => OnJudgement?.Invoke(d);
h.OnJudgement += (d, j) => OnJudgement?.Invoke(d, j);
nestedHitObjects.Add(h);
}
protected abstract TJudgement CreateJudgement();
protected abstract void UpdateState(ArmedState state);
}
}

View File

@ -3,19 +3,17 @@
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Drawables
{
/// <summary>
/// A basic class that overrides <see cref="DrawableHitObject{TObject, TJudgement}"/> and implements <see cref="IScrollingHitObject"/>.
/// A basic class that overrides <see cref="DrawableHitObject{TObject}"/> and implements <see cref="IScrollingHitObject"/>.
/// This object does not need to have its <see cref="Drawable.RelativePositionAxes"/> set to be able to scroll, as this will
/// will be set by the scrolling container that contains it.
/// </summary>
public abstract class DrawableScrollingHitObject<TObject, TJudgement> : DrawableHitObject<TObject, TJudgement>, IScrollingHitObject
public abstract class DrawableScrollingHitObject<TObject> : DrawableHitObject<TObject>, IScrollingHitObject
where TObject : HitObject
where TJudgement : Judgement
{
public BindableDouble LifetimeOffset { get; } = new BindableDouble();
@ -57,7 +55,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
set { lifetimeEnd = value; }
}
protected override void AddNested(DrawableHitObject<TObject, TJudgement> h)
protected override void AddNested(DrawableHitObject<TObject> h)
{
var scrollingHitObject = h as IScrollingHitObject;
scrollingHitObject?.LifetimeOffset.BindTo(LifetimeOffset);

View File

@ -10,17 +10,34 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary>
/// Indicates that the object has not been judged yet.
/// </summary>
[Description("")]
[Description(@"")]
None,
/// <summary>
/// Indicates that the object has been judged as a miss.
/// </summary>
[Description(@"Miss")]
Miss,
[Description(@"Meh")]
Meh,
/// <summary>
/// Indicates that the object has been judged as a hit.
/// Optional judgement.
/// </summary>
[Description(@"Hit")]
Hit,
[Description(@"OK")]
Ok,
[Description(@"Good")]
Good,
[Description(@"Great")]
Great,
/// <summary>
/// Optional judgement.
/// </summary>
[Description(@"Perfect")]
Perfect,
}
}
}

View File

@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Objects
/// </summary>
public SampleInfoList Samples = new SampleInfoList();
/// <summary>
/// Whether this <see cref="HitObject"/> is in Kiai time.
/// </summary>
public bool Kiai { get; private set; }
/// <summary>
/// Applies default values to this HitObject.
/// </summary>
@ -38,6 +43,9 @@ namespace osu.Game.Rulesets.Objects
public virtual void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
SoundControlPoint soundPoint = controlPointInfo.SoundPointAt(StartTime);
EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime);
Kiai |= effectPoint.KiaiMode;
// Initialize first sample
Samples.ForEach(s => initializeSampleInfo(s, soundPoint));

View File

@ -135,14 +135,13 @@ namespace osu.Game.Rulesets.Scoring
}
}
public abstract class ScoreProcessor<TObject, TJudgement> : ScoreProcessor
public abstract class ScoreProcessor<TObject> : ScoreProcessor
where TObject : HitObject
where TJudgement : Judgement
{
/// <summary>
/// All judgements held by this ScoreProcessor.
/// </summary>
protected readonly List<TJudgement> Judgements = new List<TJudgement>();
protected readonly List<Judgement> Judgements = new List<Judgement>();
public override bool HasFailed => Health.Value == Health.MinValue;
@ -150,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring
{
}
protected ScoreProcessor(RulesetContainer<TObject, TJudgement> rulesetContainer)
protected ScoreProcessor(RulesetContainer<TObject> rulesetContainer)
{
Judgements.Capacity = rulesetContainer.Beatmap.HitObjects.Count;
@ -171,7 +170,7 @@ namespace osu.Game.Rulesets.Scoring
/// Adds a judgement to this ScoreProcessor.
/// </summary>
/// <param name="judgement">The judgement to add.</param>
protected void AddJudgement(TJudgement judgement)
protected void AddJudgement(Judgement judgement)
{
bool exists = Judgements.Contains(judgement);
@ -181,10 +180,12 @@ namespace osu.Game.Rulesets.Scoring
{
switch (judgement.Result)
{
case HitResult.None:
break;
case HitResult.Miss:
Combo.Value = 0;
break;
case HitResult.Hit:
default:
Combo.Value++;
break;
}
@ -195,8 +196,6 @@ namespace osu.Game.Rulesets.Scoring
NotifyNewJudgement(judgement);
}
else
OnJudgementChanged(judgement);
UpdateFailed();
}
@ -210,20 +209,8 @@ namespace osu.Game.Rulesets.Scoring
/// <summary>
/// Updates any values that need post-processing. Invoked when a new judgement has occurred.
/// <para>
/// This is not triggered when existing judgements are changed - for that see <see cref="OnJudgementChanged(TJudgement)"/>.
/// </para>
/// </summary>
/// <param name="judgement">The judgement that triggered this calculation.</param>
protected abstract void OnNewJudgement(TJudgement judgement);
/// <summary>
/// Updates any values that need post-processing. Invoked when an existing judgement has changed.
/// <para>
/// This is not triggered when a new judgement has occurred - for that see <see cref="OnNewJudgement(TJudgement)"/>.
/// </para>
/// </summary>
/// <param name="judgement">The judgement that triggered this calculation.</param>
protected virtual void OnJudgementChanged(TJudgement judgement) { }
protected abstract void OnNewJudgement(Judgement judgement);
}
}

View File

@ -4,7 +4,6 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
using osu.Game.Rulesets.Judgements;
@ -14,9 +13,7 @@ using System.Linq;
namespace osu.Game.Rulesets.UI
{
public abstract class Playfield<TObject, TJudgement> : Container
where TObject : HitObject
where TJudgement : Judgement
public abstract class Playfield : Container
{
/// <summary>
/// The HitObjects contained in this Playfield.
@ -70,7 +67,7 @@ namespace osu.Game.Rulesets.UI
public override Axes RelativeSizeAxes
{
get { return Axes.Both; }
set { throw new InvalidOperationException($@"{nameof(Playfield<TObject, TJudgement>)}'s {nameof(RelativeSizeAxes)} should never be changed from {Axes.Both}"); }
set { throw new InvalidOperationException($@"{nameof(Playfield)}'s {nameof(RelativeSizeAxes)} should never be changed from {Axes.Both}"); }
}
/// <summary>
@ -82,19 +79,20 @@ namespace osu.Game.Rulesets.UI
/// Adds a DrawableHitObject to this Playfield.
/// </summary>
/// <param name="h">The DrawableHitObject to add.</param>
public virtual void Add(DrawableHitObject<TObject, TJudgement> h) => HitObjects.Add(h);
public virtual void Add(DrawableHitObject h) => HitObjects.Add(h);
/// <summary>
/// Remove a DrawableHitObject from this Playfield.
/// </summary>
/// <param name="h">The DrawableHitObject to remove.</param>
public virtual void Remove(DrawableHitObject<TObject, TJudgement> h) => HitObjects.Remove(h);
public virtual void Remove(DrawableHitObject h) => HitObjects.Remove(h);
/// <summary>
/// Triggered when an object's Judgement is updated.
/// Triggered when a new <see cref="Judgement"/> occurs on a <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="judgedObject">The object that Judgement has been updated for.</param>
public virtual void OnJudgement(DrawableHitObject<TObject, TJudgement> judgedObject) { }
/// <param name="judgedObject">The object that <paramref name="judgement"/> occured for.</param>
/// <param name="judgement">The <see cref="Judgement"/> that occurred.</param>
public virtual void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) { }
public class HitObjectContainer : CompositeDrawable
{

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// Base RulesetContainer. Doesn't hold objects.
/// <para>
/// Should not be derived - derive <see cref="RulesetContainer{TObject,TJudgement}"/> instead.
/// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead.
/// </para>
/// </summary>
public abstract class RulesetContainer : Container
@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.UI
public event Action OnAllJudged;
/// <summary>
/// Whether to apply adjustments to the child <see cref="Playfield{TObject,TJudgement}"/> based on our own size.
/// Whether to apply adjustments to the child <see cref="Playfield"/> based on our own size.
/// </summary>
public bool AspectAdjust = true;
@ -77,13 +77,6 @@ namespace osu.Game.Rulesets.UI
Ruleset = ruleset;
}
[BackgroundDependencyLoader]
private void load()
{
KeyBindingInputManager = CreateInputManager();
KeyBindingInputManager.RelativeSizeAxes = Axes.Both;
}
/// <summary>
/// Checks whether all HitObjects have been judged, and invokes OnAllJudged.
/// </summary>
@ -123,13 +116,15 @@ namespace osu.Game.Rulesets.UI
/// RulesetContainer that applies conversion to Beatmaps. Does not contain a Playfield
/// and does not load drawable hit objects.
/// <para>
/// Should not be derived - derive <see cref="RulesetContainer{TObject,TJudgement}"/> instead.
/// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead.
/// </para>
/// </summary>
/// <typeparam name="TObject">The type of HitObject contained by this RulesetContainer.</typeparam>
public abstract class RulesetContainer<TObject> : RulesetContainer
where TObject : HitObject
{
public event Action<Judgement> OnJudgement;
/// <summary>
/// The Beatmap
/// </summary>
@ -155,6 +150,20 @@ namespace osu.Game.Rulesets.UI
/// </summary>
protected readonly bool IsForCurrentRuleset;
public sealed override bool ProvidingUserCursor => !HasReplayLoaded && Playfield.ProvidingUserCursor;
protected override bool AllObjectsJudged => drawableObjects.All(h => h.AllJudged);
/// <summary>
/// The playfield.
/// </summary>
public Playfield Playfield { get; private set; }
protected override Container<Drawable> Content => content;
private Container content;
private readonly List<DrawableHitObject<TObject>> drawableObjects = new List<DrawableHitObject<TObject>>();
/// <summary>
/// Whether to assume the beatmap passed into this <see cref="RulesetContainer{TObject}"/> is for the current ruleset.
/// Creates a hit renderer for a beatmap.
@ -162,7 +171,7 @@ namespace osu.Game.Rulesets.UI
/// <param name="ruleset">The ruleset being repesented.</param>
/// <param name="workingBeatmap">The beatmap to create the hit renderer for.</param>
/// <param name="isForCurrentRuleset">Whether to assume the beatmap is for the current ruleset.</param>
internal RulesetContainer(Ruleset ruleset, WorkingBeatmap workingBeatmap, bool isForCurrentRuleset)
protected RulesetContainer(Ruleset ruleset, WorkingBeatmap workingBeatmap, bool isForCurrentRuleset)
: base(ruleset)
{
Debug.Assert(workingBeatmap != null, "RulesetContainer initialized with a null beatmap.");
@ -194,78 +203,13 @@ namespace osu.Game.Rulesets.UI
// Post-process the beatmap
processor.PostProcess(Beatmap);
KeyBindingInputManager = CreateInputManager();
KeyBindingInputManager.RelativeSizeAxes = Axes.Both;
// Add mods, should always be the last thing applied to give full control to mods
applyMods(Mods);
}
/// <summary>
/// Applies the active mods to this RulesetContainer.
/// </summary>
/// <param name="mods"></param>
private void applyMods(IEnumerable<Mod> mods)
{
if (mods == null)
return;
foreach (var mod in mods.OfType<IApplicableMod<TObject>>())
mod.ApplyToRulesetContainer(this);
}
/// <summary>
/// Creates a processor to perform post-processing operations
/// on HitObjects in converted Beatmaps.
/// </summary>
/// <returns>The Beatmap processor.</returns>
protected virtual BeatmapProcessor<TObject> CreateBeatmapProcessor() => new BeatmapProcessor<TObject>();
/// <summary>
/// Creates a converter to convert Beatmap to a specific mode.
/// </summary>
/// <returns>The Beatmap converter.</returns>
protected abstract BeatmapConverter<TObject> CreateBeatmapConverter();
}
/// <summary>
/// A derivable RulesetContainer that manages the Playfield and HitObjects.
/// </summary>
/// <typeparam name="TObject">The type of HitObject contained by this RulesetContainer.</typeparam>
/// <typeparam name="TJudgement">The type of Judgement of DrawableHitObjects contained by this RulesetContainer.</typeparam>
public abstract class RulesetContainer<TObject, TJudgement> : RulesetContainer<TObject>
where TObject : HitObject
where TJudgement : Judgement
{
public event Action<TJudgement> OnJudgement;
public sealed override bool ProvidingUserCursor => !HasReplayLoaded && Playfield.ProvidingUserCursor;
/// <summary>
/// All the converted hit objects contained by this hit renderer.
/// </summary>
public new IEnumerable<TObject> Objects => Beatmap.HitObjects;
protected override bool AllObjectsJudged => drawableObjects.All(h => h.Judged);
/// <summary>
/// The playfield.
/// </summary>
public Playfield<TObject, TJudgement> Playfield { get; private set; }
protected override Container<Drawable> Content => content;
private Container content;
private readonly List<DrawableHitObject<TObject, TJudgement>> drawableObjects = new List<DrawableHitObject<TObject, TJudgement>>();
/// <summary>
/// Creates a hit renderer for a beatmap.
/// </summary>
/// <param name="ruleset">The ruleset being repesented.</param>
/// <param name="beatmap">The beatmap to create the hit renderer for.</param>
/// <param name="isForCurrentRuleset">Whether to assume the beatmap is for the current ruleset.</param>
protected RulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(ruleset, beatmap, isForCurrentRuleset)
{
}
[BackgroundDependencyLoader]
private void load()
{
@ -280,6 +224,19 @@ namespace osu.Game.Rulesets.UI
loadObjects();
}
/// <summary>
/// Applies the active mods to this RulesetContainer.
/// </summary>
/// <param name="mods"></param>
private void applyMods(IEnumerable<Mod> mods)
{
if (mods == null)
return;
foreach (var mod in mods.OfType<IApplicableMod<TObject>>())
mod.ApplyToRulesetContainer(this);
}
public override void SetReplay(Replay replay)
{
base.SetReplay(replay);
@ -302,7 +259,12 @@ namespace osu.Game.Rulesets.UI
if (drawableObject == null)
continue;
drawableObject.OnJudgement += onJudgement;
drawableObject.OnJudgement += (d, j) =>
{
Playfield.OnJudgement(d, j);
OnJudgement?.Invoke(j);
CheckAllJudged();
};
drawableObjects.Add(drawableObject);
Playfield.Add(drawableObject);
@ -319,36 +281,36 @@ namespace osu.Game.Rulesets.UI
}
/// <summary>
/// In some cases we want to apply changes to the relative size of our contained <see cref="Playfield{TObject, TJudgement}"/> based on custom conditions.
/// Creates a processor to perform post-processing operations
/// on HitObjects in converted Beatmaps.
/// </summary>
/// <returns>The Beatmap processor.</returns>
protected virtual BeatmapProcessor<TObject> CreateBeatmapProcessor() => new BeatmapProcessor<TObject>();
/// <summary>
/// In some cases we want to apply changes to the relative size of our contained <see cref="Playfield"/> based on custom conditions.
/// </summary>
/// <returns></returns>
protected virtual Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); //a sane default
/// <summary>
/// Triggered when an object's Judgement is updated.
/// Creates a converter to convert Beatmap to a specific mode.
/// </summary>
/// <param name="judgedObject">The object that Judgement has been updated for.</param>
private void onJudgement(DrawableHitObject<TObject, TJudgement> judgedObject)
{
Playfield.OnJudgement(judgedObject);
OnJudgement?.Invoke(judgedObject.Judgement);
CheckAllJudged();
}
/// <returns>The Beatmap converter.</returns>
protected abstract BeatmapConverter<TObject> CreateBeatmapConverter();
/// <summary>
/// Creates a DrawableHitObject from a HitObject.
/// </summary>
/// <param name="h">The HitObject to make drawable.</param>
/// <returns>The DrawableHitObject.</returns>
protected abstract DrawableHitObject<TObject, TJudgement> GetVisualRepresentation(TObject h);
protected abstract DrawableHitObject<TObject> GetVisualRepresentation(TObject h);
/// <summary>
/// Creates a Playfield.
/// </summary>
/// <returns>The Playfield.</returns>
protected abstract Playfield<TObject, TJudgement> CreatePlayfield();
protected abstract Playfield CreatePlayfield();
}
/// <summary>
@ -356,11 +318,9 @@ namespace osu.Game.Rulesets.UI
/// </summary>
/// <typeparam name="TPlayfield">The type of Playfield contained by this RulesetContainer.</typeparam>
/// <typeparam name="TObject">The type of HitObject contained by this RulesetContainer.</typeparam>
/// <typeparam name="TJudgement">The type of Judgement of DrawableHitObjects contained by this RulesetContainer.</typeparam>
public abstract class RulesetContainer<TPlayfield, TObject, TJudgement> : RulesetContainer<TObject, TJudgement>
public abstract class RulesetContainer<TPlayfield, TObject> : RulesetContainer<TObject>
where TObject : HitObject
where TJudgement : Judgement
where TPlayfield : Playfield<TObject, TJudgement>
where TPlayfield : Playfield
{
/// <summary>
/// The playfield.

View File

@ -11,19 +11,15 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// A type of <see cref="Playfield{TObject, TJudgement}"/> specialized towards scrolling <see cref="DrawableHitObject"/>s.
/// A type of <see cref="Playfield"/> specialized towards scrolling <see cref="DrawableHitObject"/>s.
/// </summary>
public class ScrollingPlayfield<TObject, TJudgement> : Playfield<TObject, TJudgement>
where TObject : HitObject
where TJudgement : Judgement
public class ScrollingPlayfield : Playfield
{
/// <summary>
/// The default span of time visible by the length of the scrolling axes.
@ -65,7 +61,7 @@ namespace osu.Game.Rulesets.UI
public new readonly ScrollingHitObjectContainer HitObjects;
/// <summary>
/// Creates a new <see cref="ScrollingPlayfield{TObject, TJudgement}"/>.
/// Creates a new <see cref="ScrollingPlayfield"/>.
/// </summary>
/// <param name="scrollingAxes">The axes on which <see cref="DrawableHitObject"/>s in this container should scroll.</param>
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width</param>
@ -77,21 +73,21 @@ namespace osu.Game.Rulesets.UI
HitObjects.Reversed.BindTo(Reversed);
}
private List<ScrollingPlayfield<TObject, TJudgement>> nestedPlayfields;
private List<ScrollingPlayfield> nestedPlayfields;
/// <summary>
/// All the <see cref="ScrollingPlayfield{TObject, TJudgement}"/>s nested inside this playfield.
/// All the <see cref="ScrollingPlayfield"/>s nested inside this playfield.
/// </summary>
public IEnumerable<ScrollingPlayfield<TObject, TJudgement>> NestedPlayfields => nestedPlayfields;
public IEnumerable<ScrollingPlayfield> NestedPlayfields => nestedPlayfields;
/// <summary>
/// Adds a <see cref="ScrollingPlayfield{TObject, TJudgement}"/> to this playfield. The nested <see cref="ScrollingPlayfield{TObject, TJudgement}"/>
/// Adds a <see cref="ScrollingPlayfield"/> to this playfield. The nested <see cref="ScrollingPlayfield"/>
/// will be given all of the same speed adjustments as this playfield.
/// </summary>
/// <param name="otherPlayfield">The <see cref="ScrollingPlayfield{TObject, TJudgement}"/> to add.</param>
protected void AddNested(ScrollingPlayfield<TObject, TJudgement> otherPlayfield)
/// <param name="otherPlayfield">The <see cref="ScrollingPlayfield"/> to add.</param>
protected void AddNested(ScrollingPlayfield otherPlayfield)
{
if (nestedPlayfields == null)
nestedPlayfields = new List<ScrollingPlayfield<TObject, TJudgement>>();
nestedPlayfields = new List<ScrollingPlayfield>();
nestedPlayfields.Add(otherPlayfield);
}
@ -119,7 +115,7 @@ namespace osu.Game.Rulesets.UI
this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing));
}
private class TransformVisibleTimeRange : Transform<double, ScrollingPlayfield<TObject, TJudgement>>
private class TransformVisibleTimeRange : Transform<double, ScrollingPlayfield>
{
private double valueAt(double time)
{
@ -131,8 +127,8 @@ namespace osu.Game.Rulesets.UI
public override string TargetMember => "VisibleTimeRange.Value";
protected override void Apply(ScrollingPlayfield<TObject, TJudgement> d, double time) => d.VisibleTimeRange.Value = valueAt(time);
protected override void ReadIntoStartValue(ScrollingPlayfield<TObject, TJudgement> d) => StartValue = d.VisibleTimeRange.Value;
protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time);
protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value;
}
/// <summary>

View File

@ -9,7 +9,6 @@ using osu.Framework.Lists;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Timing;
@ -17,17 +16,16 @@ using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// A type of <see cref="RulesetContainer{TPlayfield,TObject,TJudgement}"/> that supports a <see cref="ScrollingPlayfield{TObject, TJudgement}"/>.
/// <see cref="HitObject"/>s inside this <see cref="RulesetContainer{TPlayfield,TObject,TJudgement}"/> will scroll within the playfield.
/// A type of <see cref="RulesetContainer{TPlayfield,TObject}"/> that supports a <see cref="ScrollingPlayfield"/>.
/// <see cref="HitObject"/>s inside this <see cref="RulesetContainer{TPlayfield,TObject}"/> will scroll within the playfield.
/// </summary>
public abstract class ScrollingRulesetContainer<TPlayfield, TObject, TJudgement> : RulesetContainer<TPlayfield, TObject, TJudgement>
public abstract class ScrollingRulesetContainer<TPlayfield, TObject> : RulesetContainer<TPlayfield, TObject>
where TObject : HitObject
where TJudgement : Judgement
where TPlayfield : ScrollingPlayfield<TObject, TJudgement>
where TPlayfield : ScrollingPlayfield
{
/// <summary>
/// Provides the default <see cref="MultiplierControlPoint"/>s that adjust the scrolling rate of <see cref="HitObject"/>s
/// inside this <see cref="RulesetContainer{TPlayfield,TObject,TJudgement}"/>.
/// inside this <see cref="RulesetContainer{TPlayfield,TObject}"/>.
/// </summary>
/// <returns></returns>
protected readonly SortedList<MultiplierControlPoint> DefaultControlPoints = new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default);
@ -88,7 +86,7 @@ namespace osu.Game.Rulesets.UI
DefaultControlPoints.ForEach(c => applySpeedAdjustment(c, Playfield));
}
private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield<TObject, TJudgement> playfield)
private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield playfield)
{
playfield.HitObjects.AddSpeedAdjustment(CreateSpeedAdjustmentContainer(controlPoint));
playfield.NestedPlayfields.ForEach(p => applySpeedAdjustment(controlPoint, p));

View File

@ -83,7 +83,7 @@ namespace osu.Game.Screens.Menu
{
EdgeSmoothness = new Vector2(1.5f, 0),
RelativeSizeAxes = Axes.Both,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Colour = Color4.White,
Alpha = 0,
},

View File

@ -52,7 +52,7 @@ namespace osu.Game.Screens.Menu
{
Alpha = 0,
Triangles = false,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Interactive = false,
Colour = Color4.DarkGray,
Ripple = false

View File

@ -73,7 +73,7 @@ namespace osu.Game.Screens.Menu
{
texture = Texture.WhitePixel;
AccentColour = new Color4(1, 1, 1, 0.2f);
BlendingMode = BlendingMode.Additive;
Blending = BlendingMode.Additive;
}
[BackgroundDependencyLoader]

View File

@ -50,7 +50,7 @@ namespace osu.Game.Screens.Menu
RelativeSizeAxes = Axes.Y,
Width = box_width,
Alpha = 0,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
},
rightBox = new Box
{
@ -59,7 +59,7 @@ namespace osu.Game.Screens.Menu
RelativeSizeAxes = Axes.Y,
Width = box_width,
Alpha = 0,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
}
};
}

View File

@ -108,7 +108,7 @@ namespace osu.Game.Screens.Menu
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Alpha = 0
}
}
@ -169,7 +169,7 @@ namespace osu.Game.Screens.Menu
flashLayer = new Box
{
RelativeSizeAxes = Axes.Both,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Colour = Color4.White,
Alpha = 0,
},
@ -272,14 +272,22 @@ namespace osu.Game.Screens.Menu
const float scale_adjust_cutoff = 0.4f;
const float velocity_adjust_cutoff = 0.98f;
const float paused_velocity = 0.5f;
var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value.Track?.CurrentAmplitudes.Maximum ?? 0 : 0;
logoAmplitudeContainer.ScaleTo(1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 75, Easing.OutQuint);
if (Beatmap.Value.Track.IsRunning)
{
var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value.Track.CurrentAmplitudes.Maximum : 0;
logoAmplitudeContainer.ScaleTo(1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 75, Easing.OutQuint);
if (maxAmplitude > velocity_adjust_cutoff)
triangles.Velocity = 1 + Math.Max(0, maxAmplitude - velocity_adjust_cutoff) * 50;
if (maxAmplitude > velocity_adjust_cutoff)
triangles.Velocity = 1 + Math.Max(0, maxAmplitude - velocity_adjust_cutoff) * 50;
else
triangles.Velocity = (float)Interpolation.Damp(triangles.Velocity, 1, 0.995f, Time.Elapsed);
}
else
triangles.Velocity = (float)Interpolation.Damp(triangles.Velocity, 1, 0.995f, Time.Elapsed);
{
triangles.Velocity = paused_velocity;
}
}
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)

View File

@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play
public override void Add(KeyCounter key)
{
if (key == null) throw new ArgumentNullException(nameof(key));
base.Add(key);
key.IsCounting = IsCounting;
key.FadeTime = FadeTime;

View File

@ -100,7 +100,7 @@ namespace osu.Game.Screens
Colour = getColourFor(GetType()),
Alpha = 0.2f,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
},
textContainer = new FillFlowContainer
{

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
@ -10,6 +11,8 @@ namespace osu.Game.Screens.Select
{
public class BeatmapDetailArea : Container
{
private const float details_padding = 10;
private readonly Container content;
protected override Container<Drawable> Content => content;
@ -66,9 +69,8 @@ namespace osu.Game.Screens.Select
Details = new BeatmapDetails
{
RelativeSizeAxes = Axes.X,
Masking = true,
Height = 352,
Alpha = 0,
Margin = new MarginPadding { Top = details_padding },
},
Leaderboard = new Leaderboard
{
@ -76,5 +78,12 @@ namespace osu.Game.Screens.Select
}
});
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Details.Height = Math.Min(DrawHeight - details_padding * 3 - BeatmapDetailAreaTabControl.HEIGHT, 450);
}
}
}

View File

@ -9,71 +9,189 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using System.Globalization;
using System.Linq;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Framework.Threading;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Screens.Select.Details;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select
{
public class BeatmapDetails : Container
{
private readonly MetadataSegment description;
private readonly MetadataSegment source;
private readonly MetadataSegment tags;
private const float spacing = 10;
private const float transition_duration = 250;
private readonly DifficultyRow circleSize;
private readonly DifficultyRow drainRate;
private readonly DifficultyRow overallDifficulty;
private readonly DifficultyRow approachRate;
private readonly DifficultyRow stars;
private readonly FillFlowContainer top, statsFlow;
private readonly AdvancedStats advanced;
private readonly DetailBox ratingsContainer;
private readonly UserRatings ratings;
private readonly ScrollContainer metadataScroll;
private readonly MetadataSection description, source, tags;
private readonly Container failRetryContainer;
private readonly FailRetryGraph failRetryGraph;
private readonly DimmedLoadingAnimation loading;
private readonly Container ratingsContainer;
private readonly Bar ratingsBar;
private readonly OsuSpriteText negativeRatings;
private readonly OsuSpriteText positiveRatings;
private readonly BarGraph ratingsGraph;
private readonly FillFlowContainer retryFailContainer;
private readonly BarGraph retryGraph;
private readonly BarGraph failGraph;
private APIAccess api;
private ScheduledDelegate pendingBeatmapSwitch;
private BeatmapInfo beatmap;
private BeatmapInfo beatmap;
public BeatmapInfo Beatmap
{
get { return beatmap; }
set
{
if (beatmap == value) return;
if (value == beatmap) return;
beatmap = value;
pendingBeatmapSwitch?.Cancel();
pendingBeatmapSwitch = Schedule(updateStats);
pendingBeatmapSwitch = Schedule(updateStatistics);
}
}
private void updateStats()
public BeatmapDetails()
{
if (beatmap == null) return;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f),
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = spacing },
Children = new Drawable[]
{
top = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
statsFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.5f,
Spacing = new Vector2(spacing),
Padding = new MarginPadding { Right = spacing / 2 },
Children = new[]
{
new DetailBox
{
Child = advanced = new AdvancedStats
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = spacing, Top = spacing * 2, Bottom = spacing },
},
},
ratingsContainer = new DetailBox
{
Child = ratings = new UserRatings
{
RelativeSizeAxes = Axes.X,
Height = 134,
Padding = new MarginPadding { Horizontal = spacing, Top = spacing },
},
},
},
},
metadataScroll = new ScrollContainer
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
ScrollbarVisible = false,
Padding = new MarginPadding { Left = spacing / 2 },
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
LayoutDuration = transition_duration,
Spacing = new Vector2(spacing * 2),
Margin = new MarginPadding { Top = spacing * 2 },
Children = new[]
{
description = new MetadataSection("Description")
{
TextColour = Color4.White.Opacity(0.75f),
},
source = new MetadataSection("Source")
{
TextColour = Color4.White.Opacity(0.75f),
},
tags = new MetadataSection("Tags"),
},
},
},
},
},
failRetryContainer = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Points of Failure",
Font = @"Exo2.0-Bold",
TextSize = 14,
},
failRetryGraph = new FailRetryGraph
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 14 + spacing / 2 },
},
},
},
},
},
loading = new DimmedLoadingAnimation
{
RelativeSizeAxes = Axes.Both,
},
};
}
description.Text = beatmap.Version;
source.Text = beatmap.Metadata.Source;
tags.Text = beatmap.Metadata.Tags;
[BackgroundDependencyLoader]
private void load(OsuColour colours, APIAccess api)
{
this.api = api;
tags.TextColour = colours.Yellow;
}
circleSize.Value = beatmap.Difficulty.CircleSize;
drainRate.Value = beatmap.Difficulty.DrainRate;
overallDifficulty.Value = beatmap.Difficulty.OverallDifficulty;
approachRate.Value = beatmap.Difficulty.ApproachRate;
stars.Value = (float)beatmap.StarDifficulty;
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
var requestedBeatmap = beatmap;
metadataScroll.Height = statsFlow.DrawHeight;
failRetryContainer.Height = DrawHeight - Padding.TotalVertical - (top.DrawHeight + spacing / 2);
}
private void updateStatistics()
{
if (Beatmap == null)
{
clearStats();
return;
}
ratingsContainer.FadeIn(transition_duration);
advanced.Beatmap = Beatmap;
description.Text = Beatmap.Version;
source.Text = Beatmap.Metadata.Source;
tags.Text = Beatmap.Metadata.Tags;
var requestedBeatmap = Beatmap;
if (requestedBeatmap.Metrics == null)
{
var lookup = new GetBeatmapDetailsRequest(requestedBeatmap);
@ -84,413 +202,195 @@ namespace osu.Game.Screens.Select
return;
requestedBeatmap.Metrics = res;
Schedule(() => updateMetrics(res));
Schedule(() => displayMetrics(res));
};
lookup.Failure += e => Schedule(() => updateMetrics(null));
lookup.Failure += e => Schedule(() => displayMetrics(null));
api.Queue(lookup);
loading.Show();
}
updateMetrics(requestedBeatmap.Metrics, false);
displayMetrics(requestedBeatmap.Metrics, false);
}
/// <summary>
/// Update displayed metrics.
/// </summary>
/// <param name="metrics">New metrics to overwrite the existing display. Can be null.</param>
/// <param name="failOnMissing">Whether to hide the display on null or empty metrics. If false, we will dim as if waiting for further updates.</param>
private void updateMetrics(BeatmapMetrics metrics, bool failOnMissing = true)
private void displayMetrics(BeatmapMetrics metrics, bool failOnMissing = true)
{
var hasRatings = metrics?.Ratings.Any() ?? false;
var hasRetriesFails = (metrics?.Retries.Any() ?? false) && metrics.Fails.Any();
var hasRatings = metrics?.Ratings?.Any() ?? false;
var hasRetriesFails = (metrics?.Retries?.Any() ?? false) && (metrics.Fails?.Any() ?? false);
if (failOnMissing)
loading.Hide();
if (failOnMissing) loading.Hide();
if (hasRatings)
{
var ratings = metrics.Ratings.ToList();
ratingsContainer.Show();
negativeRatings.Text = ratings.GetRange(0, ratings.Count / 2).Sum().ToString();
positiveRatings.Text = ratings.GetRange(ratings.Count / 2, ratings.Count / 2).Sum().ToString();
ratingsBar.Length = (float)ratings.GetRange(0, ratings.Count / 2).Sum() / ratings.Sum();
ratingsGraph.Values = ratings.Select(rating => (float)rating);
ratingsContainer.FadeColour(Color4.White, 500, Easing.Out);
ratings.Metrics = metrics;
ratings.FadeIn(transition_duration);
}
else if (failOnMissing)
ratingsGraph.Values = new float[10];
{
ratings.Metrics = new BeatmapMetrics
{
Ratings = new int[10],
};
}
else
ratingsContainer.FadeColour(Color4.Gray, 500, Easing.Out);
{
ratings.FadeTo(0.25f, transition_duration);
}
if (hasRetriesFails)
{
var retries = metrics.Retries;
var fails = metrics.Fails;
retryFailContainer.Show();
float maxValue = fails.Zip(retries, (fail, retry) => fail + retry).Max();
failGraph.MaxValue = maxValue;
retryGraph.MaxValue = maxValue;
failGraph.Values = fails.Select(fail => (float)fail);
retryGraph.Values = retries.Zip(fails, (retry, fail) => retry + MathHelper.Clamp(fail, 0, maxValue));
retryFailContainer.FadeColour(Color4.White, 500, Easing.Out);
failRetryGraph.Metrics = metrics;
failRetryContainer.FadeIn(transition_duration);
}
else if (failOnMissing)
{
failGraph.Values = new float[100];
retryGraph.Values = new float[100];
failRetryGraph.Metrics = new BeatmapMetrics
{
Fails = new int[100],
Retries = new int[100],
};
}
else
retryFailContainer.FadeColour(Color4.Gray, 500, Easing.Out);
{
failRetryContainer.FadeTo(0.25f, transition_duration);
}
}
public BeatmapDetails()
private void clearStats()
{
Children = new Drawable[]
description.Text = null;
source.Text = null;
tags.Text = null;
advanced.Beatmap = new BeatmapInfo
{
new Box
StarDifficulty = 0,
Difficulty = new BeatmapDifficulty
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
CircleSize = 0,
DrainRate = 0,
OverallDifficulty = 0,
ApproachRate = 0,
},
new FillFlowContainer<MetadataSegment>
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.4f,
Direction = FillDirection.Vertical,
LayoutDuration = 200,
LayoutEasing = Easing.OutQuint,
Children = new[]
{
description = new MetadataSegment("Description"),
source = new MetadataSegment("Source"),
tags = new MetadataSegment("Tags")
},
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.6f,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 15),
Padding = new MarginPadding(10) { Top = 0 },
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Padding = new MarginPadding(10),
Children = new[]
{
circleSize = new DifficultyRow("Circle Size", 7),
drainRate = new DifficultyRow("HP Drain"),
overallDifficulty = new DifficultyRow("Accuracy"),
approachRate = new DifficultyRow("Approach Rate"),
stars = new DifficultyRow("Star Difficulty"),
},
},
},
},
ratingsContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
AlwaysPresent = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = new MarginPadding
{
Top = 25,
Left = 15,
Right = 15,
},
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "User Rating",
Font = @"Exo2.0-Medium",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
ratingsBar = new Bar
{
RelativeSizeAxes = Axes.X,
Height = 5,
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
negativeRatings = new OsuSpriteText
{
Font = @"Exo2.0-Regular",
Text = "0",
},
positiveRatings = new OsuSpriteText
{
Font = @"Exo2.0-Regular",
Text = "0",
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
},
},
new OsuSpriteText
{
Text = "Rating Spread",
TextSize = 14,
Font = @"Exo2.0-Regular",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
ratingsGraph = new BarGraph
{
RelativeSizeAxes = Axes.X,
Height = 50,
},
},
},
},
},
retryFailContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Points of Failure",
Font = @"Exo2.0-Regular",
},
new Container<BarGraph>
{
RelativeSizeAxes = Axes.X,
Size = new Vector2(1 / 0.6f, 50),
Children = new[]
{
retryGraph = new BarGraph
{
RelativeSizeAxes = Axes.Both,
},
failGraph = new BarGraph
{
RelativeSizeAxes = Axes.Both,
},
},
},
}
},
},
},
loading = new LoadingAnimation()
};
loading.Hide();
ratingsContainer.FadeOut(transition_duration);
failRetryContainer.FadeOut(transition_duration);
}
private APIAccess api;
private readonly LoadingAnimation loading;
[BackgroundDependencyLoader]
private void load(OsuColour colour, APIAccess api)
private class DetailBox : Container
{
this.api = api;
private readonly Container content;
protected override Container<Drawable> Content => content;
description.AccentColour = colour.GrayB;
source.AccentColour = colour.GrayB;
tags.AccentColour = colour.YellowLight;
stars.AccentColour = colour.Yellow;
ratingsBar.BackgroundColour = colour.Green;
ratingsBar.AccentColour = colour.YellowDark;
ratingsGraph.Colour = colour.BlueDark;
failGraph.Colour = colour.YellowDarker;
retryGraph.Colour = colour.Yellow;
}
private class DifficultyRow : Container, IHasAccentColour
{
private readonly OsuSpriteText name;
private readonly Bar bar;
private readonly OsuSpriteText valueText;
private readonly float maxValue;
private float difficultyValue;
public float Value
public DetailBox()
{
get
{
return difficultyValue;
}
set
{
difficultyValue = value;
bar.Length = value / maxValue;
valueText.Text = value.ToString("N1", CultureInfo.CurrentCulture);
}
}
public Color4 AccentColour
{
get
{
return bar.AccentColour;
}
set
{
bar.AccentColour = value;
}
}
public DifficultyRow(string difficultyName, float maxValue = 10)
{
this.maxValue = maxValue;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Children = new Drawable[]
InternalChildren = new Drawable[]
{
name = new OsuSpriteText
new Box
{
Font = @"Exo2.0-Regular",
Text = difficultyName,
},
bar = new Bar
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1, 0.35f),
Padding = new MarginPadding { Left = 100, Right = 25 },
Colour = Color4.Black.Opacity(0.5f),
},
valueText = new OsuSpriteText
content = new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = @"Exo2.0-Regular",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
name.Colour = colour.GrayB;
bar.BackgroundColour = colour.Gray7;
valueText.Colour = colour.GrayB;
}
}
private class MetadataSegment : Container, IHasAccentColour
private class MetadataSection : Container
{
private readonly OsuSpriteText header;
private readonly FillFlowContainer<OsuSpriteText> content;
private readonly TextFlowContainer textFlow;
public string Text
{
set
{
if (string.IsNullOrEmpty(value))
Hide();
else
{
Show();
if (header.Text == "Tags")
content.ChildrenEnumerable = value.Split(' ').Select(text => new OsuSpriteText
{
Text = text,
Font = "Exo2.0-Regular",
});
else
content.Children = new[]
{
new OsuSpriteText
{
Text = value,
Font = "Exo2.0-Regular",
}
};
this.FadeOut(transition_duration);
return;
}
this.FadeIn(transition_duration);
textFlow.Clear();
textFlow.AddText(value, s => s.TextSize = 14);
}
}
public Color4 AccentColour
public Color4 TextColour
{
get
{
return content.Colour;
}
set
{
content.Colour = value;
}
get { return textFlow.Colour; }
set { textFlow.Colour = value; }
}
public MetadataSegment(string headerText)
public MetadataSection(string title)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Margin = new MarginPadding { Top = 10 };
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(spacing / 2),
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new OsuSpriteText
{
Text = title,
Font = @"Exo2.0-Bold",
TextSize = 14,
},
},
textFlow = new TextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
},
};
}
}
private class DimmedLoadingAnimation : VisibilityContainer
{
private readonly LoadingAnimation loading;
public DimmedLoadingAnimation()
{
Children = new Drawable[]
{
header = new OsuSpriteText
new Box
{
Font = @"Exo2.0-Bold",
Text = headerText,
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f),
},
content = new FillFlowContainer<OsuSpriteText>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(5, 0),
Margin = new MarginPadding { Top = header.TextSize }
}
loading = new LoadingAnimation(),
};
}
protected override void PopIn()
{
this.FadeIn(transition_duration, Easing.OutQuint);
loading.State = Visibility.Visible;
}
protected override void PopOut()
{
this.FadeOut(transition_duration, Easing.OutQuint);
loading.State = Visibility.Hidden;
}
}
}
}

View File

@ -0,0 +1,152 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.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.Game.Graphics.UserInterface;
using System;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select.Details
{
public class AdvancedStats : Container
{
private readonly StatisticRow firstValue, hpDrain, accuracy, approachRate, starDifficulty;
private BeatmapInfo beatmap;
public BeatmapInfo Beatmap
{
get { return beatmap; }
set
{
if (value == beatmap) return;
beatmap = value;
//mania specific
if ((Beatmap?.Ruleset?.ID ?? 0) == 3)
{
firstValue.Title = "Key Amount";
firstValue.Value = (int)Math.Round(Beatmap?.Difficulty?.CircleSize ?? 0);
}
else
{
firstValue.Title = "Circle Size";
firstValue.Value = Beatmap?.Difficulty?.CircleSize ?? 0;
}
hpDrain.Value = beatmap.Difficulty.DrainRate;
accuracy.Value = beatmap.Difficulty.OverallDifficulty;
approachRate.Value = beatmap.Difficulty.ApproachRate;
starDifficulty.Value = (float)beatmap.StarDifficulty;
}
}
public AdvancedStats()
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(4f),
Children = new[]
{
firstValue = new StatisticRow(), //circle size/key amount
hpDrain = new StatisticRow { Title = "HP Drain" },
accuracy = new StatisticRow { Title = "Accuracy" },
approachRate = new StatisticRow { Title = "Approach Rate" },
starDifficulty = new StatisticRow(10, true) { Title = "Star Difficulty" },
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
starDifficulty.AccentColour = colours.Yellow;
}
private class StatisticRow : Container, IHasAccentColour
{
private const float value_width = 25;
private const float name_width = 70;
private readonly float maxValue;
private readonly bool forceDecimalPlaces;
private readonly OsuSpriteText name, value;
private readonly Bar bar;
public string Title
{
get { return name.Text; }
set { name.Text = value; }
}
private float difficultyValue;
public float Value
{
get { return difficultyValue; }
set
{
difficultyValue = value;
bar.Length = value / maxValue;
this.value.Text = value.ToString(forceDecimalPlaces ? "0.00" : "0.##");
}
}
public Color4 AccentColour
{
get { return bar.AccentColour; }
set { bar.AccentColour = value; }
}
public StatisticRow(float maxValue = 10, bool forceDecimalPlaces = false)
{
this.maxValue = maxValue;
this.forceDecimalPlaces = forceDecimalPlaces;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Children = new Drawable[]
{
new Container
{
Width = name_width,
AutoSizeAxes = Axes.Y,
Child = name = new OsuSpriteText
{
TextSize = 13,
},
},
bar = new Bar
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Height = 5,
BackgroundColour = Color4.White.Opacity(0.5f),
Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 },
},
new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = value_width,
RelativeSizeAxes = Axes.Y,
Child = value = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
TextSize = 13,
},
},
};
}
}
}
}

View File

@ -0,0 +1,62 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using System.Linq;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select.Details
{
public class FailRetryGraph : Container
{
private readonly BarGraph retryGraph, failGraph;
private BeatmapMetrics metrics;
public BeatmapMetrics Metrics
{
get { return metrics; }
set
{
if (value == metrics) return;
metrics = value;
var retries = Metrics.Retries;
var fails = Metrics.Fails;
float maxValue = fails.Zip(retries, (fail, retry) => fail + retry).Max();
failGraph.MaxValue = maxValue;
retryGraph.MaxValue = maxValue;
failGraph.Values = fails.Select(f => (float)f);
retryGraph.Values = retries.Zip(fails, (retry, fail) => retry + MathHelper.Clamp(fail, 0, maxValue));
}
}
public FailRetryGraph()
{
Children = new[]
{
retryGraph = new BarGraph
{
RelativeSizeAxes = Axes.Both,
},
failGraph = new BarGraph
{
RelativeSizeAxes = Axes.Both,
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
retryGraph.Colour = colours.Yellow;
failGraph.Colour = colours.YellowDarker;
}
}
}

View File

@ -0,0 +1,122 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using System.Linq;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select.Details
{
public class UserRatings : Container
{
private readonly FillFlowContainer header;
private readonly Bar ratingsBar;
private readonly OsuSpriteText negativeRatings, positiveRatings;
private readonly Container graphContainer;
private readonly BarGraph graph;
private BeatmapMetrics metrics;
public BeatmapMetrics Metrics
{
get { return metrics; }
set
{
if (value == metrics) return;
metrics = value;
var ratings = Metrics.Ratings.ToList();
negativeRatings.Text = ratings.GetRange(0, ratings.Count / 2).Sum().ToString();
positiveRatings.Text = ratings.GetRange(ratings.Count / 2, ratings.Count / 2).Sum().ToString();
ratingsBar.Length = (float)ratings.GetRange(0, ratings.Count / 2).Sum() / ratings.Sum();
graph.Values = Metrics.Ratings.Select(r => (float)r);
}
}
public UserRatings()
{
Children = new Drawable[]
{
header = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "User Rating",
TextSize = 13,
},
ratingsBar = new Bar
{
RelativeSizeAxes = Axes.X,
Height = 5,
Margin = new MarginPadding { Top = 5 },
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
negativeRatings = new OsuSpriteText
{
Text = "0",
TextSize = 13,
},
positiveRatings = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Text = @"0",
TextSize = 13,
},
},
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Rating Spread",
TextSize = 13,
Margin = new MarginPadding { Top = 10, Bottom = 5 },
},
},
},
graphContainer = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Child = graph = new BarGraph
{
RelativeSizeAxes = Axes.Both,
},
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
ratingsBar.BackgroundColour = colours.Green;
ratingsBar.AccentColour = colours.Yellow;
graph.Colour = colours.BlueDark;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
graphContainer.Padding = new MarginPadding { Top = header.DrawHeight };
}
}
}

View File

@ -304,7 +304,7 @@ namespace osu.Game.Screens.Select.Leaderboards
BlurSigma = new Vector2(4),
CacheDrawnFrameBuffer = true,
RelativeSizeAxes = Axes.Both,
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Size = new Vector2(3f),
Children = new[]
{

View File

@ -119,7 +119,7 @@ namespace osu.Game.Screens.Select.Options
{
RelativeSizeAxes = Axes.Both,
EdgeSmoothness = new Vector2(1.5f, 0),
BlendingMode = BlendingMode.Additive,
Blending = BlendingMode.Additive,
Colour = Color4.White,
Alpha = 0,
},

View File

@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>osu.Game</RootNamespace>
<AssemblyName>osu.Game</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
@ -98,6 +98,7 @@
<Compile Include="Input\Bindings\GlobalKeyBindingInputManager.cs" />
<Compile Include="IO\FileStore.cs" />
<Compile Include="IO\FileInfo.cs" />
<Compile Include="Online\API\Requests\DownloadBeatmapSetRequest.cs" />
<Compile Include="Online\API\Requests\GetUsersRequest.cs" />
<Compile Include="Online\API\Requests\PostMessageRequest.cs" />
<Compile Include="Online\Chat\ErrorMessage.cs" />
@ -214,7 +215,6 @@
<Compile Include="Rulesets\Objects\Types\IHasYPosition.cs" />
<Compile Include="Rulesets\Replays\Replay.cs" />
<Compile Include="Rulesets\Judgements\DrawableJudgement.cs" />
<Compile Include="Rulesets\Judgements\IPartialJudgement.cs" />
<Compile Include="Rulesets\Replays\FramedReplayInputHandler.cs" />
<Compile Include="Rulesets\Mods\IApplicableMod.cs" />
<Compile Include="Rulesets\Mods\ModType.cs" />
@ -538,6 +538,9 @@
<Compile Include="Screens\Multiplayer\ParticipantInfo.cs" />
<Compile Include="Screens\Multiplayer\ModeTypeInfo.cs" />
<Compile Include="Beatmaps\Drawables\BeatmapSetCover.cs" />
<Compile Include="Screens\Select\Details\AdvancedStats.cs" />
<Compile Include="Screens\Select\Details\FailRetryGraph.cs" />
<Compile Include="Screens\Select\Details\UserRatings.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj">