1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-06 04:33:21 +08:00

Merge branch 'master' into spectator-state-rework

This commit is contained in:
Dan Balasescu 2022-02-08 19:51:50 +09:00
commit 45b3f472ab
24 changed files with 704 additions and 93 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -16,5 +17,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
[JsonProperty("scaled_score")] [JsonProperty("scaled_score")]
public double ScaledScore { get; set; } public double ScaledScore { get; set; }
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
yield return attribute;
yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty);
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
}
} }
} }

View File

@ -366,6 +366,17 @@ namespace osu.Game.Rulesets.Mania
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{ {
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow new StatisticRow
{ {
Columns = new[] Columns = new[]

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -22,5 +23,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("effective_miss_count")] [JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; } public double EffectiveMissCount { get; set; }
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
yield return attribute;
yield return new PerformanceDisplayAttribute(nameof(Aim), "Aim", Aim);
yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed);
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
yield return new PerformanceDisplayAttribute(nameof(Flashlight), "Flashlight Bonus", Flashlight);
}
} }
} }

View File

@ -275,6 +275,17 @@ namespace osu.Game.Rulesets.Osu
return new[] return new[]
{ {
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow new StatisticRow
{ {
Columns = new[] Columns = new[]

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -13,5 +14,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("accuracy")] [JsonProperty("accuracy")]
public double Accuracy { get; set; } public double Accuracy { get; set; }
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
yield return attribute;
yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty);
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
}
} }
} }

View File

@ -209,6 +209,17 @@ namespace osu.Game.Rulesets.Taiko
return new[] return new[]
{ {
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow new StatisticRow
{ {
Columns = new[] Columns = new[]

View File

@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("click to right of panel", () => AddStep("click to right of panel", () =>
{ {
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded); var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0)); InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(50, 0));
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });

View File

@ -0,0 +1,115 @@
// 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 NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays.Settings;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneSafeAreaHandling : OsuGameTestScene
{
private SafeAreaDefiningContainer safeAreaContainer;
private static BindableSafeArea safeArea;
private readonly Bindable<float> safeAreaPaddingTop = new BindableFloat { MinValue = 0, MaxValue = 200 };
private readonly Bindable<float> safeAreaPaddingBottom = new BindableFloat { MinValue = 0, MaxValue = 200 };
private readonly Bindable<float> safeAreaPaddingLeft = new BindableFloat { MinValue = 0, MaxValue = 200 };
private readonly Bindable<float> safeAreaPaddingRight = new BindableFloat { MinValue = 0, MaxValue = 200 };
protected override void LoadComplete()
{
base.LoadComplete();
// Usually this would be placed between the host and the game, but that's a bit of a pain to do with the test scene hierarchy.
// Add is required for the container to get a size (and give out correct metrics to the usages in SafeAreaContainer).
Add(safeAreaContainer = new SafeAreaDefiningContainer(safeArea = new BindableSafeArea())
{
RelativeSizeAxes = Axes.Both
});
// Cache is required for the test game to see the safe area.
Dependencies.CacheAs<ISafeArea>(safeAreaContainer);
}
public override void SetUpSteps()
{
AddStep("Add adjust controls", () =>
{
Add(new Container
{
Depth = float.MinValue,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
Alpha = 0.8f,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Width = 400,
Children = new Drawable[]
{
new SettingsSlider<float>
{
Current = safeAreaPaddingTop,
LabelText = "Top"
},
new SettingsSlider<float>
{
Current = safeAreaPaddingBottom,
LabelText = "Bottom"
},
new SettingsSlider<float>
{
Current = safeAreaPaddingLeft,
LabelText = "Left"
},
new SettingsSlider<float>
{
Current = safeAreaPaddingRight,
LabelText = "Right"
},
}
}
}
});
safeAreaPaddingTop.BindValueChanged(_ => updateSafeArea());
safeAreaPaddingBottom.BindValueChanged(_ => updateSafeArea());
safeAreaPaddingLeft.BindValueChanged(_ => updateSafeArea());
safeAreaPaddingRight.BindValueChanged(_ => updateSafeArea());
});
base.SetUpSteps();
}
private void updateSafeArea()
{
safeArea.Value = new MarginPadding
{
Top = safeAreaPaddingTop.Value,
Bottom = safeAreaPaddingBottom.Value,
Left = safeAreaPaddingLeft.Value,
Right = safeAreaPaddingRight.Value,
};
}
[Test]
public void TestSafeArea()
{
}
}
}

View File

@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers
private Bindable<float> posX; private Bindable<float> posX;
private Bindable<float> posY; private Bindable<float> posY;
private Bindable<MarginPadding> safeAreaPadding;
private readonly ScalingMode? targetMode; private readonly ScalingMode? targetMode;
private Bindable<ScalingMode> scalingMode; private Bindable<ScalingMode> scalingMode;
@ -50,7 +52,7 @@ namespace osu.Game.Graphics.Containers
return; return;
allowScaling = value; allowScaling = value;
if (IsLoaded) updateSize(); if (IsLoaded) Scheduler.AddOnce(updateSize);
} }
} }
@ -102,22 +104,25 @@ namespace osu.Game.Graphics.Containers
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config, ISafeArea safeArea)
{ {
scalingMode = config.GetBindable<ScalingMode>(OsuSetting.Scaling); scalingMode = config.GetBindable<ScalingMode>(OsuSetting.Scaling);
scalingMode.ValueChanged += _ => updateSize(); scalingMode.ValueChanged += _ => Scheduler.AddOnce(updateSize);
sizeX = config.GetBindable<float>(OsuSetting.ScalingSizeX); sizeX = config.GetBindable<float>(OsuSetting.ScalingSizeX);
sizeX.ValueChanged += _ => updateSize(); sizeX.ValueChanged += _ => Scheduler.AddOnce(updateSize);
sizeY = config.GetBindable<float>(OsuSetting.ScalingSizeY); sizeY = config.GetBindable<float>(OsuSetting.ScalingSizeY);
sizeY.ValueChanged += _ => updateSize(); sizeY.ValueChanged += _ => Scheduler.AddOnce(updateSize);
posX = config.GetBindable<float>(OsuSetting.ScalingPositionX); posX = config.GetBindable<float>(OsuSetting.ScalingPositionX);
posX.ValueChanged += _ => updateSize(); posX.ValueChanged += _ => Scheduler.AddOnce(updateSize);
posY = config.GetBindable<float>(OsuSetting.ScalingPositionY); posY = config.GetBindable<float>(OsuSetting.ScalingPositionY);
posY.ValueChanged += _ => updateSize(); posY.ValueChanged += _ => Scheduler.AddOnce(updateSize);
safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy();
safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -161,7 +166,10 @@ namespace osu.Game.Graphics.Containers
var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One;
var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero; var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero;
bool requiresMasking = scaling && targetSize != Vector2.One; bool requiresMasking = (scaling && targetSize != Vector2.One)
// For the top level scaling container, for now we apply masking if safe areas are in use.
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
|| (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero);
if (requiresMasking) if (requiresMasking)
sizableContainer.Masking = true; sizableContainer.Masking = true;

View File

@ -89,6 +89,12 @@ namespace osu.Game
} }
} }
/// <summary>
/// The <see cref="Edges"/> that the game should be drawn over at a top level.
/// Defaults to <see cref="Edges.None"/>.
/// </summary>
protected virtual Edges SafeAreaOverrideEdges => Edges.None;
protected OsuConfigManager LocalConfig { get; private set; } protected OsuConfigManager LocalConfig { get; private set; }
protected SessionStatics SessionStatics { get; private set; } protected SessionStatics SessionStatics { get; private set; }
@ -299,16 +305,23 @@ namespace osu.Game
GlobalActionContainer globalBindings; GlobalActionContainer globalBindings;
var mainContent = new Drawable[] base.Content.Add(new SafeAreaContainer
{ {
MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }, SafeAreaOverrideEdges = SafeAreaOverrideEdges,
RelativeSizeAxes = Axes.Both,
Child = CreateScalingContainer().WithChildren(new Drawable[]
{
(MenuCursorContainer = new MenuCursorContainer
{
RelativeSizeAxes = Axes.Both
}).WithChild(content = new OsuTooltipContainer(MenuCursorContainer.Cursor)
{
RelativeSizeAxes = Axes.Both
}),
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything.
globalBindings = new GlobalActionContainer(this) globalBindings = new GlobalActionContainer(this)
}; })
});
MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both };
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Rulesets.Difficulty namespace osu.Game.Rulesets.Difficulty
@ -12,5 +13,15 @@ namespace osu.Game.Rulesets.Difficulty
/// </summary> /// </summary>
[JsonProperty("pp")] [JsonProperty("pp")]
public double Total { get; set; } public double Total { get; set; }
/// <summary>
/// Return a <see cref="PerformanceDisplayAttribute"/> for each attribute so that a performance breakdown can be displayed.
/// Some attributes may be omitted if they are not meant for display.
/// </summary>
/// <returns></returns>
public virtual IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{
yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total);
}
} }
} }

View File

@ -0,0 +1,21 @@
// 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.Rulesets.Difficulty
{
/// <summary>
/// Data for generating a performance breakdown by comparing performance to a perfect play.
/// </summary>
public class PerformanceBreakdown
{
/// <summary>
/// Actual gameplay performance.
/// </summary>
public PerformanceAttributes Performance { get; set; }
/// <summary>
/// Performance of a perfect play for comparison.
/// </summary>
public PerformanceAttributes PerfectPerformance { get; set; }
}
}

View File

@ -0,0 +1,105 @@
// 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 System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Difficulty
{
public class PerformanceBreakdownCalculator
{
private readonly IBeatmap playableBeatmap;
private readonly BeatmapDifficultyCache difficultyCache;
private readonly ScorePerformanceCache performanceCache;
public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache)
{
this.playableBeatmap = playableBeatmap;
this.difficultyCache = difficultyCache;
this.performanceCache = performanceCache;
}
[ItemCanBeNull]
public async Task<PerformanceBreakdown> CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default)
{
PerformanceAttributes[] performanceArray = await Task.WhenAll(
// compute actual performance
performanceCache.CalculatePerformanceAsync(score, cancellationToken),
// compute performance for perfect play
getPerfectPerformance(score, cancellationToken)
).ConfigureAwait(false);
return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] };
}
[ItemCanBeNull]
private Task<PerformanceAttributes> getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default)
{
return Task.Run(async () =>
{
Ruleset ruleset = score.Ruleset.CreateInstance();
ScoreInfo perfectPlay = score.DeepClone();
perfectPlay.Accuracy = 1;
perfectPlay.Passed = true;
// calculate max combo
// todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores
perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap);
// create statistics assuming all hit objects have perfect hit result
var statistics = playableBeatmap.HitObjects
.SelectMany(getPerfectHitResults)
.GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count()))
.ToDictionary(pair => pair.hitResult, pair => pair.count);
perfectPlay.Statistics = statistics;
// calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo;
scoreProcessor.Mods.Value = perfectPlay.Mods;
perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics);
// compute rank achieved
// default to SS, then adjust the rank with mods
perfectPlay.Rank = ScoreRank.X;
foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType<IApplicableToScoreProcessor>())
{
perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1);
}
// calculate performance for this perfect score
var difficulty = await difficultyCache.GetDifficultyAsync(
playableBeatmap.BeatmapInfo,
score.Ruleset,
score.Mods,
cancellationToken
).ConfigureAwait(false);
// ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes
return difficulty == null ? null : ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate();
}, cancellationToken);
}
private int calculateMaxCombo(IBeatmap beatmap)
{
return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo());
}
private IEnumerable<HitResult> getPerfectHitResults(HitObject hitObject)
{
foreach (HitObject nested in hitObject.NestedHitObjects)
yield return nested.CreateJudgement().MaxResult;
yield return hitObject.CreateJudgement().MaxResult;
}
}
}

View File

@ -0,0 +1,33 @@
// 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.Rulesets.Difficulty
{
/// <summary>
/// Data for displaying a performance attribute to user. Includes a display name for clarity.
/// </summary>
public class PerformanceDisplayAttribute
{
/// <summary>
/// Name of the attribute property in <see cref="PerformanceAttributes"/>.
/// </summary>
public string PropertyName { get; }
/// <summary>
/// A custom display name for the attribute.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// The associated attribute value.
/// </summary>
public double Value { get; }
public PerformanceDisplayAttribute(string propertyName, string displayName, double value)
{
PropertyName = propertyName;
DisplayName = displayName;
Value = value;
}
}
}

View File

@ -8,6 +8,7 @@ using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
@ -15,7 +16,7 @@ namespace osu.Game.Scoring
/// A component which performs and acts as a central cache for performance calculations of locally databased scores. /// A component which performs and acts as a central cache for performance calculations of locally databased scores.
/// Currently not persisted between game sessions. /// Currently not persisted between game sessions.
/// </summary> /// </summary>
public class ScorePerformanceCache : MemoryCachingComponent<ScorePerformanceCache.PerformanceCacheLookup, double?> public class ScorePerformanceCache : MemoryCachingComponent<ScorePerformanceCache.PerformanceCacheLookup, PerformanceAttributes>
{ {
[Resolved] [Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } private BeatmapDifficultyCache difficultyCache { get; set; }
@ -27,10 +28,10 @@ namespace osu.Game.Scoring
/// </summary> /// </summary>
/// <param name="score">The score to do the calculation on. </param> /// <param name="score">The score to do the calculation on. </param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param> /// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
public Task<double?> CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => public Task<PerformanceAttributes> CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) =>
GetAsync(new PerformanceCacheLookup(score), token); GetAsync(new PerformanceCacheLookup(score), token);
protected override async Task<double?> ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) protected override async Task<PerformanceAttributes> ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
{ {
var score = lookup.ScoreInfo; var score = lookup.ScoreInfo;
@ -44,7 +45,7 @@ namespace osu.Game.Scoring
var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score); var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score);
return calculator?.Calculate().Total; return calculator?.Calculate();
} }
public readonly struct PerformanceCacheLookup public readonly struct PerformanceCacheLookup

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -231,7 +230,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public override bool OnBackButton() public override bool OnBackButton()
{ {
Debug.Assert(multiplayerClient.Room != null); if (multiplayerClient.Room == null)
return base.OnBackButton();
// On a manual exit, set the player back to idle unless gameplay has finished. // On a manual exit, set the player back to idle unless gameplay has finished.
if (multiplayerClient.Room.State != MultiplayerRoomState.Open) if (multiplayerClient.Room.State != MultiplayerRoomState.Open)

View File

@ -38,7 +38,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
else else
{ {
performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token)
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token); .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely().Total)), cancellationTokenSource.Token);
} }
} }

View File

@ -0,0 +1,247 @@
// 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 System.Threading;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Ranking.Statistics
{
public class PerformanceBreakdownChart : Container
{
private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;
private Drawable spinner;
private Drawable content;
private GridContainer chart;
private OsuSpriteText achievedPerformance;
private OsuSpriteText maximumPerformance;
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
[Resolved]
private ScorePerformanceCache performanceCache { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap)
{
this.score = score;
this.playableBeatmap = playableBeatmap;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
{
spinner = new LoadingSpinner(true)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre
},
content = new FillFlowContainer
{
Alpha = 0,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.6f,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Spacing = new Vector2(15, 15),
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
Width = 0.8f,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
new OsuSpriteText
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
Text = "Achieved PP",
Colour = Color4Extensions.FromHex("#66FFCC")
},
achievedPerformance = new OsuSpriteText
{
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18),
Colour = Color4Extensions.FromHex("#66FFCC")
}
},
new Drawable[]
{
new OsuSpriteText
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
Text = "Maximum",
Colour = OsuColour.Gray(0.7f)
},
maximumPerformance = new OsuSpriteText
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
Colour = OsuColour.Gray(0.7f)
}
}
}
},
chart = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
}
}
}
}
};
spinner.Show();
new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache, performanceCache)
.CalculateAsync(score, cancellationTokenSource.Token)
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())));
}
private void setPerformanceValue(PerformanceBreakdown breakdown)
{
spinner.Hide();
content.FadeIn(200);
var displayAttributes = breakdown.Performance.GetAttributesForDisplay();
var perfectDisplayAttributes = breakdown.PerfectPerformance.GetAttributesForDisplay();
setTotalValues(
displayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total)),
perfectDisplayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total))
);
var rowDimensions = new List<Dimension>();
var rows = new List<Drawable[]>();
foreach (PerformanceDisplayAttribute attr in displayAttributes)
{
if (attr.PropertyName == nameof(PerformanceAttributes.Total)) continue;
var row = createAttributeRow(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName));
if (row != null)
{
rows.Add(row);
rowDimensions.Add(new Dimension(GridSizeMode.AutoSize));
}
}
chart.RowDimensions = rowDimensions.ToArray();
chart.Content = rows.ToArray();
}
private void setTotalValues(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute)
{
achievedPerformance.Text = Math.Round(attribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString();
maximumPerformance.Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString();
}
[CanBeNull]
private Drawable[] createAttributeRow(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute)
{
// Don't display the attribute if its maximum is 0
// For example, flashlight bonus would be zero if flashlight mod isn't on
if (Precision.AlmostEquals(perfectAttribute.Value, 0f))
return null;
float percentage = (float)(attribute.Value / perfectAttribute.Value);
return new Drawable[]
{
new OsuSpriteText
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = attribute.DisplayName,
Colour = Colour4.White
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 10, Right = 10 },
Child = new Bar
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
CornerRadius = 2.5f,
Masking = true,
Height = 5,
BackgroundColour = Color4.White.Opacity(0.5f),
AccentColour = Color4Extensions.FromHex("#66FFCC"),
Length = percentage
}
},
new OsuSpriteText
{
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
Text = percentage.ToLocalisableString("0%"),
Colour = Colour4.White
}
};
}
protected override void Dispose(bool isDisposing)
{
cancellationTokenSource?.Cancel();
base.Dispose(isDisposing);
}
}
}

View File

@ -925,8 +925,10 @@ namespace osu.Game.Screens.Select
// child items (difficulties) are still visible. // child items (difficulties) are still visible.
item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0); item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0);
// We are applying alpha to the header here such that we can layer alpha transformations on top. // We are applying a multiplicative alpha (which is internally done by nesting an
item.Header.Alpha = Math.Clamp(1.75f - 1.5f * dist, 0, 1); // additional container and setting that container's alpha) such that we can
// layer alpha transformations on top.
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
} }
private enum PendingScrollOperation private enum PendingScrollOperation

View File

@ -21,6 +21,8 @@ namespace osu.Game.Screens.Select.Carousel
{ {
public class CarouselHeader : Container public class CarouselHeader : Container
{ {
public Container BorderContainer;
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected); public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
private readonly HoverLayer hoverLayer; private readonly HoverLayer hoverLayer;
@ -35,14 +37,17 @@ namespace osu.Game.Screens.Select.Carousel
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = DrawableCarouselItem.MAX_HEIGHT; Height = DrawableCarouselItem.MAX_HEIGHT;
Masking = true; InternalChild = BorderContainer = new Container
CornerRadius = corner_radius; {
BorderColour = new Color4(221, 255, 255, 255); RelativeSizeAxes = Axes.Both,
Masking = true,
InternalChildren = new Drawable[] CornerRadius = corner_radius,
BorderColour = new Color4(221, 255, 255, 255),
Children = new Drawable[]
{ {
Content, Content,
hoverLayer = new HoverLayer() hoverLayer = new HoverLayer()
}
}; };
} }
@ -61,21 +66,21 @@ namespace osu.Game.Screens.Select.Carousel
case CarouselItemState.NotSelected: case CarouselItemState.NotSelected:
hoverLayer.InsetForBorder = false; hoverLayer.InsetForBorder = false;
BorderThickness = 0; BorderContainer.BorderThickness = 0;
EdgeEffect = new EdgeEffectParameters BorderContainer.EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,
Offset = new Vector2(1), Offset = new Vector2(1),
Radius = 10, Radius = 10,
Colour = Color4.Black.Opacity(0.5f), Colour = Color4.Black.Opacity(100),
}; };
break; break;
case CarouselItemState.Selected: case CarouselItemState.Selected:
hoverLayer.InsetForBorder = true; hoverLayer.InsetForBorder = true;
BorderThickness = border_thickness; BorderContainer.BorderThickness = border_thickness;
EdgeEffect = new EdgeEffectParameters BorderContainer.EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Colour = new Color4(130, 204, 255, 150), Colour = new Color4(130, 204, 255, 150),

View File

@ -36,9 +36,9 @@ namespace osu.Game.Screens.Select.Carousel
/// <summary> /// <summary>
/// The height of a carousel beatmap, including vertical spacing. /// The height of a carousel beatmap, including vertical spacing.
/// </summary> /// </summary>
public const float HEIGHT = header_height + CAROUSEL_BEATMAP_SPACING; public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING;
private const float header_height = MAX_HEIGHT * 0.6f; private const float height = MAX_HEIGHT * 0.6f;
private readonly BeatmapInfo beatmapInfo; private readonly BeatmapInfo beatmapInfo;
@ -67,18 +67,16 @@ namespace osu.Game.Screens.Select.Carousel
private CancellationTokenSource starDifficultyCancellationSource; private CancellationTokenSource starDifficultyCancellationSource;
public DrawableCarouselBeatmap(CarouselBeatmap panel) public DrawableCarouselBeatmap(CarouselBeatmap panel)
: base(header_height)
{ {
beatmapInfo = panel.BeatmapInfo; beatmapInfo = panel.BeatmapInfo;
Item = panel; Item = panel;
// Difficulty panels should start hidden for a better initial effect.
Hide();
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(BeatmapManager manager, SongSelect songSelect) private void load(BeatmapManager manager, SongSelect songSelect)
{ {
Header.Height = height;
if (songSelect != null) if (songSelect != null)
{ {
startRequested = b => songSelect.FinaliseSelection(b); startRequested = b => songSelect.FinaliseSelection(b);

View File

@ -122,10 +122,12 @@ namespace osu.Game.Screens.Select.Carousel
}, },
}; };
background.DelayedLoadComplete += d => d.FadeInFromZero(750, Easing.OutQuint); background.DelayedLoadComplete += fadeContentIn;
mainFlow.DelayedLoadComplete += d => d.FadeInFromZero(500, Easing.OutQuint); mainFlow.DelayedLoadComplete += fadeContentIn;
} }
private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint);
protected override void Deselected() protected override void Deselected()
{ {
base.Deselected(); base.Deselected();

View File

@ -60,10 +60,12 @@ namespace osu.Game.Screens.Select.Carousel
} }
} }
protected DrawableCarouselItem(float headerHeight = MAX_HEIGHT) protected DrawableCarouselItem()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Alpha = 0;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
MovementContainer = new Container MovementContainer = new Container
@ -71,20 +73,18 @@ namespace osu.Game.Screens.Select.Carousel
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
Header = new CarouselHeader Header = new CarouselHeader(),
{
Height = headerHeight,
},
Content = new Container Content = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Y = headerHeight,
} }
} }
}, },
}; };
} }
public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -92,6 +92,12 @@ namespace osu.Game.Screens.Select.Carousel
UpdateItem(); UpdateItem();
} }
protected override void Update()
{
base.Update();
Content.Y = Header.Height;
}
protected virtual void UpdateItem() protected virtual void UpdateItem()
{ {
if (item == null) if (item == null)
@ -115,21 +121,15 @@ namespace osu.Game.Screens.Select.Carousel
private void onStateChange(ValueChangedEvent<bool> _) => Scheduler.AddOnce(ApplyState); private void onStateChange(ValueChangedEvent<bool> _) => Scheduler.AddOnce(ApplyState);
private CarouselItemState? lastAppliedState;
protected virtual void ApplyState() protected virtual void ApplyState()
{ {
Debug.Assert(Item != null);
if (lastAppliedState != Item.State.Value)
{
lastAppliedState = Item.State.Value;
// Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead. // Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead.
// Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away. // Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away.
Height = Item.TotalHeight; Height = Item.TotalHeight;
switch (lastAppliedState) Debug.Assert(Item != null);
switch (Item.State.Value)
{ {
case CarouselItemState.NotSelected: case CarouselItemState.NotSelected:
Deselected(); Deselected();
@ -139,32 +139,11 @@ namespace osu.Game.Screens.Select.Carousel
Selected(); Selected();
break; break;
} }
}
if (!Item.Visible) if (!Item.Visible)
Hide();
else
Show();
}
private bool isVisible = true;
public override void Show()
{
if (isVisible)
return;
isVisible = true;
this.FadeIn(250);
}
public override void Hide()
{
if (!isVisible)
return;
isVisible = false;
this.FadeOut(300, Easing.OutQuint); this.FadeOut(300, Easing.OutQuint);
else
this.FadeIn(250);
} }
protected virtual void Selected() protected virtual void Selected()

View File

@ -3,6 +3,7 @@
using System; using System;
using Foundation; using Foundation;
using osu.Framework.Graphics;
using osu.Game; using osu.Game;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
@ -18,6 +19,11 @@ namespace osu.iOS
protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();
protected override Edges SafeAreaOverrideEdges =>
// iOS shows a home indicator at the bottom, and adds a safe area to account for this.
// Because we have the home indicator (mostly) hidden we don't really care about drawing in this region.
Edges.Bottom;
private class IOSBatteryInfo : BatteryInfo private class IOSBatteryInfo : BatteryInfo
{ {
public override double ChargeLevel => Battery.ChargeLevel; public override double ChargeLevel => Battery.ChargeLevel;