From 511a607599215e8981321e16df99c4a32918f67c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 18:28:17 +0800 Subject: [PATCH 001/136] Display performance breakdown in a tooltip --- .../Difficulty/ManiaPerformanceAttributes.cs | 11 +++ .../Difficulty/OsuPerformanceAttributes.cs | 13 +++ .../Difficulty/TaikoPerformanceAttributes.cs | 10 +++ .../Difficulty/PerformanceAttributes.cs | 10 +++ .../Difficulty/PerformanceDisplayAttribute.cs | 26 ++++++ osu.Game/Scoring/ScorePerformanceCache.cs | 9 ++- .../Statistics/PerformanceStatistic.cs | 30 ++++--- .../Statistics/PerformanceStatisticTooltip.cs | 80 +++++++++++++++++++ 8 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index da9634ba47..0d3a53f3f3 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -16,5 +17,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty [JsonProperty("scaled_score")] public double ScaledScore { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute("Scaled Score", ScaledScore); + } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 6c7760d144..db7ca6af88 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -22,5 +23,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute("Aim", Aim); + yield return new PerformanceDisplayAttribute("Speed", Speed); + yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute("Flashlight Bonus", Flashlight); + yield return new PerformanceDisplayAttribute("Effective Miss Count", EffectiveMissCount); + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 80552880ea..efbcdd3703 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -13,5 +14,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("accuracy")] public double Accuracy { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 025b38257c..98c6c75f6c 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; namespace osu.Game.Rulesets.Difficulty @@ -12,5 +13,14 @@ namespace osu.Game.Rulesets.Difficulty /// [JsonProperty("pp")] public double Total { get; set; } + + /// + /// Return a for each attribute so that a performance breakdown can be displayed. + /// + /// + public virtual IEnumerable GetAttributesForDisplay() + { + yield return new PerformanceDisplayAttribute("Total", Total); + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs new file mode 100644 index 0000000000..5bb505847e --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Data for displaying a performance attribute to user. Includes a display name for clarity. + /// + public class PerformanceDisplayAttribute + { + /// + /// A custom display name for the attribute. + /// + public string DisplayName { get; } + + /// + /// The associated attribute value. + /// + public double Value { get; } + + public PerformanceDisplayAttribute(string displayName, double value) + { + DisplayName = displayName; + Value = value; + } + } +} diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index b855343505..a428a66aae 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Rulesets.Difficulty; 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. /// Currently not persisted between game sessions. /// - public class ScorePerformanceCache : MemoryCachingComponent + public class ScorePerformanceCache : MemoryCachingComponent { [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } @@ -27,10 +28,10 @@ namespace osu.Game.Scoring /// /// The score to do the calculation on. /// An optional to cancel the operation. - public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => + public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => GetAsync(new PerformanceCacheLookup(score), token); - protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) + protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) { var score = lookup.ScoreInfo; @@ -44,7 +45,7 @@ namespace osu.Game.Scoring var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score); - return calculator?.Calculate().Total; + return calculator?.Calculate(); } public readonly struct PerformanceCacheLookup diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index d6e4cfbe51..888552e568 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -7,12 +7,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Difficulty; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay + public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip { private readonly ScoreInfo score; @@ -22,6 +24,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private RollingCounter counter; + private PerformanceAttributes attributes; + public PerformanceStatistic(ScoreInfo score) : base("PP") { @@ -31,21 +35,17 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics [BackgroundDependencyLoader] private void load(ScorePerformanceCache performanceCache) { - if (score.PP.HasValue) - { - setPerformanceValue(score.PP.Value); - } - else - { - performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token); - } + performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token); } - private void setPerformanceValue(double? pp) + private void setPerformanceValue(PerformanceAttributes pp) { - if (pp.HasValue) - performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); + if (pp != null) + { + attributes = pp; + performance.Value = (int)Math.Round(pp.Total, MidpointRounding.AwayFromZero); + } } public override void Appear() @@ -65,5 +65,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }; + + public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); + + public PerformanceAttributes TooltipContent => attributes; } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs new file mode 100644 index 0000000000..32ba231e42 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Difficulty; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip + { + private readonly Box background; + + protected override Container Content { get; } + + public PerformanceStatisticTooltip() + { + AutoSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + Content = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Gray3; + } + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + + private PerformanceAttributes lastAttributes; + + public void SetContent(PerformanceAttributes attributes) + { + if (attributes == lastAttributes) + return; + + lastAttributes = attributes; + + UpdateDisplay(attributes); + } + + protected virtual void UpdateDisplay(PerformanceAttributes attributes) + { + Content.Clear(); + + foreach (PerformanceDisplayAttribute attr in attributes.GetAttributesForDisplay()) + { + Content.Add(new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = $"{attr.DisplayName}: {(int)Math.Round(attr.Value, MidpointRounding.AwayFromZero)}" + }); + } + } + + public void Move(Vector2 pos) => Position = pos; + } +} From 85c60bfc2d50b1c5b27348c7a8e821fbeca3b340 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 20:36:18 +0800 Subject: [PATCH 002/136] Improve tooltip design --- .../Statistics/PerformanceStatisticTooltip.cs | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 32ba231e42..618a94a309 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,6 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; + private Colour4 textColor; protected override Container Content { get; } @@ -44,6 +46,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private void load(OsuColour colours) { background.Colour = colours.Gray3; + textColor = colours.BlueLighter; } protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); @@ -67,10 +70,35 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics foreach (PerformanceDisplayAttribute attr in attributes.GetAttributesForDisplay()) { - Content.Add(new OsuSpriteText + Content.Add(new GridContainer { - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = $"{attr.DisplayName}: {(int)Math.Round(attr.Value, MidpointRounding.AwayFromZero)}" + AutoSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 140), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = attr.DisplayName, + Colour = textColor + }, + new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = ((int)Math.Round(attr.Value, MidpointRounding.AwayFromZero)).ToString(CultureInfo.CurrentCulture) + } + } + } }); } } From d014fef179a9248afc4f4b09237fa84fb16beeaf Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 20:36:36 +0800 Subject: [PATCH 003/136] Hide confusing attributes --- osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs | 1 - osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs | 1 - osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index 0d3a53f3f3..48895cd389 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); - yield return new PerformanceDisplayAttribute("Scaled Score", ScaledScore); } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index db7ca6af88..0a685b7cd6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return new PerformanceDisplayAttribute("Speed", Speed); yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); yield return new PerformanceDisplayAttribute("Flashlight Bonus", Flashlight); - yield return new PerformanceDisplayAttribute("Effective Miss Count", EffectiveMissCount); } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 98c6c75f6c..ee1868ecff 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Difficulty /// /// Return a for each attribute so that a performance breakdown can be displayed. + /// Some attributes may be omitted if they are not meant for display. /// /// public virtual IEnumerable GetAttributesForDisplay() From b81fc675e89b6df6161e18a45f6ae392fdbd183a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 20:45:25 +0800 Subject: [PATCH 004/136] Include PropertyName in PerformanceDisplayAttribute --- .../Difficulty/ManiaPerformanceAttributes.cs | 4 ++-- .../Difficulty/OsuPerformanceAttributes.cs | 8 ++++---- .../Difficulty/TaikoPerformanceAttributes.cs | 4 ++-- osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs | 2 +- .../Rulesets/Difficulty/PerformanceDisplayAttribute.cs | 8 +++++++- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index 48895cd389..17c864a268 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -23,8 +23,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty foreach (var attribute in base.GetAttributesForDisplay()) yield return attribute; - yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); - yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 0a685b7cd6..0aeaf7669f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -29,10 +29,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty foreach (var attribute in base.GetAttributesForDisplay()) yield return attribute; - yield return new PerformanceDisplayAttribute("Aim", Aim); - yield return new PerformanceDisplayAttribute("Speed", Speed); - yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); - yield return new PerformanceDisplayAttribute("Flashlight Bonus", Flashlight); + 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); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index efbcdd3703..fa5c0202dd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty foreach (var attribute in base.GetAttributesForDisplay()) yield return attribute; - yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); - yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index ee1868ecff..9bdb5f8f6f 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Difficulty /// public virtual IEnumerable GetAttributesForDisplay() { - yield return new PerformanceDisplayAttribute("Total", Total); + yield return new PerformanceDisplayAttribute(nameof(Total), "Total", Total); } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs index 5bb505847e..e95cb03053 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -7,6 +7,11 @@ namespace osu.Game.Rulesets.Difficulty /// public class PerformanceDisplayAttribute { + /// + /// Name of the attribute property in . + /// + public string PropertyName { get; } + /// /// A custom display name for the attribute. /// @@ -17,8 +22,9 @@ namespace osu.Game.Rulesets.Difficulty /// public double Value { get; } - public PerformanceDisplayAttribute(string displayName, double value) + public PerformanceDisplayAttribute(string propertyName, string displayName, double value) { + PropertyName = propertyName; DisplayName = displayName; Value = value; } From c49cd60487584fd8c2d6e208bc9158942e6ce519 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 21:26:55 +0800 Subject: [PATCH 005/136] Add bar chart to tooltip --- .../Statistics/PerformanceStatisticTooltip.cs | 108 ++++++++++++------ 1 file changed, 74 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 618a94a309..ab7156e0c5 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -2,22 +2,27 @@ // See the LICENCE file in the repository root for full licence text. using System; using System.Globalization; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Difficulty; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Statistics { public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; - private Colour4 textColor; + private Colour4 totalColour; + private Colour4 textColour; protected override Container Content { get; } @@ -46,10 +51,16 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private void load(OsuColour colours) { background.Colour = colours.Gray3; - textColor = colours.BlueLighter; + totalColour = colours.Blue; + textColour = colours.BlueLighter; + } + + protected override void PopIn() + { + if (lastAttributes.GetAttributesForDisplay().Count() > 1) + this.FadeIn(200, Easing.OutQuint); } - protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); private PerformanceAttributes lastAttributes; @@ -64,42 +75,71 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics UpdateDisplay(attributes); } + private Drawable createAttributeItem(PerformanceDisplayAttribute attribute, double attributeSum) + { + bool isTotal = attribute.PropertyName == nameof(PerformanceAttributes.Total); + return new GridContainer + { + AutoSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 110), + new Dimension(GridSizeMode.Absolute, 140), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = attribute.DisplayName, + Colour = isTotal ? totalColour : textColour + }, + new Bar + { + Alpha = isTotal ? 0 : 1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Width = 130, + Height = 5, + BackgroundColour = Color4.White.Opacity(0.5f), + Length = (float)(attribute.Value / attributeSum), + Margin = new MarginPadding { Left = 5, Right = 5 } + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = ((int)Math.Round(attribute.Value, MidpointRounding.AwayFromZero)).ToString(CultureInfo.CurrentCulture), + Colour = isTotal ? totalColour : textColour + } + } + } + }; + } + protected virtual void UpdateDisplay(PerformanceAttributes attributes) { Content.Clear(); - foreach (PerformanceDisplayAttribute attr in attributes.GetAttributesForDisplay()) + var displayAttributes = attributes.GetAttributesForDisplay(); + + double attributeSum = displayAttributes + .Where(attr => attr.PropertyName != nameof(PerformanceAttributes.Total)) + .Sum(attr => attr.Value); + + foreach (PerformanceDisplayAttribute attr in displayAttributes) { - Content.Add(new GridContainer - { - AutoSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 140), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = attr.DisplayName, - Colour = textColor - }, - new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = ((int)Math.Round(attr.Value, MidpointRounding.AwayFromZero)).ToString(CultureInfo.CurrentCulture) - } - } - } - }); + Content.Add(createAttributeItem(attr, attributeSum)); } } From eddf45329445f8b43cb37b65cbeaa3d3bce0f332 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 21:32:13 +0800 Subject: [PATCH 006/136] Fix code quality issues --- osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs | 1 + .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 6 ++---- .../Expanded/Statistics/PerformanceStatisticTooltip.cs | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs index e95cb03053..7958bc174e 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -1,5 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. + namespace osu.Game.Rulesets.Difficulty { /// diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 888552e568..4fd6964a68 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -24,8 +24,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private RollingCounter counter; - private PerformanceAttributes attributes; - public PerformanceStatistic(ScoreInfo score) : base("PP") { @@ -43,7 +41,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { if (pp != null) { - attributes = pp; + TooltipContent = pp; performance.Value = (int)Math.Round(pp.Total, MidpointRounding.AwayFromZero); } } @@ -68,6 +66,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); - public PerformanceAttributes TooltipContent => attributes; + public PerformanceAttributes TooltipContent { get; private set; } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index ab7156e0c5..e0bbf91381 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -1,5 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. + using System; using System.Globalization; using System.Linq; From 83387cb00bcb89b9974b8bcc223b56f78cb72f3e Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 21:41:17 +0800 Subject: [PATCH 007/136] Add a comment --- .../Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index e0bbf91381..deef30124c 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -58,6 +58,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override void PopIn() { + // Don't display the tooltip if "Total" is the only item if (lastAttributes.GetAttributesForDisplay().Count() > 1) this.FadeIn(200, Easing.OutQuint); } From 43e5bd731cbfa0fff4d2a0825e10ceaefb56d599 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 18 Jan 2022 21:57:12 +0800 Subject: [PATCH 008/136] Compare performance to a perfect play --- .../Statistics/PerformanceStatistic.cs | 97 +++++++++++++++++-- .../Statistics/PerformanceStatisticTooltip.cs | 38 ++++---- 2 files changed, 107 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 4fd6964a68..493f6a2dc9 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -2,19 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip + public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip { private readonly ScoreInfo score; @@ -24,6 +29,15 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private RollingCounter counter; + [Resolved] + private ScorePerformanceCache performanceCache { get; set; } + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + public PerformanceStatistic(ScoreInfo score) : base("PP") { @@ -31,18 +45,74 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } [BackgroundDependencyLoader] - private void load(ScorePerformanceCache performanceCache) + private void load() { - performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token); + Task.WhenAll( + // actual performance + performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token), + // performance for a perfect play + getPerfectPerformance(score) + ).ContinueWith(attr => + { + PerformanceAttributes[] result = attr.GetResultSafely(); + setPerformanceValue(new PerformanceBreakdown { Performance = result[0], PerfectPerformance = result[1] }); + }); } - private void setPerformanceValue(PerformanceAttributes pp) + private async Task getPerfectPerformance(ScoreInfo originalScore) { - if (pp != null) + ScoreInfo perfectScore = await getPerfectScore(originalScore).ConfigureAwait(false); + return await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationTokenSource.Token).ConfigureAwait(false); + } + + private Task getPerfectScore(ScoreInfo originalScore) + { + return Task.Factory.StartNew(() => { - TooltipContent = pp; - performance.Value = (int)Math.Round(pp.Total, MidpointRounding.AwayFromZero); + var beatmap = beatmapManager.GetWorkingBeatmap(originalScore.BeatmapInfo).GetPlayableBeatmap(originalScore.Ruleset, originalScore.Mods); + ScoreInfo perfectPlay = originalScore.DeepClone(); + perfectPlay.Accuracy = 1; + perfectPlay.Passed = true; + + // create statistics assuming all hit objects have perfect hit result + var statistics = beatmap.HitObjects + .Select(ho => ho.CreateJudgement().MaxResult) + .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) + .ToDictionary(pair => pair.hitResult, pair => pair.count); + perfectPlay.Statistics = statistics; + + // calculate max combo + var difficulty = difficultyCache.GetDifficultyAsync( + beatmap.BeatmapInfo, + originalScore.Ruleset, + originalScore.Mods, + cancellationTokenSource.Token + ).GetResultSafely(); + perfectPlay.MaxCombo = difficulty?.MaxCombo ?? originalScore.MaxCombo; + + // calculate total score + ScoreProcessor scoreProcessor = originalScore.Ruleset.CreateInstance().CreateScoreProcessor(); + 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()) + { + perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); + } + + return perfectPlay; + }, cancellationTokenSource.Token); + } + + private void setPerformanceValue(PerformanceBreakdown breakdown) + { + if (breakdown != null) + { + TooltipContent = breakdown; + performance.Value = (int)Math.Round(breakdown.Performance.Total, MidpointRounding.AwayFromZero); } } @@ -64,8 +134,15 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Origin = Anchor.TopCentre }; - public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); + public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); - public PerformanceAttributes TooltipContent { get; private set; } + public PerformanceBreakdown TooltipContent { get; private set; } + + public class PerformanceBreakdown + { + public PerformanceAttributes Performance { get; set; } + + public PerformanceAttributes PerfectPerformance { get; set; } + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index deef30124c..564c195a3d 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Globalization; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -19,7 +18,9 @@ using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip + using static PerformanceStatistic; + + public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; private Colour4 totalColour; @@ -59,27 +60,30 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override void PopIn() { // Don't display the tooltip if "Total" is the only item - if (lastAttributes.GetAttributesForDisplay().Count() > 1) + if (currentPerformance.Performance.GetAttributesForDisplay().Count() > 1) this.FadeIn(200, Easing.OutQuint); } protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); - private PerformanceAttributes lastAttributes; + private PerformanceBreakdown currentPerformance; - public void SetContent(PerformanceAttributes attributes) + public void SetContent(PerformanceBreakdown performance) { - if (attributes == lastAttributes) + if (performance == currentPerformance) return; - lastAttributes = attributes; + currentPerformance = performance; - UpdateDisplay(attributes); + UpdateDisplay(performance); } - private Drawable createAttributeItem(PerformanceDisplayAttribute attribute, double attributeSum) + private Drawable createAttributeItem(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) { bool isTotal = attribute.PropertyName == nameof(PerformanceAttributes.Total); + float fraction = (float)(attribute.Value / perfectAttribute.Value); + if (float.IsNaN(fraction)) + fraction = 0; return new GridContainer { AutoSizeAxes = Axes.Both, @@ -113,7 +117,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Width = 130, Height = 5, BackgroundColour = Color4.White.Opacity(0.5f), - Length = (float)(attribute.Value / attributeSum), + Length = fraction, Margin = new MarginPadding { Left = 5, Right = 5 } }, new OsuSpriteText @@ -121,7 +125,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = ((int)Math.Round(attribute.Value, MidpointRounding.AwayFromZero)).ToString(CultureInfo.CurrentCulture), + Text = fraction.ToLocalisableString("0%"), Colour = isTotal ? totalColour : textColour } } @@ -129,19 +133,17 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics }; } - protected virtual void UpdateDisplay(PerformanceAttributes attributes) + protected virtual void UpdateDisplay(PerformanceBreakdown performance) { Content.Clear(); - var displayAttributes = attributes.GetAttributesForDisplay(); + var displayAttributes = performance.Performance.GetAttributesForDisplay(); - double attributeSum = displayAttributes - .Where(attr => attr.PropertyName != nameof(PerformanceAttributes.Total)) - .Sum(attr => attr.Value); + var perfectDisplayAttributes = performance.PerfectPerformance.GetAttributesForDisplay(); foreach (PerformanceDisplayAttribute attr in displayAttributes) { - Content.Add(createAttributeItem(attr, attributeSum)); + Content.Add(createAttributeItem(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName))); } } From a5b53c01c813a94fadadd5f3102990f0aece2137 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 18 Jan 2022 21:59:51 +0800 Subject: [PATCH 009/136] Add comments and tidy up --- .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 6 ++++++ .../Expanded/Statistics/PerformanceStatisticTooltip.cs | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 493f6a2dc9..f00eb9d71f 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -140,8 +140,14 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public class PerformanceBreakdown { + /// + /// Actual gameplay performance. + /// public PerformanceAttributes Performance { get; set; } + /// + /// Performance of a perfect play for comparison. + /// public PerformanceAttributes PerfectPerformance { get; set; } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 564c195a3d..7209db53f6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; - private Colour4 totalColour; + private Colour4 titleColor; private Colour4 textColour; protected override Container Content { get; } @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private void load(OsuColour colours) { background.Colour = colours.Gray3; - totalColour = colours.Blue; + titleColor = colours.Blue; textColour = colours.BlueLighter; } @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = attribute.DisplayName, - Colour = isTotal ? totalColour : textColour + Colour = isTotal ? titleColor : textColour }, new Bar { @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), Text = fraction.ToLocalisableString("0%"), - Colour = isTotal ? totalColour : textColour + Colour = isTotal ? titleColor : textColour } } } From 31e03e99cd08f1bfd3c648461a6c17e959b0495a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 18 Jan 2022 22:11:43 +0800 Subject: [PATCH 010/136] Improve display of "total PP" --- osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs | 2 +- .../Expanded/Statistics/PerformanceStatisticTooltip.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 9bdb5f8f6f..f210c669a6 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Difficulty /// public virtual IEnumerable GetAttributesForDisplay() { - yield return new PerformanceDisplayAttribute(nameof(Total), "Total", Total); + yield return new PerformanceDisplayAttribute(nameof(Total), "Final PP", Total); } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 7209db53f6..528ccafd41 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -84,6 +85,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics float fraction = (float)(attribute.Value / perfectAttribute.Value); if (float.IsNaN(fraction)) fraction = 0; + string text = fraction.ToLocalisableString("0%").ToString(); + + if (isTotal) + text = (int)Math.Round(attribute.Value, MidpointRounding.AwayFromZero) + "/" + (int)Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero); + return new GridContainer { AutoSizeAxes = Axes.Both, @@ -111,12 +117,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics }, new Bar { - Alpha = isTotal ? 0 : 1, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Width = 130, Height = 5, BackgroundColour = Color4.White.Opacity(0.5f), + Colour = isTotal ? titleColor : textColour, Length = fraction, Margin = new MarginPadding { Left = 5, Right = 5 } }, @@ -125,7 +131,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = fraction.ToLocalisableString("0%"), + Text = text, Colour = isTotal ? titleColor : textColour } } From 0d708efb734571e8c2617b830b07c8406cdf5e31 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 19 Jan 2022 15:33:33 +0800 Subject: [PATCH 011/136] Split off `PerformanceBreakdown` and its own calculation logic --- .../Difficulty/PerformanceBreakdown.cs | 21 +++++ .../PerformanceBreakdownCalculator.cs | 90 +++++++++++++++++++ .../Statistics/PerformanceStatistic.cs | 80 +---------------- .../Statistics/PerformanceStatisticTooltip.cs | 2 - 4 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs create mode 100644 osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs new file mode 100644 index 0000000000..273d8613c5 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Data for generating a performance breakdown by comparing performance to a perfect play. + /// + public class PerformanceBreakdown + { + /// + /// Actual gameplay performance. + /// + public PerformanceAttributes Performance { get; set; } + + /// + /// Performance of a perfect play for comparison. + /// + public PerformanceAttributes PerfectPerformance { get; set; } + } +} diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs new file mode 100644 index 0000000000..fad7937a99 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Extensions; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Difficulty +{ + public class PerformanceBreakdownCalculator + { + private readonly BeatmapManager beatmapManager; + private readonly BeatmapDifficultyCache difficultyCache; + private readonly ScorePerformanceCache performanceCache; + + public PerformanceBreakdownCalculator(BeatmapManager beatmapManager, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache) + { + this.beatmapManager = beatmapManager; + this.difficultyCache = difficultyCache; + this.performanceCache = performanceCache; + } + + [ItemCanBeNull] + public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default) + { + PerformanceAttributes performance = await performanceCache.CalculatePerformanceAsync(score, cancellationToken).ConfigureAwait(false); + + ScoreInfo perfectScore = await getPerfectScore(score, cancellationToken).ConfigureAwait(false); + if (perfectScore == null) + return null; + + PerformanceAttributes perfectPerformance = await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationToken).ConfigureAwait(false); + + return new PerformanceBreakdown { Performance = performance, PerfectPerformance = perfectPerformance }; + } + + [ItemCanBeNull] + private Task getPerfectScore(ScoreInfo score, CancellationToken cancellationToken = default) + { + return Task.Factory.StartNew(() => + { + IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo).GetPlayableBeatmap(score.Ruleset, score.Mods); + ScoreInfo perfectPlay = score.DeepClone(); + perfectPlay.Accuracy = 1; + perfectPlay.Passed = true; + + // calculate max combo + var difficulty = difficultyCache.GetDifficultyAsync( + beatmap.BeatmapInfo, + score.Ruleset, + score.Mods, + cancellationToken + ).GetResultSafely(); + + if (difficulty == null) + return null; + + perfectPlay.MaxCombo = difficulty.Value.MaxCombo; + + // create statistics assuming all hit objects have perfect hit result + var statistics = beatmap.HitObjects + .Select(ho => ho.CreateJudgement().MaxResult) + .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 = score.Ruleset.CreateInstance().CreateScoreProcessor(); + 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()) + { + perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); + } + + return perfectPlay; + }, cancellationToken); + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index f00eb9d71f..f934dd1836 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -13,13 +11,11 @@ using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip + public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip { private readonly ScoreInfo score; @@ -47,64 +43,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics [BackgroundDependencyLoader] private void load() { - Task.WhenAll( - // actual performance - performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token), - // performance for a perfect play - getPerfectPerformance(score) - ).ContinueWith(attr => - { - PerformanceAttributes[] result = attr.GetResultSafely(); - setPerformanceValue(new PerformanceBreakdown { Performance = result[0], PerfectPerformance = result[1] }); - }); - } - - private async Task getPerfectPerformance(ScoreInfo originalScore) - { - ScoreInfo perfectScore = await getPerfectScore(originalScore).ConfigureAwait(false); - return await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationTokenSource.Token).ConfigureAwait(false); - } - - private Task getPerfectScore(ScoreInfo originalScore) - { - return Task.Factory.StartNew(() => - { - var beatmap = beatmapManager.GetWorkingBeatmap(originalScore.BeatmapInfo).GetPlayableBeatmap(originalScore.Ruleset, originalScore.Mods); - ScoreInfo perfectPlay = originalScore.DeepClone(); - perfectPlay.Accuracy = 1; - perfectPlay.Passed = true; - - // create statistics assuming all hit objects have perfect hit result - var statistics = beatmap.HitObjects - .Select(ho => ho.CreateJudgement().MaxResult) - .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) - .ToDictionary(pair => pair.hitResult, pair => pair.count); - perfectPlay.Statistics = statistics; - - // calculate max combo - var difficulty = difficultyCache.GetDifficultyAsync( - beatmap.BeatmapInfo, - originalScore.Ruleset, - originalScore.Mods, - cancellationTokenSource.Token - ).GetResultSafely(); - perfectPlay.MaxCombo = difficulty?.MaxCombo ?? originalScore.MaxCombo; - - // calculate total score - ScoreProcessor scoreProcessor = originalScore.Ruleset.CreateInstance().CreateScoreProcessor(); - 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()) - { - perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); - } - - return perfectPlay; - }, cancellationTokenSource.Token); + new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) + .CalculateAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => setPerformanceValue(t.GetResultSafely())); } private void setPerformanceValue(PerformanceBreakdown breakdown) @@ -137,18 +78,5 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); public PerformanceBreakdown TooltipContent { get; private set; } - - public class PerformanceBreakdown - { - /// - /// Actual gameplay performance. - /// - public PerformanceAttributes Performance { get; set; } - - /// - /// Performance of a perfect play for comparison. - /// - public PerformanceAttributes PerfectPerformance { get; set; } - } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 528ccafd41..930bfba96a 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -19,8 +19,6 @@ using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - using static PerformanceStatistic; - public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; From dd42c89260c26d1cd719242c78388217f23a2f6d Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 19 Jan 2022 16:08:45 +0800 Subject: [PATCH 012/136] Feed more info to the temporary score processor for more accurate total score --- osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index fad7937a99..17cb1de303 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -72,6 +72,8 @@ namespace osu.Game.Rulesets.Difficulty // calculate total score ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo; + scoreProcessor.Mods.Value = perfectPlay.Mods; perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); // compute rank achieved From 261fae68735ef6df0b43c9d1599869972ba18a1c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 20 Jan 2022 08:39:33 +0800 Subject: [PATCH 013/136] Move checks out of PopIn() --- .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 4 +++- .../Expanded/Statistics/PerformanceStatisticTooltip.cs | 7 +------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index f934dd1836..bda147cf22 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -50,7 +51,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private void setPerformanceValue(PerformanceBreakdown breakdown) { - if (breakdown != null) + // Don't display the tooltip if "Total" is the only item + if (breakdown != null && breakdown.Performance.GetAttributesForDisplay().Count() > 1) { TooltipContent = breakdown; performance.Value = (int)Math.Round(breakdown.Performance.Total, MidpointRounding.AwayFromZero); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 930bfba96a..44e5c366bb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -56,12 +56,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics textColour = colours.BlueLighter; } - protected override void PopIn() - { - // Don't display the tooltip if "Total" is the only item - if (currentPerformance.Performance.GetAttributesForDisplay().Count() > 1) - this.FadeIn(200, Easing.OutQuint); - } + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); From 42d904acee23490e51086cbae21fd9bf3aff9045 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 20 Jan 2022 12:50:28 +0800 Subject: [PATCH 014/136] Remove blocking calls and add back Task.WhenAll --- .../PerformanceBreakdownCalculator.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 17cb1de303..3c9c00ff3b 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -29,21 +28,30 @@ namespace osu.Game.Rulesets.Difficulty [ItemCanBeNull] public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default) { - PerformanceAttributes performance = await performanceCache.CalculatePerformanceAsync(score, cancellationToken).ConfigureAwait(false); + 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 async Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) + { ScoreInfo perfectScore = await getPerfectScore(score, cancellationToken).ConfigureAwait(false); if (perfectScore == null) return null; - PerformanceAttributes perfectPerformance = await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationToken).ConfigureAwait(false); - - return new PerformanceBreakdown { Performance = performance, PerfectPerformance = perfectPerformance }; + return await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationToken).ConfigureAwait(false); } [ItemCanBeNull] private Task getPerfectScore(ScoreInfo score, CancellationToken cancellationToken = default) { - return Task.Factory.StartNew(() => + return Task.Run(async () => { IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo).GetPlayableBeatmap(score.Ruleset, score.Mods); ScoreInfo perfectPlay = score.DeepClone(); @@ -51,12 +59,12 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Passed = true; // calculate max combo - var difficulty = difficultyCache.GetDifficultyAsync( + var difficulty = await difficultyCache.GetDifficultyAsync( beatmap.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken - ).GetResultSafely(); + ).ConfigureAwait(false); if (difficulty == null) return null; From 6c97fbd3f28f8374822e41950ca293a3d5a1e62f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 20 Jan 2022 13:06:00 +0800 Subject: [PATCH 015/136] Calculate perfect performance without caching --- .../PerformanceBreakdownCalculator.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 3c9c00ff3b..5cf63d0102 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -39,20 +39,11 @@ namespace osu.Game.Rulesets.Difficulty } [ItemCanBeNull] - private async Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) - { - ScoreInfo perfectScore = await getPerfectScore(score, cancellationToken).ConfigureAwait(false); - if (perfectScore == null) - return null; - - return await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationToken).ConfigureAwait(false); - } - - [ItemCanBeNull] - private Task getPerfectScore(ScoreInfo score, CancellationToken cancellationToken = default) + private Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) { return Task.Run(async () => { + Ruleset ruleset = score.Ruleset.CreateInstance(); IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo).GetPlayableBeatmap(score.Ruleset, score.Mods); ScoreInfo perfectPlay = score.DeepClone(); perfectPlay.Accuracy = 1; @@ -79,7 +70,7 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Statistics = statistics; // calculate total score - ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo; scoreProcessor.Mods.Value = perfectPlay.Mods; perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); @@ -93,7 +84,9 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); } - return perfectPlay; + // calculate performance for this perfect score + // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes + return ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); }, cancellationToken); } } From e0fe8af365535a20c077cc397a69b2bf801c9dba Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 21 Jan 2022 08:54:08 +0800 Subject: [PATCH 016/136] Schedule setPerformanceValue --- .../Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index bda147cf22..158fd82b29 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) .CalculateAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => setPerformanceValue(t.GetResultSafely())); + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); } private void setPerformanceValue(PerformanceBreakdown breakdown) From 48aa1677dcd28e0477bb84ae17075ba53d46e56e Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 23 Jan 2022 11:01:30 +0800 Subject: [PATCH 017/136] Include hit results of nested hit objects in statistics of perfect score --- .../Difficulty/PerformanceBreakdownCalculator.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 5cf63d0102..46342b237c 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . 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; @@ -64,7 +66,7 @@ namespace osu.Game.Rulesets.Difficulty // create statistics assuming all hit objects have perfect hit result var statistics = beatmap.HitObjects - .Select(ho => ho.CreateJudgement().MaxResult) + .SelectMany(getPerfectHitResults) .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) .ToDictionary(pair => pair.hitResult, pair => pair.count); perfectPlay.Statistics = statistics; @@ -89,5 +91,13 @@ namespace osu.Game.Rulesets.Difficulty return ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); }, cancellationToken); } + + private IEnumerable getPerfectHitResults(HitObject hitObject) + { + foreach (HitObject nested in hitObject.NestedHitObjects) + yield return nested.CreateJudgement().MaxResult; + + yield return hitObject.CreateJudgement().MaxResult; + } } } From 502e6af008757204e53f04eb5cbfe7f586558989 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 00:21:54 +0900 Subject: [PATCH 018/136] Remove PlayingUsers list from SpectatorClient --- .../Gameplay/TestSceneSpectatorPlayback.cs | 38 ------------------- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 10 ----- .../Dashboard/CurrentlyPlayingDisplay.cs | 30 +++++++-------- .../Visual/Spectator/TestSpectatorClient.cs | 7 ++-- 5 files changed, 19 insertions(+), 68 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 90abdf2ba3..75369661a0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -3,12 +3,9 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -20,7 +17,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; @@ -47,8 +43,6 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private readonly IBindableList users = new BindableList(); - private TestReplayRecorder recorder; private ManualClock manualClock; @@ -57,9 +51,6 @@ namespace osu.Game.Tests.Visual.Gameplay private TestFramedReplayInputHandler replayHandler; - [Resolved] - private IAPIProvider api { get; set; } - [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -78,35 +69,6 @@ namespace osu.Game.Tests.Visual.Gameplay spectatorClient.OnNewFrames += onNewFrames; - users.BindTo(spectatorClient.PlayingUsers); - users.BindCollectionChanged((obj, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); - - foreach (int user in args.NewItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.WatchUser(user); - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); - - foreach (int user in args.OldItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.StopWatchingUser(user); - } - - break; - } - }, true); - Children = new Drawable[] { new GridContainer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 9b8e67b07a..8b71dfac21 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach (int userId in PlayingUsers) + foreach ((int userId, _) in PlayingUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index fddb94fad7..c3e70edcd3 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -37,9 +37,6 @@ namespace osu.Game.Online.Spectator private readonly List watchingUsers = new List(); - public IBindableList PlayingUsers => playingUsers; - private readonly BindableList playingUsers = new BindableList(); - public IBindableDictionary PlayingUserStates => playingUserStates; private readonly BindableDictionary playingUserStates = new BindableDictionary(); @@ -88,10 +85,7 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else - { - playingUsers.Clear(); playingUserStates.Clear(); - } }), true); } @@ -99,9 +93,6 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - if (!playingUsers.Contains(userId)) - playingUsers.Add(userId); - // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched. // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29). // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations. @@ -118,7 +109,6 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - playingUsers.Remove(userId); playingUserStates.Remove(userId); OnUserFinishedPlaying?.Invoke(userId, state); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index fde20575fc..afbfb9f7ef 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableList playingUsers = new BindableList(); + private readonly IBindableDictionary playingUserStates = new BindableDictionary(); private FillFlowContainer userFlow; @@ -51,18 +51,20 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - playingUsers.BindTo(spectatorClient.PlayingUsers); - playingUsers.BindCollectionChanged(onUsersChanged, true); + playingUserStates.BindTo(spectatorClient.PlayingUserStates); + playingUserStates.BindCollectionChanged(onUsersChanged, true); } - private void onUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => + private void onUsersChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { - case NotifyCollectionChangedAction.Add: - foreach (int id in e.NewItems.OfType().ToArray()) + case NotifyDictionaryChangedAction.Add: + Debug.Assert(e.NewItems != null); + + foreach ((int userId, _) in e.NewItems) { - users.GetUserAsync(id).ContinueWith(task => + users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); @@ -71,7 +73,7 @@ namespace osu.Game.Overlays.Dashboard Schedule(() => { // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) + if (!playingUserStates.ContainsKey(user.Id)) return; userFlow.Add(createUserPanel(user)); @@ -81,13 +83,11 @@ namespace osu.Game.Overlays.Dashboard break; - case NotifyCollectionChangedAction.Remove: - foreach (int u in e.OldItems.OfType()) - userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire(); - break; + case NotifyDictionaryChangedAction.Remove: + Debug.Assert(e.OldItems != null); - case NotifyCollectionChangedAction.Reset: - userFlow.Clear(); + foreach ((int userId, _) in e.OldItems) + userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); break; } }); diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 1a1d493249..7848a825f4 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -42,7 +41,7 @@ namespace osu.Game.Tests.Visual.Spectator { OnNewFrames += (i, bundle) => { - if (PlayingUsers.Contains(i)) + if (PlayingUserStates.ContainsKey(i)) lastReceivedUserFrames[i] = bundle.Frames[^1]; }; } @@ -65,7 +64,7 @@ namespace osu.Game.Tests.Visual.Spectator /// The user to end play for. public void EndPlay(int userId) { - if (!PlayingUsers.Contains(userId)) + if (!PlayingUserStates.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState @@ -131,7 +130,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUsers.Contains(userId)) + if (PlayingUserStates.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; From 781cb9f18d55e21b153967ee389c1a45ca934e0a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 01:45:11 +0900 Subject: [PATCH 019/136] Move HasPassed/HasFailed into GameplayState --- .../TestSceneTaikoSuddenDeath.cs | 2 +- .../Visual/Gameplay/TestSceneFailAnimation.cs | 2 +- .../Visual/Gameplay/TestSceneFailJudgement.cs | 2 +- .../Visual/Gameplay/TestScenePause.cs | 12 ++++++------ .../TestScenePlayerScoreSubmission.cs | 4 ++-- .../Gameplay/TestSceneStoryboardWithOutro.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 2 +- .../Multiplayer/MultiplayerPlayerLoader.cs | 2 +- osu.Game/Screens/Play/GameplayState.cs | 10 ++++++++++ osu.Game/Screens/Play/Player.cs | 19 ++++++------------- 10 files changed, 30 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 0be005e1c4..eec88d7bf8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += b => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("failed", () => Player.HasFailed); + AddAssert("failed", () => Player.GameplayState.HasFailed); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 7167d3120a..744227c55e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible); // The pause screen and fail animation both ramp frequency. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index fa27e1abdd..6430c29dfa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddAssert("total number of results == 1", () => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 04676f656f..ea0255ab76 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseAfterFail() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayAfterFailAnimation() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayDuringFailAnimation() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); // will finish the fail animation and show the fail/pause screen. AddStep("attempt exit via pause key", () => Player.ExitViaPause()); @@ -227,7 +227,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickRetryFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -236,7 +236,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -341,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay { confirmClockRunning(false); confirmNotExited(); - AddAssert("player not failed", () => !Player.HasFailed); + AddAssert("player not failed", () => !Player.GameplayState.HasFailed); AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index a9675a2ee2..58b5df2612 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for token request", () => Player.TokenCreationRequested); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure no submission", () => Player.SubmittedScore == null); @@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 69798dcb82..b87183cbc7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index ed484e03f6..63984a1bed 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -209,7 +209,7 @@ namespace osu.Game.Tests.Visual.Navigation return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); - AddUntilStep("wait for fail", () => player.HasFailed); + AddUntilStep("wait for fail", () => player.GameplayState.HasFailed); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 470ba59a76..772651727e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerPlayerLoader : PlayerLoader { - public bool GameplayPassed => player?.GameplayPassed == true; + public bool GameplayPassed => player?.GameplayState.HasPassed == true; private Player player; diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 83881f739d..60fbfa6644 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -39,6 +39,16 @@ namespace osu.Game.Screens.Play /// public readonly Score Score; + /// + /// Whether gameplay completed without the user failing. + /// + public bool HasPassed { get; set; } + + /// + /// Whether the user failed during gameplay. + /// + public bool HasFailed { get; set; } + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0312789b12..814aacd2fa 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -72,15 +72,8 @@ namespace osu.Game.Screens.Play /// protected virtual bool PauseOnFocusLost => true; - /// - /// Whether gameplay has completed without the user having failed. - /// - public bool GameplayPassed { get; private set; } - public Action RestartRequested; - public bool HasFailed { get; private set; } - private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -559,7 +552,7 @@ namespace osu.Game.Screens.Play if (showDialogFirst && !pauseOrFailDialogVisible) { // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). - if (ValidForResume && HasFailed) + if (ValidForResume && GameplayState.HasFailed) { failAnimationLayer.FinishTransforms(true); return; @@ -678,7 +671,7 @@ namespace osu.Game.Screens.Play resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = null; - GameplayPassed = false; + GameplayState.HasPassed = false; ValidForResume = true; skipOutroOverlay.Hide(); return; @@ -688,7 +681,7 @@ namespace osu.Game.Screens.Play if (HealthProcessor.HasFailed) return; - GameplayPassed = true; + GameplayState.HasPassed = true; // Setting this early in the process means that even if something were to go wrong in the order of events following, there // is no chance that a user could return to the (already completed) Player instance from a child screen. @@ -804,7 +797,7 @@ namespace osu.Game.Screens.Play if (!CheckModsAllowFailure()) return false; - HasFailed = true; + GameplayState.HasFailed = true; Score.ScoreInfo.Passed = false; // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) @@ -859,13 +852,13 @@ namespace osu.Game.Screens.Play // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state - && !HasFailed; + && !GameplayState.HasFailed; private bool canResume => // cannot resume from a non-paused state GameplayClockContainer.IsPaused.Value // cannot resume if we are already in a fail state - && !HasFailed + && !GameplayState.HasFailed // already resuming && !IsResuming; From 38e075c522f72371742e4d02a948f7a53a0aac5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 02:02:31 +0900 Subject: [PATCH 020/136] Add HasQuit gameplay state --- osu.Game/Screens/Play/GameplayState.cs | 5 +++++ osu.Game/Screens/Play/Player.cs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 60fbfa6644..64e873b3bb 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -49,6 +49,11 @@ namespace osu.Game.Screens.Play /// public bool HasFailed { get; set; } + /// + /// Whether the user quit gameplay without either having either passed or failed. + /// + public bool HasQuit { get; set; } + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 814aacd2fa..f6a2310826 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -983,6 +983,9 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { + if (!GameplayState.HasPassed && !GameplayState.HasFailed) + GameplayState.HasQuit = true; + screenSuspension?.RemoveAndDisposeImmediately(); failAnimationLayer?.RemoveFilters(); From bfe6218ed51c6451f85046998e225de2bdcbe7e6 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Tue, 1 Feb 2022 12:43:58 +0100 Subject: [PATCH 021/136] Change default orientation to SensorLandscape --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- osu.Android/OsuGameActivity.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index 25bd659a5d..ab91b4e3b3 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -27,7 +27,7 @@ namespace osu.Android { gameActivity.RunOnUiThread(() => { - gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser; + gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : OsuGameActivity.DEFAULT_ORIENTATION; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index c9fb539d8a..e6679b61a6 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -17,7 +17,7 @@ using osu.Game.Database; namespace osu.Android { - [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser)] + [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = DEFAULT_ORIENTATION)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")] @@ -39,6 +39,8 @@ namespace osu.Android [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { + public const ScreenOrientation DEFAULT_ORIENTATION = ScreenOrientation.SensorLandscape; + private static readonly string[] osu_url_schemes = { "osu", "osump" }; private OsuGameAndroid game; From 41007169f722febe2217ff805cf95cdb6d81c3dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Feb 2022 15:51:41 +0900 Subject: [PATCH 022/136] Give SpectatorState a user state --- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Online/Spectator/SpectatingUserState.cs | 33 +++++++++++++++++++ osu.Game/Online/Spectator/SpectatorClient.cs | 10 +++++- osu.Game/Online/Spectator/SpectatorState.cs | 7 ++-- osu.Game/Rulesets/UI/ReplayRecorder.cs | 4 ++- osu.Game/Screens/Play/Player.cs | 2 +- 6 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Online/Spectator/SpectatingUserState.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 917b3c89a8..86d6de6975 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send frames and finish play", () => { spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero)); - spectatorClient.EndPlaying(); + spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true }); }); // We can't access API because we're an "online" test. diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs new file mode 100644 index 0000000000..c7ba4ba248 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Spectator +{ + public enum SpectatingUserState + { + /// + /// The spectated user has not yet played. + /// + Idle, + + /// + /// The spectated user is currently playing. + /// + Playing, + + /// + /// The spectated user has successfully completed gameplay. + /// + Completed, + + /// + /// The spectator user has failed during gameplay. + /// + Failed, + + /// + /// The spectated user has quit during gameplay. + /// + Quit + } +} diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index c3e70edcd3..f4161e1db9 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -138,6 +138,7 @@ namespace osu.Game.Online.Spectator currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); + currentState.State = SpectatingUserState.Playing; currentBeatmap = state.Beatmap; currentScore = score; @@ -148,7 +149,7 @@ namespace osu.Game.Online.Spectator public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data); - public void EndPlaying() + public void EndPlaying(GameplayState state) { // This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue). // We probably need to find a better way to handle this... @@ -163,6 +164,13 @@ namespace osu.Game.Online.Spectator IsPlaying = false; currentBeatmap = null; + if (state.HasPassed) + currentState.State = SpectatingUserState.Completed; + else if (state.HasFailed) + currentState.State = SpectatingUserState.Failed; + else + currentState.State = SpectatingUserState.Quit; + EndPlayingInternal(currentState); }); } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index ebb91e4dd2..fc62f16bba 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -24,14 +24,17 @@ namespace osu.Game.Online.Spectator [Key(2)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); + [Key(3)] + public SpectatingUserState State { get; set; } + public bool Equals(SpectatorState other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID; + return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && State == other.State; } - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID} State:{State}"; } } diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 976f95cef8..277040b2a6 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -55,7 +55,9 @@ namespace osu.Game.Rulesets.UI protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - spectatorClient?.EndPlaying(); + + if (spectatorClient != null && gameplayState != null) + spectatorClient.EndPlaying(gameplayState); } protected override void Update() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f6a2310826..1aa6cded48 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1000,7 +1000,7 @@ namespace osu.Game.Screens.Play // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // To resolve test failures, forcefully end playing synchronously when this screen exits. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. - spectatorClient.EndPlaying(); + spectatorClient.EndPlaying(GameplayState); // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // as we are no longer the current screen, we cannot guarantee the track is still usable. From f4210f7a302a3bad2130181800106cdcefeb2020 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 22:21:07 +0900 Subject: [PATCH 023/136] Rework spectator components to use new user state --- osu.Game/Online/Spectator/SpectatorClient.cs | 8 +--- .../Dashboard/CurrentlyPlayingDisplay.cs | 42 ++++++++++++------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 33 ++++++++------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index f4161e1db9..fb7526347b 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -93,12 +93,8 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched. - // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29). - // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations. if (watchingUsers.Contains(userId)) playingUserStates[userId] = state; - OnUserBeganPlaying?.Invoke(userId, state); }); @@ -109,8 +105,8 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - playingUserStates.Remove(userId); - + if (watchingUsers.Contains(userId)) + playingUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index afbfb9f7ef..383f17d8d2 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableDictionary playingUserStates = new BindableDictionary(); + private readonly IBindableDictionary userStates = new BindableDictionary(); private FillFlowContainer userFlow; @@ -51,33 +51,32 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - playingUserStates.BindTo(spectatorClient.PlayingUserStates); - playingUserStates.BindCollectionChanged(onUsersChanged, true); + userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindCollectionChanged(onUserStatesChanged, true); } - private void onUsersChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Replace: Debug.Assert(e.NewItems != null); - foreach ((int userId, _) in e.NewItems) + foreach ((int userId, SpectatorState state) in e.NewItems) { + if (state.State != SpectatingUserState.Playing) + { + removePlayingUser(userId); + continue; + } + users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); - if (user == null) return; - - Schedule(() => - { - // user may no longer be playing. - if (!playingUserStates.ContainsKey(user.Id)) - return; - - userFlow.Add(createUserPanel(user)); - }); + if (user != null) + Schedule(() => addPlayingUser(user)); }); } @@ -87,9 +86,20 @@ namespace osu.Game.Overlays.Dashboard Debug.Assert(e.OldItems != null); foreach ((int userId, _) in e.OldItems) - userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); + removePlayingUser(userId); break; } + + void addPlayingUser(APIUser user) + { + // user may no longer be playing. + if (!userStates.TryGetValue(user.Id, out var state2) || state2.State != SpectatingUserState.Playing) + return; + + userFlow.Add(createUserPanel(user)); + } + + void removePlayingUser(int userId) => userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); }); private PlayingUserPanel createUserPanel(APIUser user) => diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 3cf9f79611..783ba494eb 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Spectate [Resolved] private UserLookupCache userLookupCache { get; set; } - private readonly IBindableDictionary playingUserStates = new BindableDictionary(); + private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); @@ -77,8 +77,8 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - playingUserStates.BindTo(spectatorClient.PlayingUserStates); - playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); + userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Spectate { foreach ((int userId, _) in userMap) { - if (!playingUserStates.TryGetValue(userId, out var userState)) + if (!userStates.TryGetValue(userId, out var userState)) continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID)) @@ -107,40 +107,41 @@ namespace osu.Game.Screens.Spectate } } - private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) + private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { case NotifyDictionaryChangedAction.Add: foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateAdded(userId, state); + onUserStateChanged(userId, state); break; case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, var _) in e.OldItems.AsNonNull()) + foreach ((int userId, _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); break; case NotifyDictionaryChangedAction.Replace: - foreach ((int userId, var _) in e.OldItems.AsNonNull()) - onUserStateRemoved(userId); - foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateAdded(userId, state); + onUserStateChanged(userId, state); break; } } - private void onUserStateAdded(int userId, SpectatorState state) + private void onUserStateChanged(int userId, SpectatorState newState) { - if (state.RulesetID == null || state.BeatmapID == null) + if (newState.RulesetID == null || newState.BeatmapID == null) return; if (!userMap.ContainsKey(userId)) return; - Schedule(() => OnUserStateChanged(userId, state)); - updateGameplayState(userId); + // Do nothing for failed/completed states. + if (newState.State == SpectatingUserState.Playing) + { + Schedule(() => OnUserStateChanged(userId, newState)); + updateGameplayState(userId); + } } private void onUserStateRemoved(int userId) @@ -162,7 +163,7 @@ namespace osu.Game.Screens.Spectate Debug.Assert(userMap.ContainsKey(userId)); var user = userMap[userId]; - var spectatorState = playingUserStates[userId]; + var spectatorState = userStates[userId]; var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == spectatorState.RulesetID)?.CreateInstance(); if (resolvedRuleset == null) From 39524f3dd296d44cfb8187507a416e02a47b040c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 1 Feb 2022 22:26:30 +0800 Subject: [PATCH 024/136] Split total pp into 2 lines --- .../Statistics/PerformanceStatisticTooltip.cs | 85 ++++++++++++++++--- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 44e5c366bb..739fac9d63 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -72,16 +72,73 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics UpdateDisplay(performance); } - private Drawable createAttributeItem(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + private Drawable createItemForTotal(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) { - bool isTotal = attribute.PropertyName == nameof(PerformanceAttributes.Total); - float fraction = (float)(attribute.Value / perfectAttribute.Value); - if (float.IsNaN(fraction)) - fraction = 0; - string text = fraction.ToLocalisableString("0%").ToString(); + return + new GridContainer + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Bottom = 10 }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 250), + 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), + Text = attribute.DisplayName, + Colour = titleColor + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = Math.Round(attribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(), + Colour = titleColor + } + }, + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = "Maximum", + Colour = OsuColour.Gray(0.7f) + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(), + Colour = OsuColour.Gray(0.7f) + } + } + } + }; + } - if (isTotal) - text = (int)Math.Round(attribute.Value, MidpointRounding.AwayFromZero) + "/" + (int)Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero); + private Drawable createItemForAttribute(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + { + float percentage = (float)(attribute.Value / perfectAttribute.Value); + if (float.IsNaN(percentage)) + percentage = 0; + string text = percentage.ToLocalisableString("0%").ToString(); return new GridContainer { @@ -106,7 +163,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = attribute.DisplayName, - Colour = isTotal ? titleColor : textColour + Colour = textColour }, new Bar { @@ -115,8 +172,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Width = 130, Height = 5, BackgroundColour = Color4.White.Opacity(0.5f), - Colour = isTotal ? titleColor : textColour, - Length = fraction, + Colour = textColour, + Length = percentage, Margin = new MarginPadding { Left = 5, Right = 5 } }, new OsuSpriteText @@ -125,7 +182,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), Text = text, - Colour = isTotal ? titleColor : textColour + Colour = textColour } } } @@ -142,7 +199,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics foreach (PerformanceDisplayAttribute attr in displayAttributes) { - Content.Add(createAttributeItem(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName))); + Content.Add(attr.PropertyName == nameof(PerformanceAttributes.Total) + ? createItemForTotal(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)) + : createItemForAttribute(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName))); } } From b06128ffa5f4845bd0c9ae6dabe11120336b5cf4 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 1 Feb 2022 22:26:55 +0800 Subject: [PATCH 025/136] Rename "Final PP" to "Achieved PP" --- osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index f210c669a6..e8c4c71913 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Difficulty /// public virtual IEnumerable GetAttributesForDisplay() { - yield return new PerformanceDisplayAttribute(nameof(Total), "Final PP", Total); + yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total); } } } From b4fd1ecba20a10858a1219beddabf8122517fa22 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 11:02:01 +0800 Subject: [PATCH 026/136] Hide attribute if the maximum is 0 --- .../Statistics/PerformanceStatisticTooltip.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 739fac9d63..bd6eb057c6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -137,8 +137,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { float percentage = (float)(attribute.Value / perfectAttribute.Value); if (float.IsNaN(percentage)) - percentage = 0; - string text = percentage.ToLocalisableString("0%").ToString(); + return null; return new GridContainer { @@ -181,7 +180,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = text, + Text = percentage.ToLocalisableString("0%"), Colour = textColour } } @@ -199,9 +198,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics foreach (PerformanceDisplayAttribute attr in displayAttributes) { - Content.Add(attr.PropertyName == nameof(PerformanceAttributes.Total) + var attributeItem = attr.PropertyName == nameof(PerformanceAttributes.Total) ? createItemForTotal(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)) - : createItemForAttribute(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName))); + : createItemForAttribute(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)); + + if (attributeItem != null) + Content.Add(attributeItem); } } From c5c4c85006ffd4a34e05ba33d6e81bb9acf8acc8 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 13:29:18 +0800 Subject: [PATCH 027/136] Lazily create content of StatisticItem --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 22 +++++++++------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 25 +++++++++++-------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 22 +++++++++------- .../Ranking/Statistics/StatisticContainer.cs | 2 +- .../Ranking/Statistics/StatisticItem.cs | 12 +++++++-- .../Ranking/Statistics/StatisticsPanel.cs | 10 +++++--- 6 files changed, 59 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2098c7f5d8..8ac8001457 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,21 +370,25 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", + true, + () => new HitEventTimingDistributionGraph(score.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(score.HitEvents) - })) + new StatisticItem(string.Empty, + true, + () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + })) } } }; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 18e4bb259c..b6f417b7fe 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -278,7 +278,8 @@ namespace osu.Game.Rulesets.Osu Columns = new[] { new StatisticItem("Timing Distribution", - new HitEventTimingDistributionGraph(timedHitEvents) + true, + () => new HitEventTimingDistributionGraph(timedHitEvents) { RelativeSizeAxes = Axes.X, Height = 250 @@ -289,21 +290,25 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", + true, + () => new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - })) + new StatisticItem(string.Empty, + true, + () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) } } }; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index ca860f24c3..2a64b8dddd 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,21 +213,25 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", + true, + () => new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - })) + new StatisticItem(string.Empty, + true, + () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 485d24d024..f3bd2c6fc1 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 15 }, - Child = item.Content + Child = item.Content() } }, }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 4903983759..5e6ddf445b 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,22 +21,29 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The content to be displayed. /// - public readonly Drawable Content; + public readonly Func Content; /// /// The of this row. This can be thought of as the column dimension of an encompassing . /// public readonly Dimension Dimension; + /// + /// Whether this item requires hit events. If true, the will not be created if no hit events are available. + /// + public readonly bool RequiresHitEvents; + /// /// Creates a new , to be displayed inside a in the results screen. /// /// The name of the item. Can be to hide the item header. + /// Whether this item requires hit events. If true, the will not be created if no hit events are available. /// The content to be displayed. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) + public StatisticItem([NotNull] string name, bool requiresHitEvents, [NotNull] Func content, [CanBeNull] Dimension dimension = null) { Name = name; + RequiresHitEvents = requiresHitEvents; Content = content; Dimension = dimension; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 567a2307dd..14cdfe2f03 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -118,6 +118,10 @@ namespace osu.Game.Screens.Ranking.Statistics foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) { + var columnsToDisplay = newScore.HitEvents.Count == 0 + ? row.Columns?.Where(c => !c.RequiresHitEvents).ToArray() + : row.Columns; + rows.Add(new GridContainer { Anchor = Anchor.TopCentre, @@ -126,14 +130,14 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - row.Columns?.Select(c => new StatisticContainer(c) + columnsToDisplay?.Select(c => new StatisticContainer(c) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }).Cast().ToArray() }, - ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) - .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), + ColumnDimensions = Enumerable.Range(0, columnsToDisplay?.Length ?? 0) + .Select(i => columnsToDisplay?[i].Dimension ?? new Dimension()).ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } From 3ba5d88914d09e1725ee3abaec9fd44699e3973c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 13:41:51 +0800 Subject: [PATCH 028/136] Update statistics item display logic --- .../Ranking/Statistics/StatisticsPanel.cs | 131 +++++++++--------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 14cdfe2f03..9d89aa213c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -74,85 +74,92 @@ namespace osu.Game.Screens.Ranking.Statistics if (newScore == null) return; - if (newScore.HitEvents.Count == 0) + spinner.Show(); + + var localCancellationSource = loadCancellation = new CancellationTokenSource(); + IBeatmap playableBeatmap = null; + + // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. + Task.Run(() => { - content.Add(new FillFlowContainer + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); + }, loadCancellation.Token).ContinueWith(t => Schedule(() => + { + var rows = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Direction = FillDirection.Vertical, - Children = new Drawable[] + Spacing = new Vector2(30, 15), + Alpha = 0 + }; + + bool panelIsEmpty = true; + bool hitEventsAvailable = newScore.HitEvents.Count != 0; + + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + { + var columnsToDisplay = hitEventsAvailable + ? row.Columns + : row.Columns?.Where(c => !c.RequiresHitEvents).ToArray(); + + if (columnsToDisplay?.Any() ?? false) + panelIsEmpty = false; + + rows.Add(new GridContainer { - new MessagePlaceholder("Extended statistics are only available after watching a replay!"), - new ReplayDownloadButton(newScore) + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { - Scale = new Vector2(1.5f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + columnsToDisplay?.Select(c => new StatisticContainer(c) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }).Cast().ToArray() }, - } - }); - } - else - { - spinner.Show(); + ColumnDimensions = Enumerable.Range(0, columnsToDisplay?.Length ?? 0) + .Select(i => columnsToDisplay?[i].Dimension ?? new Dimension()).ToArray(), + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } - var localCancellationSource = loadCancellation = new CancellationTokenSource(); - IBeatmap playableBeatmap = null; - - // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. - Task.Run(() => + if (!hitEventsAvailable) { - playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); - }, loadCancellation.Token).ContinueWith(t => Schedule(() => - { - var rows = new FillFlowContainer + rows.Add(new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - Alpha = 0 - }; - - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) - { - var columnsToDisplay = newScore.HitEvents.Count == 0 - ? row.Columns?.Where(c => !c.RequiresHitEvents).ToArray() - : row.Columns; - - rows.Add(new GridContainer + Children = new Drawable[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] + new MessagePlaceholder(panelIsEmpty + ? "Extended statistics are only available after watching a replay!" + : "More statistics available after watching a replay!"), + new ReplayDownloadButton(newScore) { - columnsToDisplay?.Select(c => new StatisticContainer(c) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }).Cast().ToArray() + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }, - ColumnDimensions = Enumerable.Range(0, columnsToDisplay?.Length ?? 0) - .Select(i => columnsToDisplay?[i].Dimension ?? new Dimension()).ToArray(), - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); - } + } + }); + } - LoadComponentAsync(rows, d => - { - if (!Score.Value.Equals(newScore)) - return; + LoadComponentAsync(rows, d => + { + if (!Score.Value.Equals(newScore)) + return; - spinner.Hide(); - content.Add(d); - d.FadeIn(250, Easing.OutQuint); - }, localCancellationSource.Token); - }), localCancellationSource.Token); - } + spinner.Hide(); + content.Add(d); + d.FadeIn(250, Easing.OutQuint); + }, localCancellationSource.Token); + }), localCancellationSource.Token); } protected override bool OnClick(ClickEvent e) From 5e3d124eef996801dcb315cc48ed7499dc4aa0ef Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:20:22 +0800 Subject: [PATCH 029/136] Add scrolling to the extended statistics panel --- .../Ranking/Statistics/StatisticsPanel.cs | 78 +++++++++++++------ 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 9d89aa213c..1c96e640b4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; using osu.Game.Scoring; @@ -85,15 +86,21 @@ namespace osu.Game.Screens.Ranking.Statistics playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); }, loadCancellation.Token).ContinueWith(t => Schedule(() => { - var rows = new FillFlowContainer + FillFlowContainer rows; + Container container = new OsuScrollContainer(Direction.Vertical) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - Alpha = 0 + Alpha = 0, + Children = new[] + { + rows = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } }; bool panelIsEmpty = true; @@ -107,6 +114,8 @@ namespace osu.Game.Screens.Ranking.Statistics if (columnsToDisplay?.Any() ?? false) panelIsEmpty = false; + else + continue; rows.Add(new GridContainer { @@ -114,6 +123,7 @@ namespace osu.Game.Screens.Ranking.Statistics Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 15 }, Content = new[] { columnsToDisplay?.Select(c => new StatisticContainer(c) @@ -130,27 +140,51 @@ namespace osu.Game.Screens.Ranking.Statistics if (!hitEventsAvailable) { - rows.Add(new FillFlowContainer + if (panelIsEmpty) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + // Replace the scroll container with fill flow container to get the message centered. + container = new FillFlowContainer { - new MessagePlaceholder(panelIsEmpty - ? "Extended statistics are only available after watching a replay!" - : "More statistics available after watching a replay!"), - new ReplayDownloadButton(newScore) + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Scale = new Vector2(1.5f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - }); + new MessagePlaceholder("Extended statistics are only available after watching a replay!"), + new ReplayDownloadButton(newScore) + { + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }; + } + else + { + rows.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + new MessagePlaceholder("More statistics available after watching a replay!"), + new ReplayDownloadButton(newScore) + { + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }); + } } - LoadComponentAsync(rows, d => + LoadComponentAsync(container, d => { if (!Score.Value.Equals(newScore)) return; From 6a482827fea64cbc0869700489e4feb9389f86d6 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:23:03 +0800 Subject: [PATCH 030/136] Fix weird line breaking --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 6 ++---- osu.Game.Rulesets.Osu/OsuRuleset.cs | 9 +++------ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 6 ++---- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 8ac8001457..0ea1cd5fc7 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,8 +370,7 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", - true, + new StatisticItem("Timing Distribution", true, () => new HitEventTimingDistributionGraph(score.HitEvents) { RelativeSizeAxes = Axes.X, @@ -383,8 +382,7 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem(string.Empty, - true, + new StatisticItem(string.Empty, true, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(score.HitEvents) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b6f417b7fe..2bf47100c1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -277,8 +277,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Timing Distribution", - true, + new StatisticItem("Timing Distribution", true, () => new HitEventTimingDistributionGraph(timedHitEvents) { RelativeSizeAxes = Axes.X, @@ -290,8 +289,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Accuracy Heatmap", - true, + new StatisticItem("Accuracy Heatmap", true, () => new AccuracyHeatmap(score, playableBeatmap) { RelativeSizeAxes = Axes.X, @@ -303,8 +301,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem(string.Empty, - true, + new StatisticItem(string.Empty, true, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(timedHitEvents) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2a64b8dddd..fe4116b4a6 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,8 +213,7 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", - true, + new StatisticItem("Timing Distribution", true, () => new HitEventTimingDistributionGraph(timedHitEvents) { RelativeSizeAxes = Axes.X, @@ -226,8 +225,7 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem(string.Empty, - true, + new StatisticItem(string.Empty, true, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(timedHitEvents) From 90e30bc9e8b3e7058aebfa83336383d52d0e0efb Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:26:17 +0800 Subject: [PATCH 031/136] Remove useless null checks --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 1c96e640b4..8b69f83055 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -110,9 +110,9 @@ namespace osu.Game.Screens.Ranking.Statistics { var columnsToDisplay = hitEventsAvailable ? row.Columns - : row.Columns?.Where(c => !c.RequiresHitEvents).ToArray(); + : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); - if (columnsToDisplay?.Any() ?? false) + if (columnsToDisplay.Any()) panelIsEmpty = false; else continue; @@ -132,8 +132,8 @@ namespace osu.Game.Screens.Ranking.Statistics Origin = Anchor.Centre, }).Cast().ToArray() }, - ColumnDimensions = Enumerable.Range(0, columnsToDisplay?.Length ?? 0) - .Select(i => columnsToDisplay?[i].Dimension ?? new Dimension()).ToArray(), + ColumnDimensions = Enumerable.Range(0, columnsToDisplay.Length) + .Select(i => columnsToDisplay[i].Dimension ?? new Dimension()).ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } From 042574660c10d6a28c29a81d6f45f5b8547f956a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:29:03 +0800 Subject: [PATCH 032/136] Rename "Content" to "CreateContent" --- .../Ranking/Statistics/StatisticContainer.cs | 2 +- .../Screens/Ranking/Statistics/StatisticItem.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index f3bd2c6fc1..79f813ef64 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 15 }, - Child = item.Content() + Child = item.CreateContent() } }, }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 5e6ddf445b..cb5ba4b9fe 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -19,9 +19,9 @@ namespace osu.Game.Screens.Ranking.Statistics public readonly string Name; /// - /// The content to be displayed. + /// A function returning the content to be displayed. /// - public readonly Func Content; + public readonly Func CreateContent; /// /// The of this row. This can be thought of as the column dimension of an encompassing . @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Ranking.Statistics public readonly Dimension Dimension; /// - /// Whether this item requires hit events. If true, the will not be created if no hit events are available. + /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// public readonly bool RequiresHitEvents; @@ -37,14 +37,14 @@ namespace osu.Game.Screens.Ranking.Statistics /// Creates a new , to be displayed inside a in the results screen. /// /// The name of the item. Can be to hide the item header. - /// Whether this item requires hit events. If true, the will not be created if no hit events are available. - /// The content to be displayed. + /// Whether this item requires hit events. If true, will not be called if no hit events are available. + /// A function returning the content to be displayed. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem([NotNull] string name, bool requiresHitEvents, [NotNull] Func content, [CanBeNull] Dimension dimension = null) + public StatisticItem([NotNull] string name, bool requiresHitEvents, [NotNull] Func createContent, [CanBeNull] Dimension dimension = null) { Name = name; RequiresHitEvents = requiresHitEvents; - Content = content; + CreateContent = createContent; Dimension = dimension; } } From 36bfef4f54f4f116ec7f84747db2c4c93a883d0d Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:32:16 +0800 Subject: [PATCH 033/136] Dispose container before replacing --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8b69f83055..2327f60f81 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -143,6 +143,7 @@ namespace osu.Game.Screens.Ranking.Statistics if (panelIsEmpty) { // Replace the scroll container with fill flow container to get the message centered. + container.Dispose(); container = new FillFlowContainer { RelativeSizeAxes = Axes.Both, From b0023b9809d15cdb7f19d5a2f6be9c0d047bf9fc Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 19:00:46 +0800 Subject: [PATCH 034/136] Also dispose `rows` --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 2327f60f81..62f9eb8506 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -143,6 +143,7 @@ namespace osu.Game.Screens.Ranking.Statistics if (panelIsEmpty) { // Replace the scroll container with fill flow container to get the message centered. + rows.Dispose(); container.Dispose(); container = new FillFlowContainer { From 1e19c7046a71de056cc975deaa045eb2eff46c8b Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 19:02:29 +0800 Subject: [PATCH 035/136] Use spacing instead of bottom margin --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 62f9eb8506..68414c4d49 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -98,7 +98,8 @@ namespace osu.Game.Screens.Ranking.Statistics rows = new FillFlowContainer { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(30, 15) } } }; @@ -123,7 +124,6 @@ namespace osu.Game.Screens.Ranking.Statistics Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 15 }, Content = new[] { columnsToDisplay?.Select(c => new StatisticContainer(c) From 3c2a6fe2086a1eb560a4aefe1805a8d402b1d153 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 19:07:14 +0800 Subject: [PATCH 036/136] Don't prompt for a replay if no item requires hit events --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 68414c4d49..08be1de652 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -105,6 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics }; bool panelIsEmpty = true; + bool panelIsComplete = true; bool hitEventsAvailable = newScore.HitEvents.Count != 0; foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) @@ -113,6 +114,9 @@ namespace osu.Game.Screens.Ranking.Statistics ? row.Columns : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); + if (columnsToDisplay.Length < row.Columns.Length) + panelIsComplete = false; + if (columnsToDisplay.Any()) panelIsEmpty = false; else @@ -163,7 +167,7 @@ namespace osu.Game.Screens.Ranking.Statistics } }; } - else + else if (!panelIsComplete) { rows.Add(new FillFlowContainer { From 19eb9ad8a7e86b384ca65648b95e35e37c2ce068 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 23:02:38 +0900 Subject: [PATCH 037/136] Reorder `StatisticsItem` constructor to make a touch more sense --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 20 ++++++------ osu.Game.Rulesets.Osu/OsuRuleset.cs | 31 +++++++++---------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 20 ++++++------ .../Ranking/Statistics/StatisticItem.cs | 4 +-- 4 files changed, 34 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0ea1cd5fc7..ffb26b224f 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,23 +370,21 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", true, - () => new HitEventTimingDistributionGraph(score.HitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, true, - () => new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(score.HitEvents) - })) + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + }), true) } } }; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 2bf47100c1..1122a869b7 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -277,35 +277,32 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Timing Distribution", true, - () => new HitEventTimingDistributionGraph(timedHitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), } }, new StatisticRow { Columns = new[] { - new StatisticItem("Accuracy Heatmap", true, - () => new AccuracyHeatmap(score, playableBeatmap) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, true, - () => new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - })) + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + }), true) } } }; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index fe4116b4a6..21c99c0d2f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,23 +213,21 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", true, - () => new HitEventTimingDistributionGraph(timedHitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, true, - () => new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - })) + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + }), true) } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index cb5ba4b9fe..3cfc38e263 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -37,10 +37,10 @@ namespace osu.Game.Screens.Ranking.Statistics /// Creates a new , to be displayed inside a in the results screen. /// /// The name of the item. Can be to hide the item header. - /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// A function returning the content to be displayed. + /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem([NotNull] string name, bool requiresHitEvents, [NotNull] Func createContent, [CanBeNull] Dimension dimension = null) + public StatisticItem([NotNull] string name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) { Name = name; RequiresHitEvents = requiresHitEvents; From 9d1d13c7152592d31a212fc04b04aac1fac171b1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:00:43 +0900 Subject: [PATCH 038/136] Fix up TestSpectatorClient implementation Rather than using a list which is supposed to be updated "client"-side, now uses the "server"-side list. --- .../Visual/Spectator/TestSpectatorClient.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 7848a825f4..6862cda88c 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -39,11 +39,7 @@ namespace osu.Game.Tests.Visual.Spectator public TestSpectatorClient() { - OnNewFrames += (i, bundle) => - { - if (PlayingUserStates.ContainsKey(i)) - lastReceivedUserFrames[i] = bundle.Frames[^1]; - }; + OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; } /// @@ -62,16 +58,20 @@ namespace osu.Game.Tests.Visual.Spectator /// Ends play for an arbitrary user. /// /// The user to end play for. - public void EndPlay(int userId) + /// The spectator state to end play with. + public void EndPlay(int userId, SpectatingUserState state = SpectatingUserState.Quit) { - if (!PlayingUserStates.ContainsKey(userId)) + if (!userBeatmapDictionary.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, + State = state }); + + userBeatmapDictionary.Remove(userId); } public new void Schedule(Action action) => base.Schedule(action); @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUserStates.ContainsKey(userId)) + if (userBeatmapDictionary.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; @@ -144,6 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, + State = SpectatingUserState.Playing }); } } From 589f5e7a31f53275f7cd001adbe4deedb1e18473 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:09:38 +0900 Subject: [PATCH 039/136] Update test which has now been resolved --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index d28e5203e3..9f8470446c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -155,11 +155,12 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); checkPaused(true); + sendFrames(); - finish(); + finish(SpectatingUserState.Failed); - checkPaused(false); - // TODO: should replay until running out of frames then fail + checkPaused(false); // Should continue playing until out of frames + checkPaused(true); } [Test] @@ -246,7 +247,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish() => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id)); + private void finish(SpectatingUserState state = SpectatingUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); From fcbba3d9481ca396acd9885593065ea782362475 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:11:29 +0900 Subject: [PATCH 040/136] Rename PlayingUserStates -> WatchingUserStates --- .../Visual/Gameplay/TestSceneSpectatorHost.cs | 4 ++-- .../TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 12 ++++++------ .../Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 409cec4cf6..6d6b0bf89e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClientSendsCorrectRuleset() { - AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id)); - AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); + AddUntilStep("spectator client sending frames", () => spectatorClient.WatchingUserStates.ContainsKey(dummy_user_id)); + AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); } public override void TearDownSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 8b71dfac21..55450b36e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach ((int userId, _) in PlayingUserStates) + foreach ((int userId, _) in WatchingUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 156d544960..3646c51d94 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -37,8 +37,8 @@ namespace osu.Game.Online.Spectator private readonly List watchingUsers = new List(); - public IBindableDictionary PlayingUserStates => playingUserStates; - private readonly BindableDictionary playingUserStates = new BindableDictionary(); + public IBindableDictionary WatchingUserStates => watchingUserStates; + private readonly BindableDictionary watchingUserStates = new BindableDictionary(); private IBeatmap? currentBeatmap; private Score? currentScore; @@ -85,7 +85,7 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else - playingUserStates.Clear(); + watchingUserStates.Clear(); }), true); } @@ -94,7 +94,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { if (watchingUsers.Contains(userId)) - playingUserStates[userId] = state; + watchingUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); }); @@ -106,7 +106,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { if (watchingUsers.Contains(userId)) - playingUserStates[userId] = state; + watchingUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); @@ -193,7 +193,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { watchingUsers.Remove(userId); - playingUserStates.Remove(userId); + watchingUserStates.Remove(userId); StopWatchingUserInternal(userId); }); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 383f17d8d2..975402a443 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindTo(spectatorClient.WatchingUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 783ba494eb..0b2a7cecf6 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindTo(spectatorClient.WatchingUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( From 81a22dbd29543b3ff5387669bd6ce1fc048a4811 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:19:43 +0900 Subject: [PATCH 041/136] Add back playing users list --- osu.Game/Online/Spectator/SpectatorClient.cs | 28 +++++++++-- .../Dashboard/CurrentlyPlayingDisplay.cs | 48 ++++++++----------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 3646c51d94..9e168411b0 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -35,16 +35,28 @@ namespace osu.Game.Online.Spectator /// public abstract IBindable IsConnected { get; } + /// + /// The states of all users currently being watched. + /// + public IBindableDictionary WatchingUserStates => watchingUserStates; + + /// + /// A global list of all players currently playing. + /// + public IBindableList PlayingUsers => playingUsers; + + /// + /// All users currently being watched. + /// private readonly List watchingUsers = new List(); - public IBindableDictionary WatchingUserStates => watchingUserStates; private readonly BindableDictionary watchingUserStates = new BindableDictionary(); + private readonly BindableList playingUsers = new BindableList(); + private readonly SpectatorState currentState = new SpectatorState(); private IBeatmap? currentBeatmap; private Score? currentScore; - private readonly SpectatorState currentState = new SpectatorState(); - /// /// Whether the local user is playing. /// @@ -85,7 +97,10 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else + { watchingUserStates.Clear(); + playingUsers.Clear(); + } }), true); } @@ -93,8 +108,12 @@ namespace osu.Game.Online.Spectator { Schedule(() => { + if (!playingUsers.Contains(userId)) + playingUsers.Add(userId); + if (watchingUsers.Contains(userId)) watchingUserStates[userId] = state; + OnUserBeganPlaying?.Invoke(userId, state); }); @@ -105,8 +124,11 @@ namespace osu.Game.Online.Spectator { Schedule(() => { + playingUsers.Remove(userId); + if (watchingUsers.Contains(userId)) watchingUserStates[userId] = state; + OnUserFinishedPlaying?.Invoke(userId, state); }); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 975402a443..02ef28f825 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableDictionary userStates = new BindableDictionary(); + private readonly IBindableList playingUsers = new BindableList(); private FillFlowContainer userFlow; @@ -51,55 +52,46 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - userStates.BindTo(spectatorClient.WatchingUserStates); - userStates.BindCollectionChanged(onUserStatesChanged, true); + playingUsers.BindTo(spectatorClient.PlayingUsers); + playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } - private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) { - case NotifyDictionaryChangedAction.Add: - case NotifyDictionaryChangedAction.Replace: + case NotifyCollectionChangedAction.Add: Debug.Assert(e.NewItems != null); - foreach ((int userId, SpectatorState state) in e.NewItems) + foreach (int userId in e.NewItems) { - if (state.State != SpectatingUserState.Playing) - { - removePlayingUser(userId); - continue; - } - users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); if (user != null) - Schedule(() => addPlayingUser(user)); + { + Schedule(() => + { + // user may no longer be playing. + if (!playingUsers.Contains(user.Id)) + return; + + userFlow.Add(createUserPanel(user)); + }); + } }); } break; - case NotifyDictionaryChangedAction.Remove: + case NotifyCollectionChangedAction.Remove: Debug.Assert(e.OldItems != null); - foreach ((int userId, _) in e.OldItems) - removePlayingUser(userId); + foreach (int userId in e.OldItems) + userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); break; } - - void addPlayingUser(APIUser user) - { - // user may no longer be playing. - if (!userStates.TryGetValue(user.Id, out var state2) || state2.State != SpectatingUserState.Playing) - return; - - userFlow.Add(createUserPanel(user)); - } - - void removePlayingUser(int userId) => userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); }); private PlayingUserPanel createUserPanel(APIUser user) => From dca1bddabb789867e34562e20bba19d02e22fab1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Feb 2022 21:25:50 +0300 Subject: [PATCH 042/136] Lock supported interface orientation to landscape for iPhone --- osu.Game.Rulesets.Catch.Tests.iOS/Info.plist | 5 +++++ osu.Game.Rulesets.Mania.Tests.iOS/Info.plist | 5 +++++ osu.Game.Rulesets.Osu.Tests.iOS/Info.plist | 5 +++++ osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist | 5 +++++ osu.Game.Tests.iOS/Info.plist | 5 +++++ osu.iOS/Info.plist | 5 +++++ 6 files changed, 30 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 3ba1886d98..88317b6393 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index 09ed2dd007..8d05c2bb73 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index dd032ef1c1..63ad313a70 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index ac658cd14e..3c76622ee3 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index 1a89345bc5..835e958bc6 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 2592f909ce..1156c0954c 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -40,6 +40,11 @@ NSMicrophoneUsageDescription We don't really use the microphone. UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown From 4aa4df69f258bd4027c8c90abba8adaa6a3a7b28 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Feb 2022 22:17:23 +0300 Subject: [PATCH 043/136] Reorder iOS landscape orientations to prioritise "Landscape Right" "Landscape Right" is often the proper default for landscape-only applications. Matches up with all other landscape-only iOS games I have locally. --- osu.Game.Rulesets.Catch.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Mania.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Osu.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist | 4 ++-- osu.Game.Tests.iOS/Info.plist | 4 ++-- osu.iOS/Info.plist | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 88317b6393..33ddac6dfb 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index 8d05c2bb73..78349334b4 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index 63ad313a70..b9f371c049 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index 3c76622ee3..65c47d2115 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index 835e958bc6..ed0c2e4dbf 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 1156c0954c..02968b87a7 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -41,15 +41,15 @@ We don't really use the microphone. UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset From e2fcdc394b25cbeb0af8a74c4e68bd79f722f8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 16:29:00 +0100 Subject: [PATCH 044/136] Extract method for difficulty switch menu creation --- osu.Game/Screens/Edit/Editor.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2fead84deb..71a19c4425 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -806,6 +806,15 @@ namespace osu.Game.Screens.Edit fileMenuItems.Add(new EditorMenuItemSpacer()); + fileMenuItems.Add(createDifficultySwitchMenu()); + + fileMenuItems.Add(new EditorMenuItemSpacer()); + fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); + return fileMenuItems; + } + + private EditorMenuItem createDifficultySwitchMenu() + { var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; Debug.Assert(beatmapSet != null); @@ -818,20 +827,13 @@ namespace osu.Game.Screens.Edit difficultyItems.Add(new EditorMenuItemSpacer()); foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating)) - difficultyItems.Add(createDifficultyMenuItem(beatmap)); + { + bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap); + difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty)); + } } - fileMenuItems.Add(new EditorMenuItem("Change difficulty") { Items = difficultyItems }); - - fileMenuItems.Add(new EditorMenuItemSpacer()); - fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); - return fileMenuItems; - } - - private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo) - { - bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo); - return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty); + return new EditorMenuItem("Change difficulty") { Items = difficultyItems }; } protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap)); From 3386f038ba0857f47934c438d7d8a7024e06285c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 16:34:02 +0100 Subject: [PATCH 045/136] Add new difficulty creation menu --- osu.Game/Screens/Edit/Editor.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 71a19c4425..61c5fd2ca4 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -77,6 +77,9 @@ namespace osu.Game.Screens.Edit [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved] + private RulesetStore rulesets { get; set; } + [Resolved] private Storage storage { get; set; } @@ -806,6 +809,7 @@ namespace osu.Game.Screens.Edit fileMenuItems.Add(new EditorMenuItemSpacer()); + fileMenuItems.Add(createDifficultyCreationMenu()); fileMenuItems.Add(createDifficultySwitchMenu()); fileMenuItems.Add(new EditorMenuItemSpacer()); @@ -813,6 +817,16 @@ namespace osu.Game.Screens.Edit return fileMenuItems; } + private EditorMenuItem createDifficultyCreationMenu() + { + var rulesetItems = new List(); + + foreach (var ruleset in rulesets.AvailableRulesets.OrderBy(ruleset => ruleset.OnlineID)) + rulesetItems.Add(new EditorMenuItem(ruleset.Name)); + + return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; + } + private EditorMenuItem createDifficultySwitchMenu() { var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; From b613aedeb81ea46bcb664b9b76feac179252778f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 16:36:18 +0100 Subject: [PATCH 046/136] Fix menu item width changing when hovered --- osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 4267b82bb7..4ecc543ffd 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -117,6 +117,7 @@ namespace osu.Game.Graphics.UserInterface { NormalText = new OsuSpriteText { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: text_size), @@ -124,7 +125,7 @@ namespace osu.Game.Graphics.UserInterface }, BoldText = new OsuSpriteText { - AlwaysPresent = true, + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. Alpha = 0, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, From dc96c4888bfa5fa04ddd4228b9bf218d206a71c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 17:49:17 +0100 Subject: [PATCH 047/136] Add support for creating new blank difficulties --- osu.Game/Beatmaps/BeatmapManager.cs | 32 +++++++++++++++++++++- osu.Game/Beatmaps/BeatmapMetadata.cs | 16 ++++++++++- osu.Game/Beatmaps/BeatmapModelManager.cs | 23 ++++++++++++++-- osu.Game/Database/RealmObjectExtensions.cs | 11 +++++++- osu.Game/Models/RealmUser.cs | 6 +++- osu.Game/Screens/Edit/Editor.cs | 7 +++-- osu.Game/Screens/Edit/EditorLoader.cs | 11 ++++++-- 7 files changed, 95 insertions(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e4fdb3d471..38ba244f28 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -73,7 +73,9 @@ namespace osu.Game.Beatmaps new BeatmapModelManager(realm, storage, onlineLookupQueue); /// - /// Create a new . + /// Create a new beatmap set, backed by a model, + /// with a single difficulty which is backed by a model + /// and represented by the returned usable . /// public WorkingBeatmap CreateNew(RulesetInfo ruleset, APIUser user) { @@ -105,6 +107,34 @@ namespace osu.Game.Beatmaps return imported.PerformRead(s => GetWorkingBeatmap(s.Beatmaps.First())); } + /// + /// Add a new difficulty to the beatmap set represented by the provided . + /// The new difficulty will be backed by a model + /// and represented by the returned . + /// + public WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + { + // fetch one of the existing difficulties to copy timing points and metadata from, + // so that the user doesn't have to fill all of that out again. + // this silently assumes that all difficulties have the same timing points and metadata, + // but cases where this isn't true seem rather rare / pathological. + var referenceBeatmap = GetWorkingBeatmap(beatmapSetInfo.Beatmaps.First()); + + var newBeatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()) + }; + + foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) + newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + + var createdBeatmapInfo = beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); + return GetWorkingBeatmap(createdBeatmapInfo); + } + + // TODO: add back support for making a copy of another difficulty + // (likely via a separate `CopyDifficulty()` method). + /// /// Delete a beatmap difficulty. /// diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index f6666a6ea9..3a24c4808f 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Models; using osu.Game.Users; +using osu.Game.Utils; using Realms; #nullable enable @@ -16,7 +17,7 @@ namespace osu.Game.Beatmaps [ExcludeFromDynamicCompile] [Serializable] [MapTo("BeatmapMetadata")] - public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo + public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo, IDeepCloneable { public string Title { get; set; } = string.Empty; @@ -57,5 +58,18 @@ namespace osu.Game.Beatmaps IUser IBeatmapMetadataInfo.Author => Author; public override string ToString() => this.GetDisplayTitle(); + + public BeatmapMetadata DeepClone() => new BeatmapMetadata(Author.DeepClone()) + { + Title = Title, + TitleUnicode = TitleUnicode, + Artist = Artist, + ArtistUnicode = ArtistUnicode, + Source = Source, + Tags = Tags, + PreviewTime = PreviewTime, + AudioFile = AudioFile, + BackgroundFile = BackgroundFile + }; } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index e8104f2ecb..2ab5ac1db9 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -49,7 +49,6 @@ namespace osu.Game.Beatmaps public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) { var setInfo = beatmapInfo.BeatmapSet; - Debug.Assert(setInfo != null); // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`. @@ -85,6 +84,24 @@ namespace osu.Game.Beatmaps WorkingBeatmapCache?.Invalidate(beatmapInfo); } + /// + /// Add a new difficulty to the beatmap set represented by the provided . + /// + public BeatmapInfo AddDifficultyToBeatmapSet(BeatmapSetInfo beatmapSetInfo, Beatmap beatmap) + { + return Realm.Run(realm => + { + var beatmapInfo = beatmap.BeatmapInfo; + + beatmapSetInfo.Beatmaps.Add(beatmapInfo); + beatmapInfo.BeatmapSet = beatmapSetInfo; + + Save(beatmapInfo, beatmap); + + return beatmapInfo.Detach(); + }); + } + private static string getFilename(BeatmapInfo beatmapInfo) { var metadata = beatmapInfo.Metadata; @@ -103,9 +120,9 @@ namespace osu.Game.Beatmaps public void Update(BeatmapSetInfo item) { - Realm.Write(realm => + Realm.Write(r => { - var existing = realm.Find(item.ID); + var existing = r.Find(item.ID); item.CopyChangesToRealm(existing); }); } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 7a0ca2c85a..f89bbbe19d 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -58,7 +58,16 @@ namespace osu.Game.Database if (existing != null) copyChangesToRealm(beatmap, existing); else - d.Beatmaps.Add(beatmap); + { + var newBeatmap = new BeatmapInfo + { + ID = beatmap.ID, + BeatmapSet = d, + Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName) + }; + d.Beatmaps.Add(newBeatmap); + copyChangesToRealm(beatmap, newBeatmap); + } } }); diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs index 5fccff597c..18c849cf0a 100644 --- a/osu.Game/Models/RealmUser.cs +++ b/osu.Game/Models/RealmUser.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Database; using osu.Game.Users; +using osu.Game.Utils; using Realms; namespace osu.Game.Models { - public class RealmUser : EmbeddedObject, IUser, IEquatable + public class RealmUser : EmbeddedObject, IUser, IEquatable, IDeepCloneable { public int OnlineID { get; set; } = 1; @@ -22,5 +24,7 @@ namespace osu.Game.Models return OnlineID == other.OnlineID && Username == other.Username; } + + public RealmUser DeepClone() => (RealmUser)this.Detach().MemberwiseClone(); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 61c5fd2ca4..df8e326c5b 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -822,11 +822,14 @@ namespace osu.Game.Screens.Edit var rulesetItems = new List(); foreach (var ruleset in rulesets.AvailableRulesets.OrderBy(ruleset => ruleset.OnlineID)) - rulesetItems.Add(new EditorMenuItem(ruleset.Name)); + rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => createNewDifficulty(ruleset))); return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; } + private void createNewDifficulty(RulesetInfo rulesetInfo) + => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState()); + private EditorMenuItem createDifficultySwitchMenu() { var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; @@ -850,7 +853,7 @@ namespace osu.Game.Screens.Edit return new EditorMenuItem("Change difficulty") { Items = difficultyItems }; } - protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap)); + protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap)); private void cancelExit() { diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 15d70e28b6..731bc75b52 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -10,6 +10,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -78,7 +79,13 @@ namespace osu.Game.Screens.Edit } } - public void ScheduleDifficultySwitch(BeatmapInfo nextBeatmap, EditorState editorState) + public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState) + => scheduleDifficultySwitch(() => beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo), editorState); + + public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) + => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); + + private void scheduleDifficultySwitch(Func nextBeatmap, EditorState editorState) { scheduledDifficultySwitch?.Cancel(); ValidForResume = true; @@ -87,7 +94,7 @@ namespace osu.Game.Screens.Edit scheduledDifficultySwitch = Schedule(() => { - Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextBeatmap); + Beatmap.Value = nextBeatmap.Invoke(); state = editorState; // This screen is a weird exception to the rule that nothing after song select changes the global beatmap. From 0d51c015addfcd10fdcc24f6ef37446ef7c1fdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 18:34:33 +0100 Subject: [PATCH 048/136] Add basic test coverage for new difficulty creation --- .../Editing/TestSceneEditorBeatmapCreation.cs | 40 +++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 4 +- osu.Game/Tests/Visual/EditorTestScene.cs | 2 + 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index e3fb44534b..9ceff86426 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -90,5 +90,45 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); } + + [Test] + public void TestCreateNewDifficulty() + { + string firstDifficultyName = Guid.NewGuid().ToString(); + string secondDifficultyName = Guid.NewGuid().ToString(); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == firstDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); + }); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != firstDifficultyName; + }); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == secondDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + }); + } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index df8e326c5b..3d7677ce19 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -822,12 +822,12 @@ namespace osu.Game.Screens.Edit var rulesetItems = new List(); foreach (var ruleset in rulesets.AvailableRulesets.OrderBy(ruleset => ruleset.OnlineID)) - rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => createNewDifficulty(ruleset))); + rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset))); return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; } - private void createNewDifficulty(RulesetInfo rulesetInfo) + protected void CreateNewDifficulty(RulesetInfo rulesetInfo) => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState()); private EditorMenuItem createDifficultySwitchMenu() diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index bcf169bb1e..52d61ed422 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -107,6 +107,8 @@ namespace osu.Game.Tests.Visual public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo); + public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo); + public new bool HasUnsavedChanges => base.HasUnsavedChanges; public TestEditor(EditorLoader loader = null) From 54bb6ad40c992cb55de194ed12aac4686fea6ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 18:56:19 +0100 Subject: [PATCH 049/136] Fix working beatmaps not seeing new difficulties after add --- osu.Game/Beatmaps/BeatmapManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 38ba244f28..d9b0ec3170 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -129,6 +129,8 @@ namespace osu.Game.Beatmaps newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); var createdBeatmapInfo = beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); + + workingBeatmapCache.Invalidate(createdBeatmapInfo.BeatmapSet); return GetWorkingBeatmap(createdBeatmapInfo); } From 87e2e83288ec72a569207d0854dbba49bf4880b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:25:59 +0100 Subject: [PATCH 050/136] Add test coverage for difficulty name clash cases --- .../Editing/TestSceneEditorBeatmapCreation.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 9ceff86426..31348c1e17 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -130,5 +130,59 @@ namespace osu.Game.Tests.Visual.Editing && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); }); } + + [Test] + public void TestCreateNewBeatmapFailsWithBlankNamedDifficulties() + { + Guid setId = Guid.Empty; + + AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + + AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddAssert("beatmap set unchanged", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + } + + [Test] + public void TestCreateNewBeatmapFailsWithSameNamedDifficulties() + { + Guid setId = Guid.Empty; + const string duplicate_difficulty_name = "duplicate"; + + AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != duplicate_difficulty_name; + }); + + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); + AddStep("try to save beatmap", () => Editor.Save()); + AddAssert("beatmap set not corrupted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + // the difficulty was already created at the point of the switch. + // what we want to check is that both difficulties do not use the same file. + return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2); + }); + } } } From 4f1aac9345f8e4b292d079b630c2a00e62a264ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:42:07 +0100 Subject: [PATCH 051/136] Add safeties preventing creating multiple difficulties with same name --- osu.Game/Beatmaps/BeatmapModelManager.cs | 6 ++++++ osu.Game/Screens/Edit/Editor.cs | 16 ++++++++++++---- osu.Game/Screens/Edit/EditorLoader.cs | 16 +++++++++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 2ab5ac1db9..327e10e643 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -71,6 +71,12 @@ namespace osu.Game.Beatmaps // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); + string targetFilename = getFilename(beatmapInfo); + + // ensure that two difficulties from the set don't point at the same beatmap file. + if (setInfo.Beatmaps.Any(b => string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) + throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'."); + if (existingFileInfo != null) DeleteFile(setInfo, existingFileInfo); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3d7677ce19..64b23ccda8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -386,12 +386,20 @@ namespace osu.Game.Screens.Edit return; } + try + { + // save the loaded beatmap's data stream. + beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); + } + catch (Exception ex) + { + // can fail e.g. due to duplicated difficulty names. + Logger.Error(ex, ex.Message); + return; + } + // no longer new after first user-triggered save. isNewBeatmap = false; - - // save the loaded beatmap's data stream. - beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); - updateLastSavedHash(); } diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 731bc75b52..de47411fdc 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; @@ -80,7 +81,20 @@ namespace osu.Game.Screens.Edit } public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState) - => scheduleDifficultySwitch(() => beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo), editorState); + => scheduleDifficultySwitch(() => + { + try + { + return beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo); + } + catch (Exception ex) + { + // if the beatmap creation fails (e.g. due to duplicated difficulty names), + // bring the user back to the previous beatmap as a best-effort. + Logger.Error(ex, ex.Message); + return Beatmap.Value; + } + }, editorState); public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); From afc48d86df1be79ca37d02cb3fdee604d7f7ecaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:50:02 +0100 Subject: [PATCH 052/136] Add failing test coverage for save after safeties addition --- .../Editing/TestSceneEditorBeatmapCreation.cs | 1 + osu.Game/Screens/Edit/Editor.cs | 13 +++++++++---- osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 31348c1e17..a14c9aded3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -109,6 +109,7 @@ namespace osu.Game.Tests.Visual.Editing && set != null && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); }); + AddAssert("can save again", () => Editor.Save()); AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); AddUntilStep("wait for created", () => diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 64b23ccda8..78c5c862f7 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -378,12 +378,16 @@ namespace osu.Game.Screens.Edit Clipboard.Content.Value = state.ClipboardContent; }); - protected void Save() + /// + /// Saves the currently edited beatmap. + /// + /// Whether the save was successful. + protected bool Save() { if (!canSave) { notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" }); - return; + return false; } try @@ -395,12 +399,13 @@ namespace osu.Game.Screens.Edit { // can fail e.g. due to duplicated difficulty names. Logger.Error(ex, ex.Message); - return; + return false; } // no longer new after first user-triggered save. isNewBeatmap = false; updateLastSavedHash(); + return true; } protected override void Update() @@ -809,7 +814,7 @@ namespace osu.Game.Screens.Edit { var fileMenuItems = new List { - new EditorMenuItem("Save", MenuItemType.Standard, Save) + new EditorMenuItem("Save", MenuItemType.Standard, () => Save()) }; if (RuntimeInfo.IsDesktop) diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 52d61ed422..da13c6862f 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual public new void Redo() => base.Redo(); - public new void Save() => base.Save(); + public new bool Save() => base.Save(); public new void Cut() => base.Cut(); From 47429fb0c68b6df09b73056a8a7a5b8d3fe2e4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:54:57 +0100 Subject: [PATCH 053/136] Fix same-name safety firing wrongly --- osu.Game/Beatmaps/BeatmapModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 327e10e643..d4df4b7870 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps string targetFilename = getFilename(beatmapInfo); // ensure that two difficulties from the set don't point at the same beatmap file. - if (setInfo.Beatmaps.Any(b => string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) + if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'."); if (existingFileInfo != null) From a8ffc4fc2a08b27af6e295ee97cfee3d208d4bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:58:21 +0100 Subject: [PATCH 054/136] Add editor override to respect `IsolateSavingFromDatabase` --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Tests/Visual/EditorTestScene.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d9b0ec3170..9c9b995955 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -112,7 +112,7 @@ namespace osu.Game.Beatmaps /// The new difficulty will be backed by a model /// and represented by the returned . /// - public WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + public virtual WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) { // fetch one of the existing difficulties to copy timing points and metadata from, // so that the user doesn't have to fill all of that out again. diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index da13c6862f..331bf04644 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -136,6 +136,12 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } + public override WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + { + // don't actually care about properly creating a difficulty for this context. + return TestBeatmap; + } + private class TestWorkingBeatmapCache : WorkingBeatmapCache { private readonly TestBeatmapManager testBeatmapManager; From 62537eb4aa5f458fd959846a8bf318b16e2acef0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 12:44:33 +0900 Subject: [PATCH 055/136] Fix spectator not completing --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 21 +++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 0b2a7cecf6..460352ee07 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -112,6 +112,7 @@ namespace osu.Game.Screens.Spectate switch (e.Action) { case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Replace: foreach ((int userId, var state) in e.NewItems.AsNonNull()) onUserStateChanged(userId, state); break; @@ -120,11 +121,6 @@ namespace osu.Game.Screens.Spectate foreach ((int userId, _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); break; - - case NotifyDictionaryChangedAction.Replace: - foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateChanged(userId, state); - break; } } @@ -136,11 +132,18 @@ namespace osu.Game.Screens.Spectate if (!userMap.ContainsKey(userId)) return; - // Do nothing for failed/completed states. - if (newState.State == SpectatingUserState.Playing) + switch (newState.State) { - Schedule(() => OnUserStateChanged(userId, newState)); - updateGameplayState(userId); + case SpectatingUserState.Completed: + // Make sure that gameplay completes to the end. + if (gameplayStates.TryGetValue(userId, out var gameplayState)) + gameplayState.Score.Replay.HasReceivedAllFrames = true; + break; + + case SpectatingUserState.Playing: + Schedule(() => OnUserStateChanged(userId, newState)); + updateGameplayState(userId); + break; } } From aff36d4e163c96c9dd35ea280a1b630806b63d10 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Feb 2022 11:52:37 +0800 Subject: [PATCH 056/136] Refactor `populateStatistics` to avoid disposing --- .../Ranking/Statistics/StatisticsPanel.cs | 128 +++++++++--------- 1 file changed, 63 insertions(+), 65 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 08be1de652..cbb4e5089d 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -86,88 +86,86 @@ namespace osu.Game.Screens.Ranking.Statistics playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); }, loadCancellation.Token).ContinueWith(t => Schedule(() => { - FillFlowContainer rows; - Container container = new OsuScrollContainer(Direction.Vertical) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - Children = new[] - { - rows = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(30, 15) - } - } - }; - - bool panelIsEmpty = true; - bool panelIsComplete = true; bool hitEventsAvailable = newScore.HitEvents.Count != 0; + Container container; - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap); + + if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents)) { - var columnsToDisplay = hitEventsAvailable - ? row.Columns - : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); - - if (columnsToDisplay.Length < row.Columns.Length) - panelIsComplete = false; - - if (columnsToDisplay.Any()) - panelIsEmpty = false; - else - continue; - - rows.Add(new GridContainer + container = new FillFlowContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - columnsToDisplay?.Select(c => new StatisticContainer(c) + new MessagePlaceholder("Extended statistics are only available after watching a replay!"), + new ReplayDownloadButton(newScore) { + Scale = new Vector2(1.5f), Anchor = Anchor.Centre, Origin = Anchor.Centre, - }).Cast().ToArray() - }, - ColumnDimensions = Enumerable.Range(0, columnsToDisplay.Length) - .Select(i => columnsToDisplay[i].Dimension ?? new Dimension()).ToArray(), - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); + }, + } + }; } - - if (!hitEventsAvailable) + else { - if (panelIsEmpty) + FillFlowContainer rows; + container = new OsuScrollContainer(Direction.Vertical) { - // Replace the scroll container with fill flow container to get the message centered. - rows.Dispose(); - container.Dispose(); - container = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - Children = new Drawable[] + rows = new FillFlowContainer { - new MessagePlaceholder("Extended statistics are only available after watching a replay!"), - new ReplayDownloadButton(newScore) + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(30, 15) + } + } + }; + + bool panelIsComplete = true; + + foreach (var row in statisticRows) + { + var columnsToDisplay = hitEventsAvailable + ? row.Columns + : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); + + if (columnsToDisplay.Length < row.Columns.Length) + panelIsComplete = false; + + if (columnsToDisplay.Length == 0) + continue; + + rows.Add(new GridContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] + { + columnsToDisplay?.Select(c => new StatisticContainer(c) { - Scale = new Vector2(1.5f), Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, - } - }; + }).Cast().ToArray() + }, + ColumnDimensions = Enumerable.Range(0, columnsToDisplay.Length) + .Select(i => columnsToDisplay[i].Dimension ?? new Dimension()).ToArray(), + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); } - else if (!panelIsComplete) + + if (!hitEventsAvailable && !panelIsComplete) { rows.Add(new FillFlowContainer { From a27d0572ed3d5c8b4c5f082015de2f6c33b86005 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Feb 2022 17:00:40 +0800 Subject: [PATCH 057/136] Add test cases for manual testing --- .../Ranking/TestSceneStatisticsPanel.cs | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index f64b7b2b65..35281a85eb 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -6,10 +6,18 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; using osuTK; @@ -41,6 +49,24 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(TestResources.CreateTestScoreInfo()); } + [Test] + public void TestScoreInRulesetWhereAllStatsRequireHitEvents() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetAllStatsRequireHitEvents().RulesetInfo)); + } + + [Test] + public void TestScoreInRulesetWhereNoStatsRequireHitEvents() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetNoStatsRequireHitEvents().RulesetInfo)); + } + + [Test] + public void TestScoreInMixedRuleset() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetMixed().RulesetInfo)); + } + [Test] public void TestNullScore() { @@ -75,5 +101,134 @@ namespace osu.Game.Tests.Visual.Ranking return hitEvents; } + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) + { + throw new NotImplementedException(); + } + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) + { + throw new NotImplementedException(); + } + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) + { + throw new NotImplementedException(); + } + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) + { + throw new NotImplementedException(); + } + + public override string Description => string.Empty; + + public override string ShortName => string.Empty; + + protected static Drawable CreatePlaceholderStatistic(string message) => new Container + { + RelativeSizeAxes = Axes.X, + Masking = true, + CornerRadius = 20, + Height = 250, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.5f), + Alpha = 0.5f + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Text = message, + Margin = new MarginPadding { Left = 20 } + } + } + }; + } + + private class TestRulesetAllStatsRequireHitEvents : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events 1", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events 2", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + } + }; + } + } + + private class TestRulesetNoStatsRequireHitEvents : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events 1", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events 2", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + } + }; + } + } + + private class TestRulesetMixed : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + } + }; + } + } } } From 6974c2d255f400be9401479df9e782c79a2c04b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 19:00:03 +0900 Subject: [PATCH 058/136] Remove weird `panelIsComplete` flag and replace LINQ with simple `foreach` --- .../Ranking/Statistics/StatisticsPanel.cs | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index cbb4e5089d..898bd69b2c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . 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; @@ -131,41 +132,48 @@ namespace osu.Game.Screens.Ranking.Statistics } }; - bool panelIsComplete = true; + bool anyRequiredHitEvents = false; foreach (var row in statisticRows) { - var columnsToDisplay = hitEventsAvailable - ? row.Columns - : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); + var columns = row.Columns; - if (columnsToDisplay.Length < row.Columns.Length) - panelIsComplete = false; - - if (columnsToDisplay.Length == 0) + if (columns.Length == 0) continue; + var columnContent = new List(); + var dimensions = new List(); + + foreach (var col in columns) + { + if (!hitEventsAvailable && col.RequiresHitEvents) + { + anyRequiredHitEvents = true; + continue; + } + + columnContent.Add(new StatisticContainer(col) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + dimensions.Add(col.Dimension ?? new Dimension()); + } + rows.Add(new GridContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] - { - columnsToDisplay?.Select(c => new StatisticContainer(c) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }).Cast().ToArray() - }, - ColumnDimensions = Enumerable.Range(0, columnsToDisplay.Length) - .Select(i => columnsToDisplay[i].Dimension ?? new Dimension()).ToArray(), + Content = new[] { columnContent.ToArray() }, + ColumnDimensions = dimensions.ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } - if (!hitEventsAvailable && !panelIsComplete) + if (anyRequiredHitEvents) { rows.Add(new FillFlowContainer { From 47d577ec9c822edb762c91067fb7c17ea97a766a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 19:17:56 +0900 Subject: [PATCH 059/136] Add back constructor for ruleset compatibility --- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 3cfc38e263..b43fbbdeee 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -33,6 +33,12 @@ namespace osu.Game.Screens.Ranking.Statistics /// public readonly bool RequiresHitEvents; + [Obsolete("Use constructor which takes creation function instead.")] // Can be removed 20220803. + public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) + : this(name, () => content, true, dimension) + { + } + /// /// Creates a new , to be displayed inside a in the results screen. /// From ad47649d1c5044c64c4a4f8d50ff739031019221 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 19:38:53 +0900 Subject: [PATCH 060/136] Make `BeatmapModelManager.Save` non-virtual --- osu.Game/Beatmaps/BeatmapModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index d4df4b7870..a9da221e08 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -46,7 +46,7 @@ namespace osu.Game.Beatmaps /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) + public void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) { var setInfo = beatmapInfo.BeatmapSet; Debug.Assert(setInfo != null); From bef0a2da210c7505adef18314ec12312b3ee59d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 19:39:24 +0900 Subject: [PATCH 061/136] Remove return type from `AddDifficultyToBeatmapSet` Also removes a pointless realm encapsulation. --- osu.Game/Beatmaps/BeatmapManager.cs | 6 +++--- osu.Game/Beatmaps/BeatmapModelManager.cs | 15 +++++---------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 9c9b995955..d60cfdee9e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -128,10 +128,10 @@ namespace osu.Game.Beatmaps foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); - var createdBeatmapInfo = beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); + beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); - workingBeatmapCache.Invalidate(createdBeatmapInfo.BeatmapSet); - return GetWorkingBeatmap(createdBeatmapInfo); + workingBeatmapCache.Invalidate(beatmapSetInfo); + return GetWorkingBeatmap(newBeatmap.BeatmapInfo); } // TODO: add back support for making a copy of another difficulty diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index a9da221e08..b9f0af8833 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -93,19 +93,14 @@ namespace osu.Game.Beatmaps /// /// Add a new difficulty to the beatmap set represented by the provided . /// - public BeatmapInfo AddDifficultyToBeatmapSet(BeatmapSetInfo beatmapSetInfo, Beatmap beatmap) + public void AddDifficultyToBeatmapSet(BeatmapSetInfo beatmapSetInfo, Beatmap beatmap) { - return Realm.Run(realm => - { - var beatmapInfo = beatmap.BeatmapInfo; + var beatmapInfo = beatmap.BeatmapInfo; - beatmapSetInfo.Beatmaps.Add(beatmapInfo); - beatmapInfo.BeatmapSet = beatmapSetInfo; + beatmapSetInfo.Beatmaps.Add(beatmapInfo); + beatmapInfo.BeatmapSet = beatmapSetInfo; - Save(beatmapInfo, beatmap); - - return beatmapInfo.Detach(); - }); + Save(beatmapInfo, beatmap); } private static string getFilename(BeatmapInfo beatmapInfo) From 40953751b542281dca26e24c323e48b73b2a6c60 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Thu, 3 Feb 2022 13:28:49 +0100 Subject: [PATCH 062/136] Use `ScreenOrientation.FullUser` on Android tablets --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- osu.Android/OsuGameActivity.cs | 25 ++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index ab91b4e3b3..1b7893e5b9 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -27,7 +27,7 @@ namespace osu.Android { gameActivity.RunOnUiThread(() => { - gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : OsuGameActivity.DEFAULT_ORIENTATION; + gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.ScreenOrientation; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index e6679b61a6..7de597fe88 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . 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.IO; using System.Linq; @@ -8,16 +9,18 @@ using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Content.PM; -using Android.Net; +using Android.Graphics; using Android.OS; using Android.Provider; using Android.Views; using osu.Framework.Android; using osu.Game.Database; +using Debug = System.Diagnostics.Debug; +using Uri = Android.Net.Uri; namespace osu.Android { - [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = DEFAULT_ORIENTATION)] + [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")] @@ -39,10 +42,10 @@ namespace osu.Android [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { - public const ScreenOrientation DEFAULT_ORIENTATION = ScreenOrientation.SensorLandscape; - private static readonly string[] osu_url_schemes = { "osu", "osump" }; + public ScreenOrientation ScreenOrientation = ScreenOrientation.Unspecified; + private OsuGameAndroid game; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); @@ -56,8 +59,20 @@ namespace osu.Android // reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent) handleIntent(Intent); + Debug.Assert(Window != null); + Window.AddFlags(WindowManagerFlags.Fullscreen); Window.AddFlags(WindowManagerFlags.KeepScreenOn); + + Debug.Assert(WindowManager?.DefaultDisplay != null); + Debug.Assert(Resources?.DisplayMetrics != null); + + Point displaySize = new Point(); + WindowManager.DefaultDisplay.GetSize(displaySize); + float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; + bool isTablet = smallestWidthDp >= 600f; + + RequestedOrientation = ScreenOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; } protected override void OnNewIntent(Intent intent) => handleIntent(intent); @@ -106,7 +121,7 @@ namespace osu.Android cursor.MoveToFirst(); - var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); + int filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); string filename = cursor.GetString(filenameColumn); // SharpCompress requires archive streams to be seekable, which the stream opened by From f2850601486ec6a8b4b4a88d76b13c75fddd0320 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 21:50:15 +0900 Subject: [PATCH 063/136] Fix MultiSpectatorScreen not continuing to results --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 5 ++++- osu.Game/Screens/Play/SoloSpectator.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 16 ++++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 4646f42d63..bbdd7a3d56 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -215,8 +215,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); - protected override void EndGameplay(int userId) + protected override void EndGameplay(int userId, SpectatorState state) { + if (state.State == SpectatingUserState.Completed || state.State == SpectatingUserState.Failed) + return; + RemoveUser(userId); var instance = instances.Single(i => i.UserId == userId); diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index b530965269..a710db6d24 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Play scheduleStart(spectatorGameplayState); } - protected override void EndGameplay(int userId) + protected override void EndGameplay(int userId, SpectatorState state) { scheduledStart?.Cancel(); immediateSpectatorGameplayState = null; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 460352ee07..7b58f669a0 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -118,8 +118,8 @@ namespace osu.Game.Screens.Spectate break; case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, _) in e.OldItems.AsNonNull()) - onUserStateRemoved(userId); + foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull()) + onUserStateRemoved(userId, state); break; } } @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Spectate } } - private void onUserStateRemoved(int userId) + private void onUserStateRemoved(int userId, SpectatorState state) { if (!userMap.ContainsKey(userId)) return; @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; gameplayStates.Remove(userId); - Schedule(() => EndGameplay(userId)); + Schedule(() => EndGameplay(userId, state)); } private void updateGameplayState(int userId) @@ -212,7 +212,8 @@ namespace osu.Game.Screens.Spectate /// Ends gameplay for a user. /// /// The user to end gameplay for. - protected abstract void EndGameplay(int userId); + /// The final user state. + protected abstract void EndGameplay(int userId, SpectatorState state); /// /// Stops spectating a user. @@ -220,7 +221,10 @@ namespace osu.Game.Screens.Spectate /// The user to stop spectating. protected void RemoveUser(int userId) { - onUserStateRemoved(userId); + if (!userStates.TryGetValue(userId, out var state)) + return; + + onUserStateRemoved(userId, state); users.Remove(userId); userMap.Remove(userId); From 84171962e56faa9b5310450d01d0faedeffd2146 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Thu, 3 Feb 2022 13:55:04 +0100 Subject: [PATCH 064/136] Change name and add xmldoc --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- osu.Android/OsuGameActivity.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index 1b7893e5b9..2e83f784d3 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -27,7 +27,7 @@ namespace osu.Android { gameActivity.RunOnUiThread(() => { - gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.ScreenOrientation; + gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.DefaultOrientation; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 7de597fe88..eebd079f68 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -44,7 +44,11 @@ namespace osu.Android { private static readonly string[] osu_url_schemes = { "osu", "osump" }; - public ScreenOrientation ScreenOrientation = ScreenOrientation.Unspecified; + /// + /// The default screen orientation. + /// + /// Adjusted on startup to match expected UX for the current device type (phone/tablet). + public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; private OsuGameAndroid game; @@ -72,7 +76,7 @@ namespace osu.Android float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; bool isTablet = smallestWidthDp >= 600f; - RequestedOrientation = ScreenOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; + RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; } protected override void OnNewIntent(Intent intent) => handleIntent(intent); From d4ebff6ea1f381bad406c692ab15b097fbdefd2b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 23:18:22 +0900 Subject: [PATCH 065/136] Add failing test --- .../Multiplayer/TestSceneMultiplayer.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 3563869d8b..27bf1c209c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -871,6 +871,52 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0); } + [Test] + public void TestGameplayStartsWhileInSpectatorScreen() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddStep("join other user and make host", () => + { + client.AddUser(new APIUser { Id = 1234 }); + client.TransferHost(1234); + }); + + AddStep("set local user spectating", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("wait for spectating state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + + runGameplay(); + + AddStep("exit gameplay for other user", () => client.ChangeUserState(1234, MultiplayerUserState.Idle)); + AddUntilStep("wait for room to be idle", () => client.Room?.State == MultiplayerRoomState.Open); + + runGameplay(); + + void runGameplay() + { + AddStep("start match by other user", () => + { + client.ChangeUserState(1234, MultiplayerUserState.Ready); + client.StartMatch(); + }); + + AddUntilStep("wait for loading", () => client.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddStep("set player loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddUntilStep("wait for gameplay to start", () => client.Room?.State == MultiplayerRoomState.Playing); + } + } + private void enterGameplay() { pressReadyButton(); From 6dc0f3fd960cc5a4fd914123388a0585901e0243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Feb 2022 18:14:30 +0100 Subject: [PATCH 066/136] Merge difficulty creation methods into one One of them wasn't really doing much anymore and was more obfuscating what was actually happening at this point. --- osu.Game/Beatmaps/BeatmapManager.cs | 14 +++++++++----- osu.Game/Beatmaps/BeatmapModelManager.cs | 13 ------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d60cfdee9e..633eb8f15e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -120,15 +120,19 @@ namespace osu.Game.Beatmaps // but cases where this isn't true seem rather rare / pathological. var referenceBeatmap = GetWorkingBeatmap(beatmapSetInfo.Beatmaps.First()); - var newBeatmap = new Beatmap - { - BeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()) - }; + var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); + // populate circular beatmap set info <-> beatmap info references manually. + // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` + // rely on them being freely traversable in both directions for correct operation. + beatmapSetInfo.Beatmaps.Add(newBeatmapInfo); + newBeatmapInfo.BeatmapSet = beatmapSetInfo; + + var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); - beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); + beatmapModelManager.Save(newBeatmapInfo, newBeatmap); workingBeatmapCache.Invalidate(beatmapSetInfo); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index b9f0af8833..4c680bbcc9 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -90,19 +90,6 @@ namespace osu.Game.Beatmaps WorkingBeatmapCache?.Invalidate(beatmapInfo); } - /// - /// Add a new difficulty to the beatmap set represented by the provided . - /// - public void AddDifficultyToBeatmapSet(BeatmapSetInfo beatmapSetInfo, Beatmap beatmap) - { - var beatmapInfo = beatmap.BeatmapInfo; - - beatmapSetInfo.Beatmaps.Add(beatmapInfo); - beatmapInfo.BeatmapSet = beatmapSetInfo; - - Save(beatmapInfo, beatmap); - } - private static string getFilename(BeatmapInfo beatmapInfo) { var metadata = beatmapInfo.Metadata; From ee1feae8062919f2992bd41b23cabd19aa49f345 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 11:06:18 +0900 Subject: [PATCH 067/136] Remove unnecessary ruleset ordering Co-authored-by: Salman Ahmed --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 78c5c862f7..5503a62ba2 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -834,7 +834,7 @@ namespace osu.Game.Screens.Edit { var rulesetItems = new List(); - foreach (var ruleset in rulesets.AvailableRulesets.OrderBy(ruleset => ruleset.OnlineID)) + foreach (var ruleset in rulesets.AvailableRulesets) rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset))); return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; From 4728919bcaaa7151f3d3036a629212f8e395f570 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 15:45:27 +0900 Subject: [PATCH 068/136] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f89994cd56..04c543750e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 50cef71b26..83c3593edb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 9ec0f1c0a0..b0c056ea21 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 52fdf0349f32a73ad1022a9b5ef26de9c39edc36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 16:07:05 +0900 Subject: [PATCH 069/136] Add safe area support via `ScalingContainer` --- .../Graphics/Containers/ScalingContainer.cs | 10 +++++++- osu.Game/OsuGameBase.cs | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index d2b1e5e523..f9bd571131 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -101,6 +101,9 @@ namespace osu.Game.Graphics.Containers } } + [Resolved] + private ISafeArea safeArea { get; set; } + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -118,6 +121,8 @@ namespace osu.Game.Graphics.Containers posY = config.GetBindable(OsuSetting.ScalingPositionY); posY.ValueChanged += _ => updateSize(); + + safeArea.SafeAreaPadding.BindValueChanged(_ => updateSize()); } 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 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 && safeArea.SafeAreaPadding.Value.Total != Vector2.Zero); if (requiresMasking) sizableContainer.Masking = true; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1713e73905..5f87abcfed 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -299,16 +299,22 @@ namespace osu.Game GlobalActionContainer globalBindings; - var mainContent = new Drawable[] + base.Content.Add(new SafeAreaContainer { - MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }, - // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. - globalBindings = new GlobalActionContainer(this) - }; - - MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }; - - base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); + 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. + globalBindings = new GlobalActionContainer(this) + }) + }); KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); From 1444df4d50ce0e2d9417f0958d4a39f76ae8e85e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 16:37:11 +0900 Subject: [PATCH 070/136] Add test scene for playing with safe areas --- .../TestSceneSafeAreaHandling.cs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs new file mode 100644 index 0000000000..676ae1276b --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs @@ -0,0 +1,116 @@ +// Copyright (c) ppy Pty Ltd . 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 safeAreaPaddingTop = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable safeAreaPaddingBottom = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable safeAreaPaddingLeft = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable 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(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 + { + Current = safeAreaPaddingTop, + LabelText = "Top" + }, + new SettingsSlider + { + Current = safeAreaPaddingBottom, + LabelText = "Bottom" + }, + new SettingsSlider + { + Current = safeAreaPaddingLeft, + LabelText = "Left" + }, + new SettingsSlider + { + 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() + { + } + } +} From 30d2c7ba6a9355e88a3f605dd68d23728ede247a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 17:07:21 +0900 Subject: [PATCH 071/136] Add parenthesis to disambiguify conditionals --- osu.Game/Graphics/Containers/ScalingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index f9bd571131..aa4e3a7fde 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -166,7 +166,7 @@ namespace osu.Game.Graphics.Containers 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; - 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 && safeArea.SafeAreaPadding.Value.Total != Vector2.Zero); From b41655d5b905cd28425e2851f6900f24f7a82893 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 23:22:08 +0900 Subject: [PATCH 072/136] Fix crash when gameplay starts while in multi-spectator screen --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 1 + .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 27bf1c209c..8f6ba6375f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -914,6 +914,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for loading", () => client.Room?.State == MultiplayerRoomState.WaitingForLoad); AddStep("set player loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); AddUntilStep("wait for gameplay to start", () => client.Room?.State == MultiplayerRoomState.Playing); + AddUntilStep("wait for local user to enter spectator", () => multiplayerComponents.CurrentScreen is MultiSpectatorScreen); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4bd68f2034..020217eac6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -457,6 +457,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } + // The beatmap is queried asynchronously when the selected item changes. + // This is an issue with MultiSpectatorScreen which is effectively in an always "ready" state and receives LoadRequested() callbacks + // even when it is not truly ready (i.e. the beatmap hasn't been selected by the client yet). For the time being, a simple fix to this is to ignore the callback. + // Note that spectator will be entered automatically when the client is capable of doing so via beatmap availability callbacks (see: updateBeatmapAvailability()). + if (client.LocalUser?.State == MultiplayerUserState.Spectating && Beatmap.IsDefault) + return; + StartPlay(); readyClickOperation?.Dispose(); From 0473c6c52f9156954eb7c782a421a540cbc35e5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Feb 2022 17:53:30 +0900 Subject: [PATCH 073/136] Also handle null SelectedItem for safety --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 020217eac6..a397493bab 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -461,7 +461,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // This is an issue with MultiSpectatorScreen which is effectively in an always "ready" state and receives LoadRequested() callbacks // even when it is not truly ready (i.e. the beatmap hasn't been selected by the client yet). For the time being, a simple fix to this is to ignore the callback. // Note that spectator will be entered automatically when the client is capable of doing so via beatmap availability callbacks (see: updateBeatmapAvailability()). - if (client.LocalUser?.State == MultiplayerUserState.Spectating && Beatmap.IsDefault) + if (client.LocalUser?.State == MultiplayerUserState.Spectating && (SelectedItem.Value == null || Beatmap.IsDefault)) return; StartPlay(); From 8fc4d0c6f51053184b5d8952214aa652cf6f7c1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 18:20:17 +0900 Subject: [PATCH 074/136] Add override edge rule to overflow above home indicator on iOS --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5f87abcfed..594e7a10c4 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -301,6 +301,7 @@ namespace osu.Game base.Content.Add(new SafeAreaContainer { + SafeAreaOverrideEdges = Edges.Bottom, RelativeSizeAxes = Axes.Both, Child = CreateScalingContainer().WithChildren(new Drawable[] { From 6457cf8d9b70264e46f757b01116b85ef393c163 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 18:45:38 +0900 Subject: [PATCH 075/136] Fix weird formatting in `TestSceneSafeArea` --- .../Visual/UserInterface/TestSceneSafeAreaHandling.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs index 676ae1276b..8b4e3f6d3a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs @@ -29,11 +29,10 @@ namespace osu.Game.Tests.Visual.UserInterface // 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 - }); + Add(safeAreaContainer = new SafeAreaDefiningContainer(safeArea = new BindableSafeArea()) + { + RelativeSizeAxes = Axes.Both + }); // Cache is required for the test game to see the safe area. Dependencies.CacheAs(safeAreaContainer); From 915d63f6dea742d853f7e635c3d18093d30de815 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 18:58:29 +0900 Subject: [PATCH 076/136] Limit safe area bottom override to iOS only --- osu.Game/OsuGameBase.cs | 8 +++++++- osu.iOS/OsuGameIOS.cs | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 594e7a10c4..97d2e64072 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -89,6 +89,12 @@ namespace osu.Game } } + /// + /// The that the game should be drawn over at a top level. + /// Defaults to . + /// + public virtual Edges SafeAreaOverrideEdges { get; set; } + protected OsuConfigManager LocalConfig { get; private set; } protected SessionStatics SessionStatics { get; private set; } @@ -301,7 +307,7 @@ namespace osu.Game base.Content.Add(new SafeAreaContainer { - SafeAreaOverrideEdges = Edges.Bottom, + SafeAreaOverrideEdges = SafeAreaOverrideEdges, RelativeSizeAxes = Axes.Both, Child = CreateScalingContainer().WithChildren(new Drawable[] { diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 702aef45f5..cf14745be1 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -3,6 +3,7 @@ using System; using Foundation; +using osu.Framework.Graphics; using osu.Game; using osu.Game.Updater; using osu.Game.Utils; @@ -18,6 +19,11 @@ namespace osu.iOS protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); + public 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 { public override double ChargeLevel => Battery.ChargeLevel; From dd63b1a3500be04b494623a51806121364720ba2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Feb 2022 19:03:52 +0900 Subject: [PATCH 077/136] Fix broken spectator playback test scene --- .../Gameplay/TestSceneSpectatorPlayback.cs | 221 +++++++----------- 1 file changed, 85 insertions(+), 136 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 4af254866a..69bf2a7a38 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -3,12 +3,8 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -20,7 +16,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; @@ -32,6 +27,7 @@ using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Tests.Visual.Spectator; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -47,138 +43,105 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private readonly IBindableList users = new BindableList(); - - private TestReplayRecorder recorder; - private ManualClock manualClock; private OsuSpriteText latencyDisplay; private TestFramedReplayInputHandler replayHandler; - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private SpectatorClient spectatorClient { get; set; } - - [Cached] - private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); - [SetUpSteps] public void SetUpSteps() { - AddStep("Reset recorder state", cleanUpState); - AddStep("Setup containers", () => { replay = new Replay(); manualClock = new ManualClock(); + SpectatorClient spectatorClient; + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] + { + (typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())), + (typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty())) + }, + Children = new Drawable[] + { + spectatorClient, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Recorder = new TestReplayRecorder + { + ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Sending", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + }, + new Drawable[] + { + playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Clock = new FramedClock(manualClock), + ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) + { + GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Receiving", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + } + } + }, + latencyDisplay = new OsuSpriteText() + } + }; spectatorClient.OnNewFrames += onNewFrames; - - users.BindTo(spectatorClient.PlayingUsers); - users.BindCollectionChanged((obj, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); - - foreach (int user in args.NewItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.WatchUser(user); - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); - - foreach (int user in args.OldItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.StopWatchingUser(user); - } - - break; - } - }, true); - - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - Recorder = recorder = new TestReplayRecorder - { - ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.Brown, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Sending", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, - } - }, - new Drawable[] - { - playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - Clock = new FramedClock(manualClock), - ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) - { - GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.DarkBlue, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Receiving", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, - } - } - } - }, - latencyDisplay = new OsuSpriteText() - }; }); } @@ -238,20 +201,6 @@ namespace osu.Game.Tests.Visual.Gameplay manualClock.CurrentTime = time.Value; } - [TearDownSteps] - public void TearDown() - { - AddStep("stop recorder", cleanUpState); - } - - private void cleanUpState() - { - // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`. - recorder?.RemoveAndDisposeImmediately(); - recorder = null; - spectatorClient.OnNewFrames -= onNewFrames; - } - public class TestFramedReplayInputHandler : FramedReplayInputHandler { public TestFramedReplayInputHandler(Replay replay) From fa3d1115fa9535d288e9cc74756b3cc68548ed5b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Feb 2022 19:17:50 +0900 Subject: [PATCH 078/136] Remove online api requirement --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 69bf2a7a38..a4d8460846 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -36,8 +36,6 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene { - protected override bool UseOnlineAPI => true; - private TestRulesetInputManager playbackManager; private TestRulesetInputManager recordingManager; From 503025b970b1df6bb4935c13df7c4f9d82e56036 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 19:19:44 +0900 Subject: [PATCH 079/136] Fix completely incorrect and dangerous usage of bindable binding --- osu.Game/Graphics/Containers/ScalingContainer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index aa4e3a7fde..f505a62a33 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers private Bindable posX; private Bindable posY; + private Bindable safeAreaPadding; + private readonly ScalingMode? targetMode; private Bindable scalingMode; @@ -101,11 +103,8 @@ namespace osu.Game.Graphics.Containers } } - [Resolved] - private ISafeArea safeArea { get; set; } - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, ISafeArea safeArea) { scalingMode = config.GetBindable(OsuSetting.Scaling); scalingMode.ValueChanged += _ => updateSize(); @@ -122,7 +121,8 @@ namespace osu.Game.Graphics.Containers posY = config.GetBindable(OsuSetting.ScalingPositionY); posY.ValueChanged += _ => updateSize(); - safeArea.SafeAreaPadding.BindValueChanged(_ => updateSize()); + safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy(); + safeAreaPadding.BindValueChanged(_ => updateSize()); } protected override void LoadComplete() @@ -169,7 +169,7 @@ namespace osu.Game.Graphics.Containers 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 && safeArea.SafeAreaPadding.Value.Total != Vector2.Zero); + || (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero); if (requiresMasking) sizableContainer.Masking = true; From 0f48c0131ca99a6b9f3283ebbd037cd991e181b6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 4 Feb 2022 15:42:52 +0900 Subject: [PATCH 080/136] Layer playback of beatmap-changed and random-beatmap samples --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++++ osu.Game/Screens/Select/SongSelect.cs | 21 +++------------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f17daa8697..3d5ed70dda 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -154,6 +154,7 @@ namespace osu.Game.Screens.Select private readonly DrawablePool setPool = new DrawablePool(100); private Sample spinSample; + private Sample randomSelectSample; private int visibleSetsCount; @@ -178,6 +179,7 @@ namespace osu.Game.Screens.Select private void load(OsuConfigManager config, AudioManager audio) { spinSample = audio.Samples.Get("SongSelect/random-spin"); + randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); @@ -495,6 +497,8 @@ namespace osu.Game.Screens.Select var chan = spinSample.GetChannel(); chan.Frequency.Value = 1f + Math.Min(1f, distance / visibleSetsCount); chan.Play(); + + randomSelectSample?.Play(); } private void select(CarouselItem item) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ee807762bf..f5b11448f8 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -100,7 +100,6 @@ namespace osu.Game.Screens.Select private Sample sampleChangeDifficulty; private Sample sampleChangeBeatmap; - private Sample sampleRandomBeatmap; private Container carouselContainer; @@ -110,8 +109,6 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; - private bool randomSelectionPending; - [Resolved] private MusicController music { get; set; } @@ -291,7 +288,6 @@ namespace osu.Game.Screens.Select sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand"); - sampleRandomBeatmap = audio.Samples.Get(@"SongSelect/select-random"); SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); if (dialogOverlay != null) @@ -319,16 +315,8 @@ namespace osu.Game.Screens.Select (new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom { - NextRandom = () => - { - randomSelectionPending = true; - Carousel.SelectNextRandom(); - }, - PreviousRandom = () => - { - randomSelectionPending = true; - Carousel.SelectPreviousRandom(); - } + NextRandom = () => Carousel.SelectNextRandom(), + PreviousRandom = Carousel.SelectPreviousRandom }, null), (new FooterButtonOptions(), BeatmapOptions) }; @@ -498,9 +486,7 @@ namespace osu.Game.Screens.Select { if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50) { - if (randomSelectionPending) - sampleRandomBeatmap.Play(); - else if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) + if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) sampleChangeDifficulty.Play(); else sampleChangeBeatmap.Play(); @@ -508,7 +494,6 @@ namespace osu.Game.Screens.Select audioFeedbackLastPlaybackTime = Time.Current; } - randomSelectionPending = false; beatmapInfoPrevious = beatmap; } From e2262bf3b25f14f625e76bec34118203299c31d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 20:33:15 +0900 Subject: [PATCH 081/136] Schedule all calls to `updateSize` for safety --- osu.Game/Graphics/Containers/ScalingContainer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index f505a62a33..b3423345b1 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Graphics.Containers return; allowScaling = value; - if (IsLoaded) updateSize(); + if (IsLoaded) Scheduler.AddOnce(updateSize); } } @@ -107,29 +107,29 @@ namespace osu.Game.Graphics.Containers private void load(OsuConfigManager config, ISafeArea safeArea) { scalingMode = config.GetBindable(OsuSetting.Scaling); - scalingMode.ValueChanged += _ => updateSize(); + scalingMode.ValueChanged += _ => Scheduler.AddOnce(updateSize); sizeX = config.GetBindable(OsuSetting.ScalingSizeX); - sizeX.ValueChanged += _ => updateSize(); + sizeX.ValueChanged += _ => Scheduler.AddOnce(updateSize); sizeY = config.GetBindable(OsuSetting.ScalingSizeY); - sizeY.ValueChanged += _ => updateSize(); + sizeY.ValueChanged += _ => Scheduler.AddOnce(updateSize); posX = config.GetBindable(OsuSetting.ScalingPositionX); - posX.ValueChanged += _ => updateSize(); + posX.ValueChanged += _ => Scheduler.AddOnce(updateSize); posY = config.GetBindable(OsuSetting.ScalingPositionY); - posY.ValueChanged += _ => updateSize(); + posY.ValueChanged += _ => Scheduler.AddOnce(updateSize); safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy(); - safeAreaPadding.BindValueChanged(_ => updateSize()); + safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize)); } protected override void LoadComplete() { base.LoadComplete(); - updateSize(); + Scheduler.AddOnce(updateSize); sizableContainer.FinishTransforms(); } From b5dde6f1ad22d1c80502d307dee061556e3e404f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 21:38:52 +0900 Subject: [PATCH 082/136] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 04c543750e..71525a7acb 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 83c3593edb..c8f634284b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index b0c056ea21..5978f6d685 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From 5e47ce333ce42831d8b4d48fdd4aa4437956587a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Feb 2022 16:10:49 +0300 Subject: [PATCH 083/136] Change `SafeAreaOverrideEdges` to be get-only and protected --- osu.Game/OsuGameBase.cs | 2 +- osu.iOS/OsuGameIOS.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 97d2e64072..5b2eb5607a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -93,7 +93,7 @@ namespace osu.Game /// The that the game should be drawn over at a top level. /// Defaults to . /// - public virtual Edges SafeAreaOverrideEdges { get; set; } + protected virtual Edges SafeAreaOverrideEdges => Edges.None; protected OsuConfigManager LocalConfig { get; private set; } diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index cf14745be1..9c1795e45e 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -19,7 +19,7 @@ namespace osu.iOS protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); - public override Edges SafeAreaOverrideEdges => + 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; From d62885f30b64cd2cfbac451591b952ff9663a5a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 22:31:41 +0900 Subject: [PATCH 084/136] Don't schedule call to `updateSize` in `LoadComplete` to ensure following `FinishTransforms` runs as expected Co-authored-by: Salman Ahmed --- osu.Game/Graphics/Containers/ScalingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index b3423345b1..0d543bdbc8 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.Containers { base.LoadComplete(); - Scheduler.AddOnce(updateSize); + updateSize(); sizableContainer.FinishTransforms(); } From 750f90e7288b04f0abe4b7c4abb4805d52d5fda8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Feb 2022 22:38:40 +0900 Subject: [PATCH 085/136] Simplify TestSpectatorClient implementation --- .../Tests/Visual/Spectator/TestSpectatorClient.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 1a1d493249..453c086604 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -40,11 +39,7 @@ namespace osu.Game.Tests.Visual.Spectator public TestSpectatorClient() { - OnNewFrames += (i, bundle) => - { - if (PlayingUsers.Contains(i)) - lastReceivedUserFrames[i] = bundle.Frames[^1]; - }; + OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; } /// @@ -65,7 +60,7 @@ namespace osu.Game.Tests.Visual.Spectator /// The user to end play for. public void EndPlay(int userId) { - if (!PlayingUsers.Contains(userId)) + if (!userBeatmapDictionary.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState @@ -73,6 +68,8 @@ namespace osu.Game.Tests.Visual.Spectator BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, }); + + userBeatmapDictionary.Remove(userId); } public new void Schedule(Action action) => base.Schedule(action); @@ -131,7 +128,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUsers.Contains(userId)) + if (userBeatmapDictionary.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; From eb25730b614ececc5f6672e298dc5e786984c92e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 5 Feb 2022 16:12:58 +0900 Subject: [PATCH 086/136] Revert "Merge pull request #16716 from peppy/carousel-less-invalidations" This reverts commit 8d13e0514b27ce5e6f587ba8918c165d3e1ddbbf, reversing changes made to 95582a9023488da37edb3b9236f6b4aeb197ccb9. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +- .../Screens/Select/Carousel/CarouselHeader.cs | 29 ++++---- .../Carousel/DrawableCarouselBeatmap.cs | 10 ++- .../Carousel/DrawableCarouselBeatmapSet.cs | 6 +- .../Select/Carousel/DrawableCarouselItem.cs | 71 +++++++------------ 5 files changed, 54 insertions(+), 68 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3d5ed70dda..c3d340ac61 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -925,8 +925,10 @@ namespace osu.Game.Screens.Select // child items (difficulties) are still visible. 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. - item.Header.Alpha = Math.Clamp(1.75f - 1.5f * dist, 0, 1); + // We are applying a multiplicative alpha (which is internally done by nesting an + // 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 diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 533694b265..ed3aea3445 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselHeader : Container { + public Container BorderContainer; + public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); private readonly HoverLayer hoverLayer; @@ -35,14 +37,17 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.X; Height = DrawableCarouselItem.MAX_HEIGHT; - Masking = true; - CornerRadius = corner_radius; - BorderColour = new Color4(221, 255, 255, 255); - - InternalChildren = new Drawable[] + InternalChild = BorderContainer = new Container { - Content, - hoverLayer = new HoverLayer() + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + BorderColour = new Color4(221, 255, 255, 255), + Children = new Drawable[] + { + Content, + hoverLayer = new HoverLayer() + } }; } @@ -61,21 +66,21 @@ namespace osu.Game.Screens.Select.Carousel case CarouselItemState.NotSelected: hoverLayer.InsetForBorder = false; - BorderThickness = 0; - EdgeEffect = new EdgeEffectParameters + BorderContainer.BorderThickness = 0; + BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(1), Radius = 10, - Colour = Color4.Black.Opacity(0.5f), + Colour = Color4.Black.Opacity(100), }; break; case CarouselItemState.Selected: hoverLayer.InsetForBorder = true; - BorderThickness = border_thickness; - EdgeEffect = new EdgeEffectParameters + BorderContainer.BorderThickness = border_thickness; + BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index a3483aa60a..3576b77ae8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -36,9 +36,9 @@ namespace osu.Game.Screens.Select.Carousel /// /// The height of a carousel beatmap, including vertical spacing. /// - 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; @@ -67,18 +67,16 @@ namespace osu.Game.Screens.Select.Carousel private CancellationTokenSource starDifficultyCancellationSource; public DrawableCarouselBeatmap(CarouselBeatmap panel) - : base(header_height) { beatmapInfo = panel.BeatmapInfo; Item = panel; - - // Difficulty panels should start hidden for a better initial effect. - Hide(); } [BackgroundDependencyLoader(true)] private void load(BeatmapManager manager, SongSelect songSelect) { + Header.Height = height; + if (songSelect != null) { startRequested = b => songSelect.FinaliseSelection(b); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 63c004f4bc..618c5cf5ec 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -122,10 +122,12 @@ namespace osu.Game.Screens.Select.Carousel }, }; - background.DelayedLoadComplete += d => d.FadeInFromZero(750, Easing.OutQuint); - mainFlow.DelayedLoadComplete += d => d.FadeInFromZero(500, Easing.OutQuint); + background.DelayedLoadComplete += fadeContentIn; + mainFlow.DelayedLoadComplete += fadeContentIn; } + private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); + protected override void Deselected() { base.Deselected(); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 5e7ca0825a..cde3edad39 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -60,10 +60,12 @@ namespace osu.Game.Screens.Select.Carousel } } - protected DrawableCarouselItem(float headerHeight = MAX_HEIGHT) + protected DrawableCarouselItem() { RelativeSizeAxes = Axes.X; + Alpha = 0; + InternalChildren = new Drawable[] { MovementContainer = new Container @@ -71,20 +73,18 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - Header = new CarouselHeader - { - Height = headerHeight, - }, + Header = new CarouselHeader(), Content = new Container { RelativeSizeAxes = Axes.Both, - Y = headerHeight, } } }, }; } + public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha; + protected override void LoadComplete() { base.LoadComplete(); @@ -92,6 +92,12 @@ namespace osu.Game.Screens.Select.Carousel UpdateItem(); } + protected override void Update() + { + base.Update(); + Content.Y = Header.Height; + } + protected virtual void UpdateItem() { if (item == null) @@ -115,56 +121,29 @@ namespace osu.Game.Screens.Select.Carousel private void onStateChange(ValueChangedEvent _) => Scheduler.AddOnce(ApplyState); - private CarouselItemState? lastAppliedState; - protected virtual void ApplyState() { + // 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. + Height = Item.TotalHeight; + Debug.Assert(Item != null); - if (lastAppliedState != Item.State.Value) + switch (Item.State.Value) { - lastAppliedState = Item.State.Value; + case CarouselItemState.NotSelected: + Deselected(); + break; - // 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. - Height = Item.TotalHeight; - - switch (lastAppliedState) - { - case CarouselItemState.NotSelected: - Deselected(); - break; - - case CarouselItemState.Selected: - Selected(); - break; - } + case CarouselItemState.Selected: + Selected(); + break; } if (!Item.Visible) - Hide(); + this.FadeOut(300, Easing.OutQuint); 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.FadeIn(250); } protected virtual void Selected() From 2e1a9f137994a3085a592d216c075054126ceb38 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:13:16 +0800 Subject: [PATCH 087/136] Add performance breakdown as statistic item in extended statistics panel --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 11 + .../Statistics/PerformanceBreakdownChart.cs | 244 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 428e7b9df5..769e83ec00 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -298,6 +298,17 @@ namespace osu.Game.Rulesets.Osu } }, new StatisticRow + { + Columns = new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + } + }, + new StatisticRow { Columns = new[] { diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs new file mode 100644 index 0000000000..b58974eb96 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -0,0 +1,244 @@ +// Copyright (c) ppy Pty Ltd . 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.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 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; } + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + public PerformanceBreakdownChart(ScoreInfo score) + { + this.score = score; + } + + [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(beatmapManager, 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(); + var rows = new List(); + + 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) + { + float percentage = (float)(attribute.Value / perfectAttribute.Value); + if (float.IsNaN(percentage)) + return null; + + 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); + } + } +} From c35ef917a171564750d54c3bee09d24339d27ee2 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:18:23 +0800 Subject: [PATCH 088/136] Remove tooltip from performance statistic --- .../Statistics/PerformanceStatistic.cs | 43 ++-- .../Statistics/PerformanceStatisticTooltip.cs | 212 ------------------ 2 files changed, 14 insertions(+), 241 deletions(-) delete mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 158fd82b29..859b42d66d 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -2,21 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Cursor; -using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.Difficulty; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip + public class PerformanceStatistic : StatisticDisplay { private readonly ScoreInfo score; @@ -26,15 +22,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private RollingCounter counter; - [Resolved] - private ScorePerformanceCache performanceCache { get; set; } - - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - - [Resolved] - private BeatmapManager beatmapManager { get; set; } - public PerformanceStatistic(ScoreInfo score) : base("PP") { @@ -42,21 +29,23 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } [BackgroundDependencyLoader] - private void load() + private void load(ScorePerformanceCache performanceCache) { - new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) - .CalculateAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); + if (score.PP.HasValue) + { + setPerformanceValue(score.PP.Value); + } + else + { + performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely().Total)), cancellationTokenSource.Token); + } } - private void setPerformanceValue(PerformanceBreakdown breakdown) + private void setPerformanceValue(double? pp) { - // Don't display the tooltip if "Total" is the only item - if (breakdown != null && breakdown.Performance.GetAttributesForDisplay().Count() > 1) - { - TooltipContent = breakdown; - performance.Value = (int)Math.Round(breakdown.Performance.Total, MidpointRounding.AwayFromZero); - } + if (pp.HasValue) + performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); } public override void Appear() @@ -76,9 +65,5 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }; - - public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); - - public PerformanceBreakdown TooltipContent { get; private set; } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs deleted file mode 100644 index bd6eb057c6..0000000000 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.Difficulty; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Ranking.Expanded.Statistics -{ - public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip - { - private readonly Box background; - private Colour4 titleColor; - private Colour4 textColour; - - protected override Container Content { get; } - - public PerformanceStatisticTooltip() - { - AutoSizeAxes = Axes.Both; - Masking = true; - CornerRadius = 5; - - InternalChildren = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - Content = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 } - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = colours.Gray3; - titleColor = colours.Blue; - textColour = colours.BlueLighter; - } - - protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); - - protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); - - private PerformanceBreakdown currentPerformance; - - public void SetContent(PerformanceBreakdown performance) - { - if (performance == currentPerformance) - return; - - currentPerformance = performance; - - UpdateDisplay(performance); - } - - private Drawable createItemForTotal(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) - { - return - new GridContainer - { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Bottom = 10 }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 250), - 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), - Text = attribute.DisplayName, - Colour = titleColor - }, - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = Math.Round(attribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(), - Colour = titleColor - } - }, - new Drawable[] - { - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = "Maximum", - Colour = OsuColour.Gray(0.7f) - }, - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(), - Colour = OsuColour.Gray(0.7f) - } - } - } - }; - } - - private Drawable createItemForAttribute(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) - { - float percentage = (float)(attribute.Value / perfectAttribute.Value); - if (float.IsNaN(percentage)) - return null; - - return new GridContainer - { - AutoSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 110), - new Dimension(GridSizeMode.Absolute, 140), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = attribute.DisplayName, - Colour = textColour - }, - new Bar - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Width = 130, - Height = 5, - BackgroundColour = Color4.White.Opacity(0.5f), - Colour = textColour, - Length = percentage, - Margin = new MarginPadding { Left = 5, Right = 5 } - }, - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = percentage.ToLocalisableString("0%"), - Colour = textColour - } - } - } - }; - } - - protected virtual void UpdateDisplay(PerformanceBreakdown performance) - { - Content.Clear(); - - var displayAttributes = performance.Performance.GetAttributesForDisplay(); - - var perfectDisplayAttributes = performance.PerfectPerformance.GetAttributesForDisplay(); - - foreach (PerformanceDisplayAttribute attr in displayAttributes) - { - var attributeItem = attr.PropertyName == nameof(PerformanceAttributes.Total) - ? createItemForTotal(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)) - : createItemForAttribute(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)); - - if (attributeItem != null) - Content.Add(attributeItem); - } - } - - public void Move(Vector2 pos) => Position = pos; - } -} From 440b674bb017f54affd7f201580936ced8a8a126 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:30:35 +0800 Subject: [PATCH 089/136] Add statistic item for mania & taiko --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 11 +++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index ffb26b224f..aa9b4d5f1d 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -378,6 +378,17 @@ namespace osu.Game.Rulesets.Mania } }, new StatisticRow + { + Columns = new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + } + }, + new StatisticRow { Columns = new[] { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 21c99c0d2f..37e959f8b4 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -221,6 +221,17 @@ namespace osu.Game.Rulesets.Taiko } }, new StatisticRow + { + Columns = new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + } + }, + new StatisticRow { Columns = new[] { From 0b1fef38af1660d6c17c87bdb0c94cff9770cfb0 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:36:34 +0800 Subject: [PATCH 090/136] Use the playable beatmap provided in `CreateStatisticsForScore` --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- .../PerformanceBreakdownCalculator.cs | 17 ++++++++--------- .../Statistics/PerformanceBreakdownChart.cs | 9 ++++----- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index aa9b4d5f1d..9027b71069 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -381,7 +381,7 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 769e83ec00..68e91e42a7 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -301,7 +301,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 37e959f8b4..ddc594a70a 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 46342b237c..d68c2edb40 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Difficulty { public class PerformanceBreakdownCalculator { - private readonly BeatmapManager beatmapManager; + private readonly IBeatmap playableBeatmap; private readonly BeatmapDifficultyCache difficultyCache; private readonly ScorePerformanceCache performanceCache; - public PerformanceBreakdownCalculator(BeatmapManager beatmapManager, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache) + public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache) { - this.beatmapManager = beatmapManager; + this.playableBeatmap = playableBeatmap; this.difficultyCache = difficultyCache; this.performanceCache = performanceCache; } @@ -46,14 +46,13 @@ namespace osu.Game.Rulesets.Difficulty return Task.Run(async () => { Ruleset ruleset = score.Ruleset.CreateInstance(); - IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo).GetPlayableBeatmap(score.Ruleset, score.Mods); ScoreInfo perfectPlay = score.DeepClone(); perfectPlay.Accuracy = 1; perfectPlay.Passed = true; // calculate max combo var difficulty = await difficultyCache.GetDifficultyAsync( - beatmap.BeatmapInfo, + playableBeatmap.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken @@ -65,10 +64,10 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.MaxCombo = difficulty.Value.MaxCombo; // create statistics assuming all hit objects have perfect hit result - var statistics = beatmap.HitObjects - .SelectMany(getPerfectHitResults) - .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) - .ToDictionary(pair => pair.hitResult, pair => pair.count); + 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 diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index b58974eb96..fb9c93f0e6 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -26,6 +26,7 @@ namespace osu.Game.Screens.Ranking.Statistics public class PerformanceBreakdownChart : Container { private readonly ScoreInfo score; + private readonly IBeatmap playableBeatmap; private Drawable spinner; private Drawable content; @@ -41,12 +42,10 @@ namespace osu.Game.Screens.Ranking.Statistics [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } - [Resolved] - private BeatmapManager beatmapManager { get; set; } - - public PerformanceBreakdownChart(ScoreInfo score) + public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap) { this.score = score; + this.playableBeatmap = playableBeatmap; } [BackgroundDependencyLoader] @@ -146,7 +145,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Show(); - new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) + new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache, performanceCache) .CalculateAsync(score, cancellationTokenSource.Token) .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); } From ee6d4b25836d8051e4802b02c68075c28e2494b4 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:39:01 +0800 Subject: [PATCH 091/136] Move performance breakdown to the top to prevent re-ordering after watching replay --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 12 ++++++------ osu.Game.Rulesets.Osu/OsuRuleset.cs | 22 +++++++++++----------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 12 ++++++------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 9027b71069..180b9ef71b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,22 +370,22 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 250 - }, true), + AutoSizeAxes = Axes.Y + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }), + Height = 250 + }, true), } }, new StatisticRow diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 68e91e42a7..ad00a025a1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -275,6 +275,17 @@ namespace osu.Game.Rulesets.Osu return new[] { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + } + }, new StatisticRow { Columns = new[] @@ -298,17 +309,6 @@ namespace osu.Game.Rulesets.Osu } }, new StatisticRow - { - Columns = new[] - { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }), - } - }, - new StatisticRow { Columns = new[] { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index ddc594a70a..e56aabaf9d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,22 +213,22 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 250 - }, true), + AutoSizeAxes = Axes.Y + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }), + Height = 250 + }, true), } }, new StatisticRow From b31c1513f6ef0e7c17e7a3af39bb19174084255a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 22:41:04 +0800 Subject: [PATCH 092/136] Fix test failure The cursor was clicking too far to the right. --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 988f429ff5..167acc94c4 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("click to right of panel", () => { var expandedPanel = this.ChildrenOfType().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); }); From f78c853bc752f33d5fff6688fd286182e8ecd476 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 6 Feb 2022 10:59:53 +0800 Subject: [PATCH 093/136] Calculate max combo locally in `PerformanceBreakdownCalculator` --- .../PerformanceBreakdownCalculator.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index d68c2edb40..87e60fbe9d 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -51,17 +51,7 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Passed = true; // calculate max combo - var difficulty = await difficultyCache.GetDifficultyAsync( - playableBeatmap.BeatmapInfo, - score.Ruleset, - score.Mods, - cancellationToken - ).ConfigureAwait(false); - - if (difficulty == null) - return null; - - perfectPlay.MaxCombo = difficulty.Value.MaxCombo; + perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap); // create statistics assuming all hit objects have perfect hit result var statistics = playableBeatmap.HitObjects @@ -86,11 +76,23 @@ namespace osu.Game.Rulesets.Difficulty } // 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 ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); + 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 getPerfectHitResults(HitObject hitObject) { foreach (HitObject nested in hitObject.NestedHitObjects) From 56c90a21ceec78749b3b76f26c46c54cb3533169 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 6 Feb 2022 11:22:12 +0800 Subject: [PATCH 094/136] Add a todo --- osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 87e60fbe9d..3d384f5914 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -51,6 +51,7 @@ namespace osu.Game.Rulesets.Difficulty 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 From c2416c490e3b81c1fe7aa61c90200f9757fed7c8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 11:29:39 +0900 Subject: [PATCH 095/136] Fix crash on disconnection during multi-spectate --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 4646f42d63..bff7023102 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -228,7 +227,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate 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. if (multiplayerClient.Room.State != MultiplayerRoomState.Open) From 10bdb7240ff3615f53cb7a0ce61b88f5b3d3428c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 8 Feb 2022 14:36:29 +0800 Subject: [PATCH 096/136] Pre-check for divisor zero and add explanation --- .../Ranking/Statistics/PerformanceBreakdownChart.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index fb9c93f0e6..5b42554716 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -12,6 +12,7 @@ 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; @@ -192,10 +193,13 @@ namespace osu.Game.Screens.Ranking.Statistics [CanBeNull] private Drawable[] createAttributeRow(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) { - float percentage = (float)(attribute.Value / perfectAttribute.Value); - if (float.IsNaN(percentage)) + // 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 From 9c2d57d70783cdc71c8a8ee5439dcbc5eec41073 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 19:36:16 +0900 Subject: [PATCH 097/136] Add failing test --- .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 94e61eaee0..343fc7e6e0 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -92,7 +92,8 @@ namespace osu.Game.Tests.Online AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddUntilStep("wait for import", () => beatmaps.CurrentImport != null); - addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); + AddAssert("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet)); + addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable); } [Test] From b1a73996ba69de0cd1e02c9cb25266d52d29e2b7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 19:36:41 +0900 Subject: [PATCH 098/136] Fix incorrect check for beatmap availability --- osu.Game/Stores/BeatmapImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 709dd67087..2a2786b861 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; From 0d99017178af1b373212f4174242e7bf8ee224a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:27:08 +0900 Subject: [PATCH 099/136] Add state tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 9f8470446c..65583919d6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -235,6 +235,71 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null); } + [Test] + public void TestPlayingState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestCompletionState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send completion", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Completed)); + AddUntilStep("state is completed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Completed); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestQuitState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Quit)); + AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestFailedState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Failed); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; From 4c76027178933d9bcade478495f0588746220a18 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:29:49 +0900 Subject: [PATCH 100/136] Rename completed state to passed --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 6 +++--- osu.Game/Online/Spectator/SpectatingUserState.cs | 10 +++++----- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 65583919d6..a263bb1e6f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -247,7 +247,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestCompletionState() + public void TestPassedState() { loadSpectatingScreen(); @@ -255,8 +255,8 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send completion", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Completed)); - AddUntilStep("state is completed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Completed); + AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Passed)); + AddUntilStep("state is passed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Passed); start(); sendFrames(); diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs index c7ba4ba248..30be57b739 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -6,7 +6,7 @@ namespace osu.Game.Online.Spectator public enum SpectatingUserState { /// - /// The spectated user has not yet played. + /// The spectated user is not yet playing. /// Idle, @@ -16,17 +16,17 @@ namespace osu.Game.Online.Spectator Playing, /// - /// The spectated user has successfully completed gameplay. + /// The spectated user has passed gameplay. /// - Completed, + Passed, /// - /// The spectator user has failed during gameplay. + /// The spectator user has failed gameplay. /// Failed, /// - /// The spectated user has quit during gameplay. + /// The spectated user has quit gameplay. /// Quit } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 9e168411b0..ee5fdc917a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -186,7 +186,7 @@ namespace osu.Game.Online.Spectator currentBeatmap = null; if (state.HasPassed) - currentState.State = SpectatingUserState.Completed; + currentState.State = SpectatingUserState.Passed; else if (state.HasFailed) currentState.State = SpectatingUserState.Failed; else diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 125e4b261c..60bf87ff61 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -216,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void EndGameplay(int userId, SpectatorState state) { - if (state.State == SpectatingUserState.Completed || state.State == SpectatingUserState.Failed) + if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) return; RemoveUser(userId); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 7b58f669a0..394afb3a4c 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { - case SpectatingUserState.Completed: + case SpectatingUserState.Passed: // Make sure that gameplay completes to the end. if (gameplayStates.TryGetValue(userId, out var gameplayState)) gameplayState.Score.Replay.HasReceivedAllFrames = true; From c1766d8a411f1626255c1d74fbf0b634e7bc4610 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:29:53 +0900 Subject: [PATCH 101/136] Add paused state --- osu.Game/Online/Spectator/SpectatingUserState.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs index 30be57b739..e00c2e1947 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -15,6 +15,11 @@ namespace osu.Game.Online.Spectator /// Playing, + /// + /// The spectated user is currently paused. Unused for the time being. + /// + Paused, + /// /// The spectated user has passed gameplay. /// From 79d1d54e336c2cb0eff7399912933eb32f24b19e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Feb 2022 20:35:38 +0900 Subject: [PATCH 102/136] Rename parameter to match other usages --- osu.Game/Stores/BeatmapImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 2a2786b861..e6b655589c 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; From 886d1d2df637dcbac1e3b7147b3062ba576ce178 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 21:20:33 +0900 Subject: [PATCH 103/136] Refactorings --- .../Visual/Gameplay/TestSceneSpectator.cs | 3 ++- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- .../Dashboard/CurrentlyPlayingDisplay.cs | 20 +++++++++---------- .../Spectate/MultiSpectatorScreen.cs | 2 ++ osu.Game/Screens/Play/GameplayState.cs | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a263bb1e6f..e0946df2dc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -160,7 +160,8 @@ namespace osu.Game.Tests.Visual.Gameplay finish(SpectatingUserState.Failed); checkPaused(false); // Should continue playing until out of frames - checkPaused(true); + checkPaused(true); // And eventually stop after running out of frames and fail. + // Todo: Should check for + display a failed message. } [Test] diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ee5fdc917a..ed6e355ddf 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -98,8 +98,8 @@ namespace osu.Game.Online.Spectator } else { - watchingUserStates.Clear(); playingUsers.Clear(); + watchingUserStates.Clear(); } }), true); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 02ef28f825..117de88166 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -69,17 +69,17 @@ namespace osu.Game.Overlays.Dashboard { var user = task.GetResultSafely(); - if (user != null) - { - Schedule(() => - { - // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) - return; + if (user == null) + return; - userFlow.Add(createUserPanel(user)); - }); - } + Schedule(() => + { + // user may no longer be playing. + if (!playingUsers.Contains(user.Id)) + return; + + userFlow.Add(createUserPanel(user)); + }); }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 60bf87ff61..55df8301da 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -216,6 +216,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void EndGameplay(int userId, SpectatorState state) { + // Allowed passed/failed users to complete their remaining replay frames. + // The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used. if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) return; diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 64e873b3bb..c6a072da74 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play public bool HasFailed { get; set; } /// - /// Whether the user quit gameplay without either having either passed or failed. + /// Whether the user quit gameplay without having either passed or failed. /// public bool HasQuit { get; set; } From c242a63b1177b4e772fc52dff5b4eb2030bd48b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Feb 2022 10:16:43 +0900 Subject: [PATCH 104/136] Fix playlist overlay null reference when attempting an empty selection As reported at https://github.com/ppy/osu/discussions/16829. --- osu.Game/Overlays/Music/PlaylistOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 59ade0918d..ce816f84f0 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Music filter.Search.OnCommit += (sender, newText) => { - list.FirstVisibleSet.PerformRead(set => + list.FirstVisibleSet?.PerformRead(set => { BeatmapInfo toSelect = set.Beatmaps.FirstOrDefault(); From 4966c4e974c9a3e7b98910adeee5b658d6889e22 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 11:51:47 +0900 Subject: [PATCH 105/136] Remove redundant parameter --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index e0946df2dc..7235bce789 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Quit)); + AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); start(); From ffc4c64f7e7fb0951c9c3f3109497b37f50196b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 12:09:04 +0900 Subject: [PATCH 106/136] Unify namings across the board --- .../Visual/Gameplay/TestSceneSpectator.cs | 22 ++++++------ .../Visual/Gameplay/TestSceneSpectatorHost.cs | 4 +-- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- ...tingUserState.cs => SpectatedUserState.cs} | 4 +-- osu.Game/Online/Spectator/SpectatorClient.cs | 36 +++++++++---------- osu.Game/Online/Spectator/SpectatorState.cs | 2 +- .../Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 6 ++-- .../Visual/Spectator/TestSpectatorClient.cs | 4 +-- 9 files changed, 41 insertions(+), 41 deletions(-) rename osu.Game/Online/Spectator/{SpectatingUserState.cs => SpectatedUserState.cs} (90%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 7235bce789..157c248d69 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Gameplay checkPaused(true); sendFrames(); - finish(SpectatingUserState.Failed); + finish(SpectatedUserState.Failed); checkPaused(false); // Should continue playing until out of frames checkPaused(true); // And eventually stop after running out of frames and fail. @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -256,13 +256,13 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Passed)); - AddUntilStep("state is passed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Passed); + AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Passed)); + AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -275,12 +275,12 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id)); - AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); + AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -292,13 +292,13 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Failed)); - AddUntilStep("state is failed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Failed); + AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } private OsuFramedReplayInputHandler replayHandler => @@ -313,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish(SpectatingUserState state = SpectatingUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); + private void finish(SpectatedUserState state = SpectatedUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 6d6b0bf89e..034519fbf8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClientSendsCorrectRuleset() { - AddUntilStep("spectator client sending frames", () => spectatorClient.WatchingUserStates.ContainsKey(dummy_user_id)); - AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); + AddUntilStep("spectator client sending frames", () => spectatorClient.WatchedUserStates.ContainsKey(dummy_user_id)); + AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchedUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); } public override void TearDownSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 55450b36e2..1322fbc96e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach ((int userId, _) in WatchingUserStates) + foreach ((int userId, _) in WatchedUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatedUserState.cs similarity index 90% rename from osu.Game/Online/Spectator/SpectatingUserState.cs rename to osu.Game/Online/Spectator/SpectatedUserState.cs index e00c2e1947..0f0a3068b8 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatedUserState.cs @@ -3,7 +3,7 @@ namespace osu.Game.Online.Spectator { - public enum SpectatingUserState + public enum SpectatedUserState { /// /// The spectated user is not yet playing. @@ -26,7 +26,7 @@ namespace osu.Game.Online.Spectator Passed, /// - /// The spectator user has failed gameplay. + /// The spectated user has failed gameplay. /// Failed, diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ed6e355ddf..a54ea0d9ee 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -38,7 +38,7 @@ namespace osu.Game.Online.Spectator /// /// The states of all users currently being watched. /// - public IBindableDictionary WatchingUserStates => watchingUserStates; + public IBindableDictionary WatchedUserStates => watchedUserStates; /// /// A global list of all players currently playing. @@ -48,9 +48,9 @@ namespace osu.Game.Online.Spectator /// /// All users currently being watched. /// - private readonly List watchingUsers = new List(); + private readonly List watchedUsers = new List(); - private readonly BindableDictionary watchingUserStates = new BindableDictionary(); + private readonly BindableDictionary watchedUserStates = new BindableDictionary(); private readonly BindableList playingUsers = new BindableList(); private readonly SpectatorState currentState = new SpectatorState(); @@ -85,8 +85,8 @@ namespace osu.Game.Online.Spectator if (connected.NewValue) { // get all the users that were previously being watched - int[] users = watchingUsers.ToArray(); - watchingUsers.Clear(); + int[] users = watchedUsers.ToArray(); + watchedUsers.Clear(); // resubscribe to watched users. foreach (int userId in users) @@ -99,7 +99,7 @@ namespace osu.Game.Online.Spectator else { playingUsers.Clear(); - watchingUserStates.Clear(); + watchedUserStates.Clear(); } }), true); } @@ -111,8 +111,8 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - if (watchingUsers.Contains(userId)) - watchingUserStates[userId] = state; + if (watchedUsers.Contains(userId)) + watchedUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); }); @@ -126,8 +126,8 @@ namespace osu.Game.Online.Spectator { playingUsers.Remove(userId); - if (watchingUsers.Contains(userId)) - watchingUserStates[userId] = state; + if (watchedUsers.Contains(userId)) + watchedUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); @@ -159,7 +159,7 @@ namespace osu.Game.Online.Spectator currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); - currentState.State = SpectatingUserState.Playing; + currentState.State = SpectatedUserState.Playing; currentBeatmap = state.Beatmap; currentScore = score; @@ -186,11 +186,11 @@ namespace osu.Game.Online.Spectator currentBeatmap = null; if (state.HasPassed) - currentState.State = SpectatingUserState.Passed; + currentState.State = SpectatedUserState.Passed; else if (state.HasFailed) - currentState.State = SpectatingUserState.Failed; + currentState.State = SpectatedUserState.Failed; else - currentState.State = SpectatingUserState.Quit; + currentState.State = SpectatedUserState.Quit; EndPlayingInternal(currentState); }); @@ -200,10 +200,10 @@ namespace osu.Game.Online.Spectator { Debug.Assert(ThreadSafety.IsUpdateThread); - if (watchingUsers.Contains(userId)) + if (watchedUsers.Contains(userId)) return; - watchingUsers.Add(userId); + watchedUsers.Add(userId); WatchUserInternal(userId); } @@ -214,8 +214,8 @@ namespace osu.Game.Online.Spectator // Todo: This should not be a thing, but requires framework changes. Schedule(() => { - watchingUsers.Remove(userId); - watchingUserStates.Remove(userId); + watchedUsers.Remove(userId); + watchedUserStates.Remove(userId); StopWatchingUserInternal(userId); }); } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index fc62f16bba..77686d12da 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -25,7 +25,7 @@ namespace osu.Game.Online.Spectator public IEnumerable Mods { get; set; } = Enumerable.Empty(); [Key(3)] - public SpectatingUserState State { get; set; } + public SpectatedUserState State { get; set; } public bool Equals(SpectatorState other) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 55df8301da..571b6b4324 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -218,7 +218,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { // Allowed passed/failed users to complete their remaining replay frames. // The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used. - if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) + if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed) return; RemoveUser(userId); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 394afb3a4c..9d0f8abddc 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - userStates.BindTo(spectatorClient.WatchingUserStates); + userStates.BindTo(spectatorClient.WatchedUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( @@ -134,13 +134,13 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { - case SpectatingUserState.Passed: + case SpectatedUserState.Passed: // Make sure that gameplay completes to the end. if (gameplayStates.TryGetValue(userId, out var gameplayState)) gameplayState.Score.Replay.HasReceivedAllFrames = true; break; - case SpectatingUserState.Playing: + case SpectatedUserState.Playing: Schedule(() => OnUserStateChanged(userId, newState)); updateGameplayState(userId); break; diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 6862cda88c..1322a99ea7 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Spectator /// /// The user to end play for. /// The spectator state to end play with. - public void EndPlay(int userId, SpectatingUserState state = SpectatingUserState.Quit) + public void EndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { if (!userBeatmapDictionary.ContainsKey(userId)) return; @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, - State = SpectatingUserState.Playing + State = SpectatedUserState.Playing }); } } From 18251c9285a559ac3f4070cdc8538211175e18ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 12:20:07 +0900 Subject: [PATCH 107/136] Clean up SpectatorScreen based on suggestions --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Play/SoloSpectator.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 571b6b4324..e5eeeb3448 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -207,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) { } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index a710db6d24..a0b07fcbd9 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -166,7 +166,7 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) { clearDisplay(); showBeatmapPanel(spectatorState); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 9d0f8abddc..9eb374f0f7 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Spectate continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID)) - updateGameplayState(userId); + startGameplay(userId); } } @@ -141,8 +141,8 @@ namespace osu.Game.Screens.Spectate break; case SpectatedUserState.Playing: - Schedule(() => OnUserStateChanged(userId, newState)); - updateGameplayState(userId); + Schedule(() => OnNewPlayingUserState(userId, newState)); + startGameplay(userId); break; } } @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Spectate Schedule(() => EndGameplay(userId, state)); } - private void updateGameplayState(int userId) + private void startGameplay(int userId) { Debug.Assert(userMap.ContainsKey(userId)); @@ -195,11 +195,11 @@ namespace osu.Game.Screens.Spectate } /// - /// Invoked when a spectated user's state has changed. + /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing. /// /// The user whose state has changed. /// The new state. - protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState); + protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState); /// /// Starts gameplay for a user. From 1b8ada087d97ffdf8b62697a678ddaf8a8c4c654 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Feb 2022 19:25:51 +0800 Subject: [PATCH 108/136] Set `NoDefaultExcludes` to true This allows files such as .editorconfig and .gitignore to be included in the nupkg --- Templates/osu.Game.Templates.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj index 31a24a301f..4624d3d771 100644 --- a/Templates/osu.Game.Templates.csproj +++ b/Templates/osu.Game.Templates.csproj @@ -15,6 +15,7 @@ true false content + true From f1c6fdb2afce097de6a636cfff97aef2b337e70f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Feb 2022 19:53:34 +0800 Subject: [PATCH 109/136] Update `.editorconfig`, `.gitignore` and DotSettings Basically just copied from root directory --- .../Rulesets/ruleset-empty/.editorconfig | 20 +-- Templates/Rulesets/ruleset-empty/.gitignore | 128 +++++++++++++----- ...ame.Rulesets.EmptyFreeform.sln.DotSettings | 83 +++++++++++- .../Rulesets/ruleset-example/.editorconfig | 20 +-- Templates/Rulesets/ruleset-example/.gitignore | 128 +++++++++++++----- ...osu.Game.Rulesets.Pippidon.sln.DotSettings | 83 +++++++++++- .../ruleset-scrolling-empty/.editorconfig | 20 +-- .../ruleset-scrolling-empty/.gitignore | 128 +++++++++++++----- ...me.Rulesets.EmptyScrolling.sln.DotSettings | 83 +++++++++++- .../ruleset-scrolling-example/.editorconfig | 20 +-- .../ruleset-scrolling-example/.gitignore | 128 +++++++++++++----- ...osu.Game.Rulesets.Pippidon.sln.DotSettings | 83 +++++++++++- 12 files changed, 696 insertions(+), 228 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/.editorconfig b/Templates/Rulesets/ruleset-empty/.editorconfig index f3badda9b3..b01749195f 100644 --- a/Templates/Rulesets/ruleset-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-empty/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error + +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-empty/.gitignore b/Templates/Rulesets/ruleset-empty/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-empty/.gitignore +++ b/Templates/Rulesets/ruleset-empty/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings index aa8f8739c1..2ff0f4d30b 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -909,26 +922,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-example/.editorconfig b/Templates/Rulesets/ruleset-example/.editorconfig index f3badda9b3..b01749195f 100644 --- a/Templates/Rulesets/ruleset-example/.editorconfig +++ b/Templates/Rulesets/ruleset-example/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error + +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-example/.gitignore b/Templates/Rulesets/ruleset-example/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-example/.gitignore +++ b/Templates/Rulesets/ruleset-example/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index aa8f8739c1..2ff0f4d30b 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -909,26 +922,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig index f3badda9b3..b01749195f 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error + +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore +++ b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings index aa8f8739c1..2ff0f4d30b 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -909,26 +922,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig index f3badda9b3..b01749195f 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error + +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-scrolling-example/.gitignore b/Templates/Rulesets/ruleset-scrolling-example/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/.gitignore +++ b/Templates/Rulesets/ruleset-scrolling-example/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index aa8f8739c1..2ff0f4d30b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -909,26 +922,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True From d06d584867e57ee6cb03cc566c1114ada0f47473 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Feb 2022 19:56:32 +0800 Subject: [PATCH 110/136] Change assembly titles So that they match the source name specified in template.json and can get replaced --- .../osu.Game.Rulesets.EmptyFreeform.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.csproj | 2 +- .../osu.Game.Rulesets.EmptyScrolling.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj index cfe2bd1cb2..092a013614 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.EmptyFreeform Library AnyCPU osu.Game.Rulesets.EmptyFreeform diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj index 61b859f45b..a3607343c9 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.Pippidon Library AnyCPU osu.Game.Rulesets.Pippidon diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj index 9dce3c9a0a..2ea52429ab 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.EmptyScrolling Library AnyCPU osu.Game.Rulesets.EmptyScrolling diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj index 61b859f45b..a3607343c9 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.Pippidon Library AnyCPU osu.Game.Rulesets.Pippidon From 036d17d9fd215423f34ace116203af936ef7428e Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Feb 2022 20:48:14 +0800 Subject: [PATCH 111/136] Remove licence headers --- Templates/Rulesets/ruleset-empty/.editorconfig | 2 -- .../osu.Game.Rulesets.EmptyFreeform.sln.DotSettings | 3 --- Templates/Rulesets/ruleset-example/.editorconfig | 2 -- .../ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings | 3 --- Templates/Rulesets/ruleset-scrolling-empty/.editorconfig | 2 -- .../osu.Game.Rulesets.EmptyScrolling.sln.DotSettings | 3 --- Templates/Rulesets/ruleset-scrolling-example/.editorconfig | 2 -- .../osu.Game.Rulesets.Pippidon.sln.DotSettings | 3 --- 8 files changed, 20 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/.editorconfig b/Templates/Rulesets/ruleset-empty/.editorconfig index b01749195f..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-empty/.editorconfig @@ -190,5 +190,3 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error - -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings index 2ff0f4d30b..9752e08599 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings @@ -730,9 +730,6 @@ </Group> </TypePattern> </Patterns> - 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. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/Templates/Rulesets/ruleset-example/.editorconfig b/Templates/Rulesets/ruleset-example/.editorconfig index b01749195f..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-example/.editorconfig +++ b/Templates/Rulesets/ruleset-example/.editorconfig @@ -190,5 +190,3 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error - -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index 2ff0f4d30b..9752e08599 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -730,9 +730,6 @@ </Group> </TypePattern> </Patterns> - 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. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig index b01749195f..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig @@ -190,5 +190,3 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error - -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings index 2ff0f4d30b..9752e08599 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings @@ -730,9 +730,6 @@ </Group> </TypePattern> </Patterns> - 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. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig index b01749195f..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig @@ -190,5 +190,3 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error - -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index 2ff0f4d30b..9752e08599 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -730,9 +730,6 @@ </Group> </TypePattern> </Patterns> - 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. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> From f47748591a2deb2604f9556bfbe4bdad754ba96a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 12:11:26 +0900 Subject: [PATCH 112/136] Update fastlane to latest release This pulls in the fix from souyuz to allow us to bring things up-to-date again (see https://github.com/voydz/souyuz/pull/36#event-6033249116). Have tested builds locally to work as expected. --- Gemfile.lock | 71 ++++++++++++++++++------------------ fastlane/README.md | 91 +++++++++++++++++++++++++++++++--------------- 2 files changed, 96 insertions(+), 66 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 86c8baabe6..1010027af9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,17 +8,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.551.0) - aws-sdk-core (3.125.5) + aws-partitions (1.553.0) + aws-sdk-core (3.126.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.53.0) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-kms (1.54.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.111.3) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-s3 (1.112.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -27,8 +27,8 @@ GEM claide (1.1.0) colored (1.2) colored2 (3.1.2) - commander-fastlane (4.4.6) - highline (~> 1.7.2) + commander (4.6.0) + highline (~> 2.0.0) declarative (0.0.20) digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) @@ -36,7 +36,7 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) emoji_regex (3.2.3) - excon (0.90.0) + excon (0.91.0) faraday (1.9.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -66,15 +66,15 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.181.0) + fastlane (2.204.2) CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.3, < 3.0.0) + addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) colored - commander-fastlane (>= 4.4.6, < 5.0.0) + commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) @@ -83,19 +83,20 @@ GEM faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.37.0, < 0.39.0) - google-cloud-storage (>= 1.15.0, < 2.0.0) - highline (>= 1.7.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (~> 2.0.0) naturally (~> 2.2) + optparse (~> 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) simctl (~> 1.6.3) - slack-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (>= 1.4.5, < 2.0.0) tty-screen (>= 0.6.3, < 1.0.0) @@ -105,18 +106,12 @@ GEM xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) fastlane-plugin-clean_testflight_testers (0.3.0) - fastlane-plugin-souyuz (0.9.1) - souyuz (= 0.9.1) + fastlane-plugin-souyuz (0.11.1) + souyuz (= 0.11.1) fastlane-plugin-xamarin (0.6.3) gh_inspector (1.1.3) - google-api-client (0.38.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (~> 0.9) - httpclient (>= 2.8.1, < 3.0) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.0) - signet (~> 0.12) + google-apis-androidpublisher_v3 (0.16.0) + google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) @@ -128,6 +123,8 @@ GEM webrick google-apis-iamcredentials_v1 (0.10.0) google-apis-core (>= 0.4, < 2.a) + google-apis-playcustomapp_v1 (0.7.0) + google-apis-core (>= 0.4, < 2.a) google-apis-storage_v1 (0.11.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) @@ -144,14 +141,14 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.17.1) + googleauth (1.1.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.15) - highline (1.7.10) + signet (>= 0.16, < 2.a) + highline (2.0.3) http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) @@ -161,16 +158,19 @@ GEM memoist (0.16.2) mini_magick (4.11.0) mini_mime (1.1.2) - mini_portile2 (2.4.0) + mini_portile2 (2.7.1) multi_json (1.15.0) multipart-post (2.0.0) nanaimo (0.3.0) naturally (2.2.1) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + nokogiri (1.13.1) + mini_portile2 (~> 2.7.0) + racc (~> 1.4) + optparse (0.1.1) os (1.1.4) plist (3.6.0) public_suffix (4.0.6) + racc (1.6.0) rake (13.0.6) representable (3.1.1) declarative (< 0.1.0) @@ -190,10 +190,9 @@ GEM simctl (1.6.8) CFPropertyList naturally - slack-notifier (2.4.0) - souyuz (0.9.1) - fastlane (>= 1.103.0) - highline (~> 1.7) + souyuz (0.11.1) + fastlane (>= 2.182.0) + highline (~> 2.0) nokogiri (~> 1.7) terminal-notifier (2.0.0) terminal-table (1.8.0) diff --git a/fastlane/README.md b/fastlane/README.md index 8273fdaa5d..9d5e11f7cb 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -1,78 +1,109 @@ fastlane documentation -================ +---- + # Installation Make sure you have the latest version of the Xcode command line tools installed: -``` +```sh xcode-select --install ``` -Install _fastlane_ using -``` -[sudo] gem install fastlane -NV -``` -or alternatively using `brew install fastlane` +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions + ## Android + ### android beta + +```sh +[bundle exec] fastlane android beta ``` -fastlane android beta -``` + Deploy to play store + ### android build_github + +```sh +[bundle exec] fastlane android build_github ``` -fastlane android build_github -``` + Deploy to github release + ### android build + +```sh +[bundle exec] fastlane android build ``` -fastlane android build -``` + Compile the project + ### android update_version + +```sh +[bundle exec] fastlane android update_version ``` -fastlane android update_version -``` + ---- + ## iOS + ### ios beta + +```sh +[bundle exec] fastlane ios beta ``` -fastlane ios beta -``` + Deploy to testflight + ### ios build + +```sh +[bundle exec] fastlane ios build ``` -fastlane ios build -``` + Compile the project + ### ios provision + +```sh +[bundle exec] fastlane ios provision ``` -fastlane ios provision -``` + Install provisioning profiles using match + ### ios update_version + +```sh +[bundle exec] fastlane ios update_version ``` -fastlane ios update_version -``` + + ### ios testflight_prune_dry -``` -fastlane ios testflight_prune_dry + +```sh +[bundle exec] fastlane ios testflight_prune_dry ``` + + ### ios testflight_prune + +```sh +[bundle exec] fastlane ios testflight_prune ``` -fastlane ios testflight_prune -``` + ---- -This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. -More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). -The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). From a3896a8ebdc87b95c9d97a1f5112c65bafe761b9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:18:29 +0900 Subject: [PATCH 113/136] Remove allowance of null dependency --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 277040b2a6..0a94b55221 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI public int RecordFrameRate = 60; - [Resolved(canBeNull: true)] + [Resolved] private SpectatorClient spectatorClient { get; set; } [Resolved] @@ -48,8 +48,7 @@ namespace osu.Game.Rulesets.UI base.LoadComplete(); inputManager = GetContainingInputManager(); - - spectatorClient?.BeginPlaying(gameplayState, target); + spectatorClient.BeginPlaying(gameplayState, target); } protected override void Dispose(bool isDisposing) From f7fb7825cc322c3ac08a0c310928591ed96e3142 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:21:33 +0900 Subject: [PATCH 114/136] Simplify disposal --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 0a94b55221..6843beef7b 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -54,9 +55,7 @@ namespace osu.Game.Rulesets.UI protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - - if (spectatorClient != null && gameplayState != null) - spectatorClient.EndPlaying(gameplayState); + spectatorClient?.EndPlaying(gameplayState); } protected override void Update() From ebd105422fd6c2903bced01cb820d7e0d542789a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:22:08 +0900 Subject: [PATCH 115/136] Remove unused using --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 6843beef7b..dcd8f12028 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; From 88bb9d4237c62d37678d8e0e110438ea247b7ae5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 15:50:03 +0900 Subject: [PATCH 116/136] Fix migration errors not outputting the call stack to logs --- .../Settings/Sections/Maintenance/MigrationRunScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index b0b61554eb..adb347e7b8 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance .ContinueWith(t => { if (t.IsFaulted) - Logger.Log($"Error during migration: {t.Exception?.Message}", level: LogLevel.Error); + Logger.Error(t.Exception, $"Error during migration: {t.Exception?.Message}"); Schedule(this.Exit); }); From 44f2d8a4481d686aea94d6d480cfc0e17ec7351b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 18:48:37 +0900 Subject: [PATCH 117/136] Allow game folder migration to fail gracefully when cleanup cannot completely succeed --- osu.Game.Tournament/IO/TournamentStorage.cs | 4 ++- osu.Game/IO/MigratableStorage.cs | 30 +++++++++++----- osu.Game/IO/OsuStorage.cs | 7 ++-- osu.Game/OsuGameBase.cs | 9 ++--- .../Maintenance/MigrationRunScreen.cs | 36 ++++++++++++++++--- 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 347d368a04..b4859d0c91 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tournament.IO public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty); - public override void Migrate(Storage newStorage) + public override bool Migrate(Storage newStorage) { // this migration only happens once on moving to the per-tournament storage system. // listed files are those known at that point in time. @@ -94,6 +94,8 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(newStorage); storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); + + return true; } private void moveFileIfExists(string file, DirectoryInfo destination) diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 1b76725b04..e478144294 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -33,7 +33,8 @@ namespace osu.Game.IO /// A general purpose migration method to move the storage to a different location. /// The target storage of the migration. /// - public virtual void Migrate(Storage newStorage) + /// Whether cleanup could complete. + public virtual bool Migrate(Storage newStorage) { var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newStorage.GetFullPath(".")); @@ -57,17 +58,20 @@ namespace osu.Game.IO CopyRecursive(source, destination); ChangeTargetStorage(newStorage); - DeleteRecursive(source); + + return DeleteRecursive(source); } - protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) + protected bool DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) { + bool allFilesDeleted = true; + foreach (System.IO.FileInfo fi in target.GetFiles()) { if (topLevelExcludes && IgnoreFiles.Contains(fi.Name)) continue; - AttemptOperation(() => fi.Delete()); + allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false); } foreach (DirectoryInfo dir in target.GetDirectories()) @@ -75,11 +79,13 @@ namespace osu.Game.IO if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name)) continue; - AttemptOperation(() => dir.Delete(true)); + allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false); } if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - AttemptOperation(target.Delete); + allFilesDeleted &= AttemptOperation(target.Delete, throwOnFailure: false); + + return allFilesDeleted; } protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) @@ -110,19 +116,25 @@ namespace osu.Game.IO /// /// The action to perform. /// The number of attempts (250ms wait between each). - protected static void AttemptOperation(Action action, int attempts = 10) + /// Whether to throw an exception on failure. If false, will silently fail. + protected static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true) { while (true) { try { action(); - return; + return true; } catch (Exception) { if (attempts-- == 0) - throw; + { + if (throwOnFailure) + throw; + + return false; + } } Thread.Sleep(250); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 802c71e363..6e7cb545e3 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -113,11 +113,14 @@ namespace osu.Game.IO } } - public override void Migrate(Storage newStorage) + public override bool Migrate(Storage newStorage) { - base.Migrate(newStorage); + bool cleanupSucceeded = base.Migrate(newStorage); + storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath(".")); storageConfig.Save(); + + return cleanupSucceeded; } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5b2eb5607a..0b2644d5ba 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -413,7 +413,7 @@ namespace osu.Game Scheduler.AddDelayed(GracefullyExit, 2000); } - public void Migrate(string path) + public bool Migrate(string path) { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); @@ -432,14 +432,15 @@ namespace osu.Game readyToRun.Wait(); - (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + + Logger.Log(@"Migration complete!"); + return cleanupSucceded != false; } finally { realmBlocker?.Dispose(); } - - Logger.Log(@"Migration complete!"); } protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index adb347e7b8..fb7ff0dbd1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -4,13 +4,16 @@ using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osuTK; @@ -23,6 +26,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [Resolved(canBeNull: true)] private OsuGame game { get; set; } + [Resolved] + private NotificationOverlay notifications { get; set; } + + [Resolved] + private Storage storage { get; set; } + + [Resolved] + private GameHost host { get; set; } + public override bool AllowBackButton => false; public override bool AllowExternalScreenChange => false; @@ -84,17 +96,33 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Beatmap.Value = Beatmap.Default; + var originalStorage = new NativeStorage(storage.GetFullPath(string.Empty), host); + migrationTask = Task.Run(PerformMigration) - .ContinueWith(t => + .ContinueWith(task => { - if (t.IsFaulted) - Logger.Error(t.Exception, $"Error during migration: {t.Exception?.Message}"); + if (task.IsFaulted) + { + Logger.Error(task.Exception, $"Error during migration: {task.Exception?.Message}"); + } + else if (!task.GetResultSafely()) + { + notifications.Post(new SimpleNotification + { + Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.", + Activated = () => + { + originalStorage.PresentExternally(); + return true; + } + }); + } Schedule(this.Exit); }); } - protected virtual void PerformMigration() => game?.Migrate(destination.FullName); + protected virtual bool PerformMigration() => game?.Migrate(destination.FullName) != false; public override void OnEntering(IScreen last) { From 19cb8cb03a100c9baa80c23b7b307ff300130df2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 19:21:17 +0900 Subject: [PATCH 118/136] Update tests --- .../Settings/TestSceneMigrationScreens.cs | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs index 2883e54385..a68090504d 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs @@ -3,32 +3,69 @@ using System.IO; using System.Threading; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections.Maintenance; namespace osu.Game.Tests.Visual.Settings { public class TestSceneMigrationScreens : ScreenTestScene { + [Cached] + private readonly NotificationOverlay notifications; + public TestSceneMigrationScreens() { - AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen())); + Children = new Drawable[] + { + notifications = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + }; + } + + [Test] + public void TestDeleteSuccess() + { + AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen(true))); + } + + [Test] + public void TestDeleteFails() + { + AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen(false))); } private class TestMigrationSelectScreen : MigrationSelectScreen { - protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen()); + private readonly bool deleteSuccess; + + public TestMigrationSelectScreen(bool deleteSuccess) + { + this.deleteSuccess = deleteSuccess; + } + + protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen(deleteSuccess)); private class TestMigrationRunScreen : MigrationRunScreen { - protected override void PerformMigration() - { - Thread.Sleep(3000); - } + private readonly bool success; - public TestMigrationRunScreen() + public TestMigrationRunScreen(bool success) : base(null) { + this.success = success; + } + + protected override bool PerformMigration() + { + Thread.Sleep(3000); + return success; } } } From 2939bc4644b2b5079a5af5ffa578288676cf0ca2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 01:49:52 +0900 Subject: [PATCH 119/136] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 71525a7acb..147f576c55 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c8f634284b..dd10807ec2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 5978f6d685..6fbc468586 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From 9cd88ec2b84d8460af461bd91deec5fc355ddced Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Feb 2022 21:23:38 +0300 Subject: [PATCH 120/136] Update API models with score pinning changes --- osu.Game/Online/API/Requests/GetUserScoresRequest.cs | 3 ++- osu.Game/Online/API/Requests/Responses/APIUser.cs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs index 653abf7427..5d39799f6b 100644 --- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs @@ -39,6 +39,7 @@ namespace osu.Game.Online.API.Requests { Best, Firsts, - Recent + Recent, + Pinned } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index e4a432b074..2b64e5de06 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -151,6 +151,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"scores_recent_count")] public int ScoresRecentCount; + [JsonProperty(@"scores_pinned_count")] + public int ScoresPinnedCount; + [JsonProperty(@"beatmap_playcounts_count")] public int BeatmapPlayCountsCount; From 4f7003928acb9a14a12adc02c343cb9329181ec5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Feb 2022 21:32:07 +0300 Subject: [PATCH 121/136] Add score container for pinned scores in ranks section --- .../Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs | 3 +++ osu.Game/Overlays/Profile/Sections/RanksSection.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 5532e35cc5..5c67da1911 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -46,6 +46,9 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks case ScoreType.Recent: return user.ScoresRecentCount; + case ScoreType.Pinned: + return user.ScoresPinnedCount; + default: return 0; } diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 00a68d5bf9..02d8bd8c52 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -18,6 +18,8 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { + // todo: update to use UsersStrings.ShowExtraTopRanksPinnedTitle once that exists. + new PaginatedScoreContainer(ScoreType.Pinned, User, "Pinned Scores"), new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle), new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle) }; From 9574bc13820665f5e6721915b3c05367ac9dccb7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:00:40 +0300 Subject: [PATCH 122/136] Allow `IRulesetInfo`s of same type to be comparable At first I was planning on making `CompareTo` implemented at `IRulesetInfo` itself and shared across classes, but turns out it only implements it explicitly and not allow direct `IRulesetInfo.Equals` calls. It messed with my head enough that I decided to just let each class have its own implementation and only allow same type. --- osu.Game/Rulesets/IRulesetInfo.cs | 2 +- osu.Game/Rulesets/RulesetInfo.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 44731a2495..60a02212fc 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets /// /// A representation of a ruleset's metadata. /// - public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable + public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable { /// /// The user-exposed name of this ruleset. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 0a0941d1ff..88e3988431 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [MapTo("Ruleset")] - public class RulesetInfo : RealmObject, IEquatable, IRulesetInfo + public class RulesetInfo : RealmObject, IEquatable, IComparable, IRulesetInfo { [PrimaryKey] public string ShortName { get; set; } = string.Empty; @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets return ShortName == other.ShortName; } - public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); + public bool Equals(IRulesetInfo? other) => other is RulesetInfo r && Equals(r); public int CompareTo(RulesetInfo other) { @@ -63,6 +63,14 @@ namespace osu.Game.Rulesets return string.Compare(ShortName, other.ShortName, StringComparison.Ordinal); } + public int CompareTo(IRulesetInfo other) + { + if (!(other is RulesetInfo ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(RulesetInfo)}.", nameof(other)); + + return CompareTo(ruleset); + } + public override int GetHashCode() { // Importantly, ignore the underlying realm hash code, as it will usually not match. From 1b729e891d53e8134efcd5ef8f09d5655c5bf20f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:01:10 +0300 Subject: [PATCH 123/136] Update pointless `CompareTo` implementation once again --- osu.Game/Rulesets/EFRulesetInfo.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index ba56adac49..4174aa773c 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [Table(@"RulesetInfo")] - public sealed class EFRulesetInfo : IEquatable, IRulesetInfo + public sealed class EFRulesetInfo : IEquatable, IComparable, IRulesetInfo { public int? ID { get; set; } @@ -42,7 +42,15 @@ namespace osu.Game.Rulesets public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; - public int CompareTo(RulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + public int CompareTo(EFRulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + + public int CompareTo(IRulesetInfo other) + { + if (!(other is EFRulesetInfo ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(EFRulesetInfo)}.", nameof(other)); + + return CompareTo(ruleset); + } public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo); From 26839f6ad8f3c49c3d1c63016b3af47e9afd2602 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:17:24 +0300 Subject: [PATCH 124/136] Consider `OnlineID`s during ruleset equality if available Required for `APIBeatmap`s, which provide `Ruleset` instances with `OnlineID` available only. Also consistent with the comparer implementation. --- osu.Game/Rulesets/RulesetInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 88e3988431..ba7c8d191d 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets if (ReferenceEquals(this, other)) return true; if (other == null) return false; + if (OnlineID >= 0 && other.OnlineID >= 0) + return OnlineID == other.OnlineID; + return ShortName == other.ShortName; } From 6f0e32826c6d90c92c1130cd7aa07894e3b7f38f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:06:28 +0300 Subject: [PATCH 125/136] Standardise ordering/grouping of `IRulesetInfo`/`RulesetInfo`s --- .../Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs | 2 +- osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs | 6 ++---- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs index 7753d8480a..eeb86f4702 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool firstGroup = true; - foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) + foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { if (!firstGroup) { diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 5b211084ab..5b467d67e2 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -62,10 +62,8 @@ namespace osu.Game.Beatmaps.Drawables // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 bool collapsed = beatmapSet.Beatmaps.Count() > 12; - foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) - { - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key, rulesetGrouping, collapsed)); - } + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 5503a62ba2..2aec63fa65 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -851,7 +851,7 @@ namespace osu.Game.Screens.Edit var difficultyItems = new List(); - foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset.ShortName).OrderBy(group => group.Key)) + foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset).OrderBy(group => group.Key)) { if (difficultyItems.Count > 0) difficultyItems.Add(new EditorMenuItemSpacer()); diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 82523c9d9d..760915b528 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset.ShortName) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset) .Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } From c29cc78853f9cfb5efc4e923c7d9fca6fda8fbf7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:44:54 +0300 Subject: [PATCH 126/136] Fix failing test case --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index b429619044..9083415a78 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -457,10 +457,12 @@ namespace osu.Game.Tests.Visual.UserInterface public override ModType Type => ModType.Conversion; } - private class TestUnimplementedModOsuRuleset : OsuRuleset + private class TestUnimplementedModOsuRuleset : OsuRuleset, ILegacyRuleset { public override string ShortName => "unimplemented"; + int ILegacyRuleset.LegacyID => -1; + public override IEnumerable GetModsFor(ModType type) { if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); From 92e22c57a77a2860e2ddc78ee85da46f2e5f43d4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 08:02:51 +0300 Subject: [PATCH 127/136] Introduce private `APIRuleset` for online ID equality comparison --- .../API/Requests/Responses/APIBeatmap.cs | 26 ++++++++++++++++++- osu.Game/Rulesets/RulesetInfo.cs | 3 --- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ebbac0dcab..dca60e54cb 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -98,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses public string MD5Hash => Checksum; - public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID }; + public IRulesetInfo Ruleset => new APIRuleset { OnlineID = RulesetID }; [JsonIgnore] public string Hash => throw new NotImplementedException(); @@ -106,5 +106,29 @@ namespace osu.Game.Online.API.Requests.Responses #endregion public bool Equals(IBeatmapInfo? other) => other is APIBeatmap b && this.MatchesOnlineID(b); + + private class APIRuleset : IRulesetInfo + { + public int OnlineID { get; set; } = -1; + + public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})"; + public string ShortName => nameof(APIRuleset); + public string InstantiationInfo => string.Empty; + + public Ruleset CreateInstance() => throw new NotImplementedException(); + + public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r); + + public int CompareTo(IRulesetInfo other) + { + if (!(other is APIRuleset ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other)); + + return OnlineID.CompareTo(ruleset.OnlineID); + } + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => OnlineID; + } } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index ba7c8d191d..88e3988431 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -44,9 +44,6 @@ namespace osu.Game.Rulesets if (ReferenceEquals(this, other)) return true; if (other == null) return false; - if (OnlineID >= 0 && other.OnlineID >= 0) - return OnlineID == other.OnlineID; - return ShortName == other.ShortName; } From b06caf2bf7b3a8705d26101f5e1fba464542a701 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 09:12:02 +0300 Subject: [PATCH 128/136] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 147f576c55..1a2859c851 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index dd10807ec2..a9c0226951 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6fbc468586..5e0b264834 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From f049f175d5d2d3a971bce6825d8cdbd2fedea42d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 15:19:55 +0900 Subject: [PATCH 129/136] Revert "Fix failing test case" This reverts commit c29cc78853f9cfb5efc4e923c7d9fca6fda8fbf7. --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 9083415a78..b429619044 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -457,12 +457,10 @@ namespace osu.Game.Tests.Visual.UserInterface public override ModType Type => ModType.Conversion; } - private class TestUnimplementedModOsuRuleset : OsuRuleset, ILegacyRuleset + private class TestUnimplementedModOsuRuleset : OsuRuleset { public override string ShortName => "unimplemented"; - int ILegacyRuleset.LegacyID => -1; - public override IEnumerable GetModsFor(ModType type) { if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); From f012f64fd1f441c96c589035afd63010b41d0b1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 16:45:57 +0900 Subject: [PATCH 130/136] Add test coverage checking carousel panel visual state after ruleset filter change --- .../SongSelect/TestSceneBeatmapCarousel.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 4e46901e08..540b820250 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -41,6 +41,68 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestExternalRulesetChange() + { + createCarousel(new List()); + + AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(0), + AllowConvertedBeatmaps = true, + }, false)); + + AddStep("add mixed ruleset beatmapset", () => + { + var testMixed = TestResources.CreateTestBeatmapSetInfo(3); + + for (int i = 0; i <= 2; i++) + { + testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); + } + + carousel.UpdateBeatmapSet(testMixed); + }); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1; + }); + + AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(1), + AllowConvertedBeatmaps = true, + }, false)); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 2 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 1) == 1; + }); + + AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(2), + AllowConvertedBeatmaps = true, + }, false)); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 2 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 2) == 1; + }); + } + [Test] public void TestScrollPositionMaintainedOnAdd() { From ccd664896185a0aff852fb8eaf6c47cb1b7bf125 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 09:22:09 +0300 Subject: [PATCH 131/136] Update pinned score container header to use localised title --- osu.Game/Overlays/Profile/Sections/RanksSection.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 02d8bd8c52..f48e33dc12 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -18,8 +18,7 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - // todo: update to use UsersStrings.ShowExtraTopRanksPinnedTitle once that exists. - new PaginatedScoreContainer(ScoreType.Pinned, User, "Pinned Scores"), + new PaginatedScoreContainer(ScoreType.Pinned, User, UsersStrings.ShowExtraTopRanksPinnedTitle), new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle), new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle) }; From beb3731c0b079c6b2bdaf1d051bd51b5350899f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 15:53:45 +0900 Subject: [PATCH 132/136] Standardise and combine base implementation of score submission requests These share too much yet have very different constructor signatures and property exposure. Just a clean-up pass as I begin to look at replay submission. --- .../Online/Rooms/SubmitRoomScoreRequest.cs | 32 ++------------- osu.Game/Online/Rooms/SubmitScoreRequest.cs | 41 +++++++++++++++++++ .../Online/Solo/SubmitSoloScoreRequest.cs | 33 ++------------- osu.Game/Screens/Play/RoomSubmittingPlayer.cs | 2 +- osu.Game/Screens/Play/SoloPlayer.cs | 2 +- 5 files changed, 51 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Online/Rooms/SubmitScoreRequest.cs diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index e24d113822..39193be1af 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -1,46 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Net.Http; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using osu.Game.Online.API; -using osu.Game.Online.Solo; using osu.Game.Scoring; namespace osu.Game.Online.Rooms { - public class SubmitRoomScoreRequest : APIRequest + public class SubmitRoomScoreRequest : SubmitScoreRequest { - private readonly long scoreId; private readonly long roomId; private readonly long playlistItemId; - private readonly SubmittableScore score; - public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo) + public SubmitRoomScoreRequest(ScoreInfo scoreInfo, long scoreId, long roomId, long playlistItemId) + : base(scoreInfo, scoreId) { - this.scoreId = scoreId; this.roomId = roomId; this.playlistItemId = playlistItemId; - score = new SubmittableScore(scoreInfo); } - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - - req.ContentType = "application/json"; - req.Method = HttpMethod.Put; - req.Timeout = 30000; - - req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - })); - - return req; - } - - protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}"; + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{ScoreId}"; } } diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs new file mode 100644 index 0000000000..14f858f007 --- /dev/null +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using Newtonsoft.Json; +using osu.Framework.IO.Network; +using osu.Game.Online.API; +using osu.Game.Online.Solo; +using osu.Game.Scoring; + +namespace osu.Game.Online.Rooms +{ + public abstract class SubmitScoreRequest : APIRequest + { + public readonly SubmittableScore Score; + + protected readonly long ScoreId; + + protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId) + { + Score = new SubmittableScore(scoreInfo); + this.ScoreId = scoreId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.ContentType = "application/json"; + req.Method = HttpMethod.Put; + req.Timeout = 30000; + + req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + })); + + return req; + } + } +} diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index 78ebddb2e6..77fd7b813b 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -1,46 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Net.Http; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; namespace osu.Game.Online.Solo { - public class SubmitSoloScoreRequest : APIRequest + public class SubmitSoloScoreRequest : SubmitScoreRequest { - public readonly SubmittableScore Score; - - private readonly long scoreId; - private readonly int beatmapId; - public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo) + public SubmitSoloScoreRequest(ScoreInfo scoreInfo, long scoreId, int beatmapId) + : base(scoreInfo, scoreId) { this.beatmapId = beatmapId; - this.scoreId = scoreId; - Score = new SubmittableScore(scoreInfo); } - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - - req.ContentType = "application/json"; - req.Method = HttpMethod.Put; - req.Timeout = 30000; - - req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - })); - - return req; - } - - protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{scoreId}"; + protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{ScoreId}"; } } diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs index 1002e7607f..fc96dfa965 100644 --- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs +++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { Debug.Assert(Room.RoomID.Value != null); - return new SubmitRoomScoreRequest(token, Room.RoomID.Value.Value, PlaylistItem.ID, score.ScoreInfo); + return new SubmitRoomScoreRequest(score.ScoreInfo, token, Room.RoomID.Value.Value, PlaylistItem.ID); } } } diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index eced2d142b..824c0072e3 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play Debug.Assert(beatmap.OnlineID > 0); - return new SubmitSoloScoreRequest(beatmap.OnlineID, token, score.ScoreInfo); + return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } } } From 28bbf34b1495908b1e145f8575f6ffaaf0979bd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 16:21:49 +0900 Subject: [PATCH 133/136] Remove unnecessary `this.` prefix --- osu.Game/Online/Rooms/SubmitScoreRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs index 14f858f007..b263262d2b 100644 --- a/osu.Game/Online/Rooms/SubmitScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Online.Rooms protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId) { Score = new SubmittableScore(scoreInfo); - this.ScoreId = scoreId; + ScoreId = scoreId; } protected override WebRequest CreateWebRequest() From 2ed3d5853144436f76260feb38f0d4d1c3a56e52 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 08:51:09 +0800 Subject: [PATCH 134/136] Ignore short spinners for relax mod --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 5d191119b9..905d55c64e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -87,8 +87,9 @@ namespace osu.Game.Rulesets.Osu.Mods requiresHold |= slider.Ball.IsHovered || h.IsHovered; break; - case DrawableSpinner _: - requiresHold = true; + case DrawableSpinner spinner: + if (spinner.HitObject.SpinsRequired > 0) + requiresHold = true; break; } } From 053f41d755999ec120c1674c958cfadadb7898f7 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 10:06:43 +0800 Subject: [PATCH 135/136] Simplify code --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 905d55c64e..10abd24e80 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSpinner spinner: - if (spinner.HitObject.SpinsRequired > 0) - requiresHold = true; + requiresHold = spinner.HitObject.SpinsRequired > 0; break; } } From 639d813d06fe2275b6220fc41b6133e0390de5f5 Mon Sep 17 00:00:00 2001 From: PercyDan <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 11:15:03 +0800 Subject: [PATCH 136/136] Don't override previous value Co-authored-by: Salman Ahmed --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 10abd24e80..1bf63ef6d4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSpinner spinner: - requiresHold = spinner.HitObject.SpinsRequired > 0; + requiresHold |= spinner.HitObject.SpinsRequired > 0; break; } }